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,77 @@
# Stage 35 (read-only): does the approved concept turnaround describe OUR body?
#
# blender --background --python 35_silhouette.py -- <concept_front.png> <our_front.png>
#
# This decides how the turnaround can be used. If the silhouettes match, the concept renders could
# be projected onto the mesh as a texture source. If they do not, they are a style and tone
# reference only — and quietly projecting them would smear one body's paint onto another's shape.
# Widths are normalised by figure height, so framing and resolution cancel.
import bpy, sys, os
import numpy as np
argv = sys.argv[sys.argv.index("--") + 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]
def profile(p):
A = load(p)
sat = A.max(axis=2) - A.min(axis=2)
m = (sat > 0.06) & (A.max(axis=2) > 0.15)
ys, xs = np.nonzero(m)
y0, y1 = ys.min(), ys.max()
Hf = float(y1 - y0)
cx = xs.mean()
out = []
for t in np.arange(0.02, 1.0, 0.02):
r = int(round(y0 + t * Hf))
row = np.nonzero(m[r])[0]
if len(row) < 2:
out.append((t, np.nan, np.nan))
continue
# full extent (includes arms where they are out), and the CENTRAL run (the torso/leg)
full = (row.max() - row.min()) / Hf
# central run: walk out from the pixel nearest the body's x centre
k = row[np.argmin(np.abs(row - cx))]
lo = k
while lo - 1 in set(row.tolist()) if False else (lo - 1 >= 0 and m[r, lo - 1]):
lo -= 1
hi = k
while hi + 1 < m.shape[1] and m[r, hi + 1]:
hi += 1
out.append((t, full, (hi - lo) / Hf))
return np.array(out), Hf
A, ha = profile(argv[0])
B, hb = profile(argv[1])
print(f"concept figure height {ha:.0f} px ours {hb:.0f} px")
print("\n t concept_full ours_full concept_core ours_core core_diff")
diffs = []
for (t, fa, ca), (_, fb, cb) in zip(A, B):
if np.isnan(ca) or np.isnan(cb):
continue
d = cb - ca
# the central run is only meaningful where it is a single body part:
# below the arms (t>0.30) and above the ankles
flag = ""
if 0.30 < t < 0.95:
diffs.append(d)
if abs(d) > 0.02:
flag = " <-- differs"
print(f" {t:.2f} {fa:9.3f} {fb:9.3f} {ca:10.3f} {cb:9.3f} {d:+8.3f}{flag}")
if diffs:
diffs = np.array(diffs)
print(f"\nCORE WIDTH (torso/legs, t 0.30-0.95), ours minus concept, as fraction of height:")
print(f" mean {diffs.mean():+.4f} mean|d| {np.abs(diffs).mean():.4f} "
f"max|d| {np.abs(diffs).max():.4f}")
print(f" bands differing by >2% of height: {int((np.abs(diffs)>0.02).sum())} of {len(diffs)}")
print("SILHOUETTE_DONE")