feat: clothing lane, character sources, and DCC bridges

Bulk import of the working lanes that were living untracked on the PC.

Content:
- characters/  Lena/male body lanes, bakes, texture work, run logs
- clothing/    garment pipeline, configs, gates, contract docs
- garments/    MD-authored garment sources (.zprj/.zpac)
- UAL-Lib/     Universal Animation Library 2 source (.blend/.fbx/.glb)
- tools/       blender_bridge, iclone_bridge, md_bridge, tailor, glm_agent
- docs/, plans/, dev/, .agents/plans/

Repo hygiene:
- .gitattributes: LFS now covers .blend, .zprj, .zpac, .obj, .npy and the
  Reallusion .iAvatar/.ccAvatar/.ccRestore containers. Without this the
  ~3.8 GB in this commit would land as raw blobs. .png/.jpg are left out
  on purpose — ~250 are already tracked raw and converting them would
  rewrite every one without shrinking history.
- .gitignore: exclude /accurig/ (~1 GB AccuRig program files, redistributable
  from Reallusion, nothing authored here) and /dev/null/ (git-lfs hook copies
  dropped by a `>/dev/null` redirect on Windows).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 15:55:43 -07:00
parent 3363209cac
commit 3ba86b2ea8
558 changed files with 68622 additions and 8 deletions
@@ -0,0 +1,129 @@
# Stage 32 (read-only): why did 4,502 triangles (3.66% of her surface) fail to rasterise into the
# new atlas? They render as grey tears, so they are not the harmless sub-texel slivers I assumed.
#
# blender --background --python 32_skipprobe.py -- <seamed.blend>
import bpy, sys
import numpy as np
BLEND = sys.argv[sys.argv.index("--") + 1]
bpy.ops.wm.open_mainfile(filepath=BLEND)
ob = max([o for o in bpy.data.objects if o.type == 'MESH'], key=lambda o: len(o.data.vertices))
me = ob.data
n_v, n_l, n_f = len(me.vertices), len(me.loops), len(me.polygons)
loops_v = np.empty(n_l, dtype=np.int32); me.loops.foreach_get("vertex_index", loops_v)
co = np.empty(n_v * 3); me.vertices.foreach_get("co", co); co = co.reshape(-1, 3)
ls = np.empty(n_f, dtype=np.int32); me.polygons.foreach_get("loop_start", ls)
lt = np.empty(n_f, dtype=np.int32); me.polygons.foreach_get("loop_total", lt)
li = ls[lt == 3]
IDX = np.stack([li, li + 1, li + 2], axis=1)
def uv_of(name):
a = np.empty(n_l * 2)
me.uv_layers[name].data.foreach_get("uv", a)
return a.reshape(-1, 2)
UN = uv_of("UVMap_atlas")
UO = uv_of("UVMap_tripo")
print(f"UVMap_atlas RAW range: u {UN[:,0].min():.4f}..{UN[:,0].max():.4f} "
f"v {UN[:,1].min():.4f}..{UN[:,1].max():.4f}")
print(f"UVMap_tripo RAW range: u {UO[:,0].min():.4f}..{UO[:,0].max():.4f} "
f"v {UO[:,1].min():.4f}..{UO[:,1].max():.4f}")
out01 = ((UN[:, 0] < -1e-6) | (UN[:, 0] > 1 + 1e-6)
| (UN[:, 1] < -1e-6) | (UN[:, 1] > 1 + 1e-6))
print(f"loops with UVMap_atlas OUTSIDE [0,1]: {int(out01.sum())} of {n_l} "
f"({100.0*out01.mean():.2f}%)")
W = 4096
Pn_raw = UN[IDX]
Pn_clip = np.clip(Pn_raw, 0, 1)
def uvarea(P):
return 0.5 * np.abs((P[:, 1, 0] - P[:, 0, 0]) * (P[:, 2, 1] - P[:, 0, 1])
- (P[:, 2, 0] - P[:, 0, 0]) * (P[:, 1, 1] - P[:, 0, 1]))
ar_raw = uvarea(Pn_raw) * W * W # in texels
ar_clip = uvarea(Pn_clip) * W * W
P3 = co[loops_v[IDX]]
a3 = 0.5 * np.linalg.norm(np.cross(P3[:, 1] - P3[:, 0], P3[:, 2] - P3[:, 0]), axis=1)
UNITM = 1.777 / (co[:, 2].max() - co[:, 2].min())
cm2 = a3 * UNITM * UNITM * 1e4
tiny_raw = ar_raw < 0.5
killed_by_clip = (ar_raw >= 0.5) & (ar_clip < 0.5)
print(f"\ntriangles with <0.5 texel of NEW uv area (before clipping): {int(tiny_raw.sum())} "
f"holding {cm2[tiny_raw].sum():.1f} cm2")
print(f"triangles that had area but LOSE it to [0,1] clipping: "
f"{int(killed_by_clip.sum())} holding {cm2[killed_by_clip].sum():.1f} cm2")
bad = tiny_raw | killed_by_clip
print(f"\ntotal suspect: {int(bad.sum())} triangles, {cm2[bad].sum():.1f} cm2 "
f"({100.0*cm2[bad].sum()/cm2.sum():.2f}% of surface)")
if bad.any():
cen = P3[bad].mean(axis=1)
z = (cen[:, 2] - co[:, 2].min()) / (co[:, 2].max() - co[:, 2].min())
print(" height distribution of the suspect faces (u = fraction of body height):")
hist, ed = np.histogram(z, bins=10, range=(0, 1))
for c, l_, h_ in zip(hist, ed[:-1], ed[1:]):
if c:
print(f" u {l_:.1f}-{h_:.1f}: {c:5d}")
print(f" their source-atlas uv area: median {np.median(uvarea(UO[IDX][bad])*W*W):.2f} texels "
f"(all triangles: {np.median(uvarea(UO[IDX])*W*W):.2f})")
# Are they degenerate in 3D as well, or real surface?
print(f"\n3D area of suspect faces: median {np.median(cm2[bad]):.4f} cm2 vs "
f"{np.median(cm2):.4f} cm2 for all faces")
# At the reported uniform 2.2 px/mm a 0.11 cm2 face should get ~53 texels, not <0.5. So the
# density is NOT uniform across all islands — only across the big ones that got reported.
# Measure it per island, over every island.
Q = 1 << 20
k = (loops_v.astype(np.int64) * Q * Q
+ np.round(np.clip(UN[:, 0], 0, 1) * (Q - 1)).astype(np.int64) * Q
+ np.round(np.clip(UN[:, 1], 0, 1) * (Q - 1)).astype(np.int64))
_, uvv = np.unique(k, return_inverse=True)
n_uvv = uvv.max() + 1
T = np.stack([uvv[li], uvv[li + 1], uvv[li + 2]], axis=1)
par = np.arange(n_uvv, dtype=np.int64)
def find(x):
r = x
while par[r] != r:
r = par[r]
while par[x] != r:
par[x], x = r, par[x]
return r
for a_, b_, c_ in T:
ra, rb, rc = find(a_), find(b_), find(c_)
if ra != rb:
par[rb] = ra
if ra != rc:
par[rc] = ra
_, isl = np.unique(np.array([find(i) for i in range(n_uvv)]), return_inverse=True)
fisl = isl[T[:, 0]]
n_isl = isl.max() + 1
iuv = np.bincount(fisl, weights=uvarea(Pn_clip), minlength=n_isl)
i3d = np.bincount(fisl, weights=a3, minlength=n_isl)
inf_ = np.bincount(fisl, minlength=n_isl)
UN_M = UNITM
dens = np.where(i3d > 0, np.sqrt(np.maximum(iuv, 0) / np.maximum(i3d, 1e-20))
* W / (UN_M * 1000.0), np.nan)
print(f"\n=== PER-ISLAND TEXEL DENSITY, all {n_isl} islands (px/mm) ===")
fd = dens[np.isfinite(dens)]
for p in (1, 5, 25, 50, 75, 95, 99):
print(f" p{p:<2d} {np.percentile(fd, p):.4f}")
low = np.isfinite(dens) & (dens < 0.5)
print(f" islands under 0.5 px/mm: {int(low.sum())} of {n_isl}, holding "
f"{(i3d[low]*UN_M*UN_M*1e4).sum():.1f} cm2 "
f"({100.0*i3d[low].sum()/i3d.sum():.2f}% of surface) in {int(inf_[low].sum())} faces")
big = np.isfinite(dens) & (dens > 1.5)
print(f" islands over 1.5 px/mm: {int(big.sum())} holding "
f"{100.0*i3d[big].sum()/i3d.sum():.2f}% of surface")
print("SKIPPROBE_DONE")