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:
@@ -0,0 +1,159 @@
|
||||
# Probe the pre-decimated Tripo sculpt: is the sports top a SEPARABLE shell?
|
||||
# Answers, per connected component (after welding UV-seam splits):
|
||||
# size, z-range, open-boundary edge count, and what fraction of its faces are painted
|
||||
# garment (colour-keyed on lena+glb_basecolor via each face's own UV texels).
|
||||
#
|
||||
# blender --background --python 01_probe.py -- <copy.glb> <checkpoint.blend>
|
||||
import bpy, bmesh, sys, time
|
||||
import numpy as np
|
||||
|
||||
argv = sys.argv[sys.argv.index("--") + 1:]
|
||||
SRC, CKPT = argv[0], argv[1]
|
||||
t0 = time.time()
|
||||
|
||||
|
||||
def log(m):
|
||||
print(f"[probe {time.time()-t0:6.1f}s] {m}", flush=True)
|
||||
|
||||
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
bpy.ops.import_scene.gltf(filepath=SRC)
|
||||
ob = max([o for o in bpy.data.objects if o.type == 'MESH'],
|
||||
key=lambda o: len(o.data.vertices))
|
||||
me = ob.data
|
||||
log(f"imported: {len(me.vertices)}v {len(me.polygons)}f, object '{ob.name}'")
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(me)
|
||||
bmesh.ops.remove_doubles(bm, verts=list(bm.verts), dist=1e-6)
|
||||
bm.to_mesh(me)
|
||||
bm.free()
|
||||
me.update()
|
||||
log(f"after weld 1e-6: {len(me.vertices)}v {len(me.polygons)}f")
|
||||
|
||||
# save checkpoint so later stages skip the import+weld cost
|
||||
bpy.context.preferences.filepaths.save_version = 0 # no .blend1 autosave
|
||||
bpy.ops.wm.save_as_mainfile(filepath=CKPT)
|
||||
log(f"checkpoint saved: {CKPT}")
|
||||
|
||||
n_v = len(me.vertices)
|
||||
n_f = len(me.polygons)
|
||||
|
||||
# ---- connected components over faces (edge-connected), union-find in numpy ----
|
||||
# face -> vertices
|
||||
loop_tot = np.empty(n_f, dtype=np.int32)
|
||||
me.polygons.foreach_get("loop_total", loop_tot)
|
||||
loop_start = np.empty(n_f, dtype=np.int32)
|
||||
me.polygons.foreach_get("loop_start", loop_start)
|
||||
loops_v = np.empty(len(me.loops), dtype=np.int32)
|
||||
me.loops.foreach_get("vertex_index", loops_v)
|
||||
|
||||
parent = np.arange(n_v, dtype=np.int64)
|
||||
|
||||
|
||||
def find(a):
|
||||
root = a
|
||||
while parent[root] != root:
|
||||
root = parent[root]
|
||||
while parent[a] != root:
|
||||
parent[a], a = root, parent[a]
|
||||
return root
|
||||
|
||||
|
||||
# union via edges
|
||||
n_e = len(me.edges)
|
||||
ev = np.empty(n_e * 2, dtype=np.int32)
|
||||
me.edges.foreach_get("vertices", ev)
|
||||
ev = ev.reshape(-1, 2)
|
||||
for a, b in ev:
|
||||
ra, rb = find(a), find(b)
|
||||
if ra != rb:
|
||||
parent[rb] = ra
|
||||
log("union-find done")
|
||||
|
||||
root_of = np.array([find(i) for i in range(n_v)], dtype=np.int64)
|
||||
uniq, inv, counts = np.unique(root_of, return_inverse=True, return_counts=True)
|
||||
order = np.argsort(-counts)
|
||||
log(f"components: {len(uniq)}")
|
||||
|
||||
# vertex positions
|
||||
co = np.empty(n_v * 3, dtype=np.float64)
|
||||
me.vertices.foreach_get("co", co)
|
||||
co = co.reshape(-1, 3)
|
||||
|
||||
# boundary edges per component: edge belongs to 1 face only
|
||||
# count faces per edge
|
||||
edge_face_count = np.zeros(n_e, dtype=np.int32)
|
||||
for p in me.polygons:
|
||||
for ek in p.edge_keys:
|
||||
pass # too slow; use loops instead
|
||||
# faster: build edge keys from loops via me.polygons edge indices
|
||||
# Blender exposes loop edges: me.loops[i].edge_index
|
||||
loops_e = np.empty(len(me.loops), dtype=np.int32)
|
||||
me.loops.foreach_get("edge_index", loops_e)
|
||||
np.add.at(edge_face_count, loops_e, 1)
|
||||
boundary_edge = edge_face_count == 1
|
||||
log(f"boundary edges total: {boundary_edge.sum()}")
|
||||
|
||||
# ---- colour key per vertex (first-loop UV), calibrated on this texture ----
|
||||
img = None
|
||||
for i in bpy.data.images:
|
||||
if "basecolor" in i.name.lower():
|
||||
img = i
|
||||
break
|
||||
if img is None:
|
||||
img = max(bpy.data.images, key=lambda i: i.size[0] * i.size[1])
|
||||
w, h = img.size
|
||||
log(f"texture '{img.name}' {w}x{h}")
|
||||
px = np.empty(w * h * 4, dtype=np.float32)
|
||||
img.pixels.foreach_get(px)
|
||||
rgb = px.reshape(h, w, 4)[:, :, :3]
|
||||
|
||||
uvl = me.uv_layers.active.data
|
||||
uv = np.empty(len(me.loops) * 2, dtype=np.float64)
|
||||
uvl.foreach_get("uv", uv)
|
||||
uv = uv.reshape(-1, 2)
|
||||
# first loop per vertex
|
||||
first_loop = np.full(n_v, -1, dtype=np.int64)
|
||||
for li in range(len(loops_v) - 1, -1, -1):
|
||||
first_loop[loops_v[li]] = li
|
||||
has_uv = first_loop >= 0
|
||||
vx = np.clip(uv[first_loop, 0], 0, 1) * (w - 1)
|
||||
vy = np.clip(uv[first_loop, 1], 0, 1) * (h - 1) # bpy-imported UVs: already flipped
|
||||
vcol = rgb[vy.astype(int), vx.astype(int)]
|
||||
r, g, b = vcol[:, 0], vcol[:, 1], vcol[:, 2]
|
||||
mx = vcol.max(axis=1)
|
||||
mn = vcol.min(axis=1)
|
||||
sat = np.where(mx > 1e-5, (mx - mn) / np.maximum(mx, 1e-5), 0)
|
||||
rb = r / np.maximum(b, 1e-5)
|
||||
|
||||
# calibrate: thigh skin (z 0.28-0.34) vs briefs centre (z 0.50-0.56 front)
|
||||
thigh = (co[:, 2] > 0.28) & (co[:, 2] < 0.34)
|
||||
briefs = (co[:, 2] > 0.50) & (co[:, 2] < 0.56) & (co[:, 1] < 0) & (np.abs(co[:, 0]) < 0.05)
|
||||
bra = (co[:, 2] > 0.64) & (co[:, 2] < 0.72) & (co[:, 1] < 0) & (np.abs(co[:, 0]) < 0.05)
|
||||
for nm, s in (("thigh skin", thigh), ("briefs", briefs), ("bra front", bra)):
|
||||
if s.sum():
|
||||
log(f" [{nm}] n={s.sum()} sat p50={np.median(sat[s]):.3f} rb p50={np.median(rb[s]):.3f} "
|
||||
f"rgb ({np.median(r[s]):.3f},{np.median(g[s]):.3f},{np.median(b[s]):.3f})")
|
||||
|
||||
# pick thresholds midway between garment and skin medians
|
||||
sat_thr = (np.median(sat[briefs]) + np.median(sat[thigh])) / 2 if briefs.sum() and thigh.sum() else 0.45
|
||||
rb_thr = (np.median(rb[briefs]) + np.median(rb[thigh])) / 2 if briefs.sum() and thigh.sum() else 1.9
|
||||
garment_v = (sat < sat_thr) & (rb < rb_thr)
|
||||
log(f"thresholds: sat<{sat_thr:.3f} rb<{rb_thr:.2f} -> {garment_v.sum()} garment verts")
|
||||
|
||||
# ---- per-component report (top 12 by size) ----
|
||||
# per-vertex boundary flag
|
||||
vb = np.zeros(n_v, dtype=bool)
|
||||
vb[ev[boundary_edge].ravel()] = True
|
||||
|
||||
print("\n=== COMPONENTS (top 12 by vertex count) ===")
|
||||
for ci in order[:12]:
|
||||
root = uniq[ci]
|
||||
m = root_of == root
|
||||
zc = co[m, 2]
|
||||
gfrac = garment_v[m].mean()
|
||||
bcount = vb[m].sum()
|
||||
print(f"comp root={root}: verts={m.sum():7d} z[{zc.min():.3f},{zc.max():.3f}] "
|
||||
f"garment-painted={100*gfrac:5.1f}% boundary-verts={bcount}")
|
||||
print("PROBE_DONE")
|
||||
Reference in New Issue
Block a user