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,29 @@
import bpy, sys
import numpy as np
bpy.ops.wm.open_mainfile(filepath=sys.argv[sys.argv.index("--")+1])
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 = len(me.vertices)
co = np.empty(n_v*3); me.vertices.foreach_get("co", co); co = co.reshape(-1,3)
img = next(i for i in bpy.data.images if "basecolor" in i.name.lower())
w,h = img.size
px = np.empty(w*h*4, dtype=np.float32); img.pixels.foreach_get(px)
rgb = px.reshape(h,w,4)[:,:,:3]
lv = np.empty(len(me.loops), dtype=np.int32); me.loops.foreach_get("vertex_index", lv)
uv = np.empty(len(me.loops)*2); me.uv_layers.active.data.foreach_get("uv", uv); uv = uv.reshape(-1,2)
fl = np.full(n_v, len(lv), dtype=np.int64); np.minimum.at(fl, lv, np.arange(len(lv)))
fl = np.minimum(fl, len(lv)-1)
c = rgb[(np.clip(uv[fl,1],0,1)*(h-1)).astype(int), (np.clip(uv[fl,0],0,1)*(w-1)).astype(int)]
mx = c.max(axis=1); mn = c.min(axis=1)
sat = np.where(mx>1e-5,(mx-mn)/np.maximum(mx,1e-5),0)
rb = c[:,0]/np.maximum(c[:,2],1e-5)
luma = 0.2126*c[:,0]+0.7152*c[:,1]+0.0722*c[:,2]
for nm, m in (
("strap front", (co[:,2]>0.75)&(co[:,2]<0.83)&(np.abs(co[:,0])>0.035)&(np.abs(co[:,0])<0.09)&(co[:,1]<0)),
("strap top", (co[:,2]>0.775)&(co[:,2]<0.805)&(np.abs(co[:,0])>0.035)&(np.abs(co[:,0])<0.09)),
("shoulder skin",(co[:,2]>0.75)&(co[:,2]<0.80)&(np.abs(co[:,0])>0.10)&(np.abs(co[:,0])<0.16)),
("chest skin", (co[:,2]>0.74)&(co[:,2]<0.77)&(np.abs(co[:,0])<0.03)&(co[:,1]<0)),
):
if m.sum():
print(f"{nm}: n={m.sum():6d} sat p25/50/75 = {np.percentile(sat[m],25):.3f}/{np.median(sat[m]):.3f}/{np.percentile(sat[m],75):.3f} rb {np.median(rb[m]):.2f} luma {np.median(luma[m]):.3f}")
print("STRAP_PROBE_DONE")