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,80 @@
# Stage 34 (read-only): measure the APPROVED concept turnaround against our body, in the same
# units, so "uniform skin" stops being an adjective.
#
# blender --background --python 34_target_tone.py -- <concept_dir> <our_render.png>
#
# Both inputs are renders of a body on a flat grey background, so the comparison is like for like.
# Redness (r-g) is the useful statistic: it barely moves with lighting, so a body lit by one light
# should hold it nearly constant. Lena's Tripo texture does not — her feet measure 50% redder than
# her belly — and the question is what the approved reference does.
import bpy, sys, os
import numpy as np
argv = sys.argv[sys.argv.index("--") + 1:]
CONCEPT_DIR, OURS = argv[0], argv[1]
def load(p):
im = bpy.data.images.load(p)
w, h = im.size
b = np.empty(w * h * 4, dtype=np.float32)
im.pixels.foreach_get(b)
a = b.reshape(h, w, 4)[:, :, :3].copy()
bpy.data.images.remove(im)
return a[::-1] # Blender rows are bottom-up; flip to image order
def body_mask(A):
# background is flat and desaturated; skin is not
sat = A.max(axis=2) - A.min(axis=2)
return (sat > 0.06) & (A.max(axis=2) > 0.15)
def report(name, A):
m = body_mask(A)
if m.sum() < 500:
print(f"{name}: no body found")
return
ys, xs = np.nonzero(m)
y0, y1 = ys.min(), ys.max()
rg = (A[:, :, 0] - A[:, :, 1])
luma = A.mean(axis=2)
print(f"\n=== {name} === {m.sum()} body pixels, rows {y0}..{y1}")
print(f" WHOLE BODY r-g mean {rg[m].mean():.3f} std {rg[m].std():.3f} "
f"luma mean {luma[m].mean():.3f}")
# slice by height: 0 = top of the figure (head), 1 = bottom (feet)
print(" by height band (0=head .. 1=feet):")
rows = []
for i in range(10):
a = y0 + (y1 - y0) * i / 10.0
b = y0 + (y1 - y0) * (i + 1) / 10.0
band = np.zeros_like(m)
band[int(a):int(b) + 1, :] = True
mm = m & band
if mm.sum() < 200:
continue
rows.append((i / 10.0, rg[mm].mean(), luma[mm].mean(), int(mm.sum())))
print(f" {i/10.0:.1f}-{(i+1)/10.0:.1f} r-g {rg[mm].mean():.3f} "
f"luma {luma[mm].mean():.3f} n={int(mm.sum())}")
if rows:
arr = np.array([r[1] for r in rows])
print(f" SPREAD of r-g across height bands: {arr.max()-arr.min():.3f} "
f"(min {arr.min():.3f} at {rows[int(np.argmin(arr))][0]:.1f}, "
f"max {arr.max():.3f} at {rows[int(np.argmax(arr))][0]:.1f})")
return rg[m], luma[m]
res = {}
for f in sorted(os.listdir(CONCEPT_DIR)):
if f.lower().endswith(".png"):
res[f] = report("CONCEPT " + f, load(os.path.join(CONCEPT_DIR, f)))
ours = report("OURS " + os.path.basename(OURS), load(OURS))
print("\n" + "=" * 72)
print("VERDICT — how much does redness vary over the body?")
for k, v in res.items():
if v:
print(f" concept {k:<32} r-g std {v[0].std():.4f}")
if ours:
print(f" ours {os.path.basename(OURS):<32} r-g std {ours[0].std():.4f}")
print("TARGET_TONE_DONE")