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,147 @@
|
||||
# Tailor measurements from a Quaternius-skinned body GLB.
|
||||
# Slices the mesh at scanned heights, restricted to vertices whose dominant
|
||||
# vertex group is in an allowed bone set (so arms don't inflate the chest).
|
||||
# Run: blender --background --python measure_body.py -- <body.glb> <out.json>
|
||||
import bpy, sys, json, math
|
||||
|
||||
argv = sys.argv[sys.argv.index("--") + 1:]
|
||||
SRC, OUT = argv[0], argv[1]
|
||||
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
bpy.ops.import_scene.gltf(filepath=SRC)
|
||||
|
||||
mesh_obj = max((o for o in bpy.data.objects if o.type == "MESH"),
|
||||
key=lambda o: len(o.data.vertices))
|
||||
arm_obj = next(o for o in bpy.data.objects if o.type == "ARMATURE")
|
||||
|
||||
# --- world-space bone head positions (rest pose) ---
|
||||
bones = {}
|
||||
for b in arm_obj.data.bones:
|
||||
bones[b.name] = arm_obj.matrix_world @ b.head_local
|
||||
|
||||
# --- world-space vertex positions + dominant vertex group ---
|
||||
mw = mesh_obj.matrix_world
|
||||
group_name = {g.index: g.name for g in mesh_obj.vertex_groups}
|
||||
verts = [] # (co_world, dominant_group_name)
|
||||
for v in mesh_obj.data.vertices:
|
||||
best, bw = None, 0.0
|
||||
for g in v.groups:
|
||||
if g.weight > bw:
|
||||
bw, best = g.weight, group_name.get(g.group)
|
||||
verts.append((mw @ v.co, best))
|
||||
|
||||
edges = [(e.vertices[0], e.vertices[1]) for e in mesh_obj.data.edges]
|
||||
|
||||
def hull_perimeter(pts):
|
||||
"""2D convex hull (monotone chain) perimeter."""
|
||||
pts = sorted(set(pts))
|
||||
if len(pts) < 3:
|
||||
return 0.0
|
||||
def cross(o, a, b):
|
||||
return (a[0]-o[0])*(b[1]-o[1]) - (a[1]-o[1])*(b[0]-o[0])
|
||||
lower, upper = [], []
|
||||
for p in pts:
|
||||
while len(lower) >= 2 and cross(lower[-2], lower[-1], p) <= 0:
|
||||
lower.pop()
|
||||
lower.append(p)
|
||||
for p in reversed(pts):
|
||||
while len(upper) >= 2 and cross(upper[-2], upper[-1], p) <= 0:
|
||||
upper.pop()
|
||||
upper.append(p)
|
||||
hull = lower[:-1] + upper[:-1]
|
||||
return sum(math.dist(hull[i], hull[(i+1) % len(hull)]) for i in range(len(hull)))
|
||||
|
||||
def slice_circ(z, allowed):
|
||||
"""Perimeter of the mesh cross-section at height z, using edges whose
|
||||
both endpoints' dominant group is in `allowed` (prefix match)."""
|
||||
def ok(g):
|
||||
return g is not None and any(g.startswith(a) for a in allowed)
|
||||
pts = []
|
||||
for i0, i1 in edges:
|
||||
c0, g0 = verts[i0]
|
||||
c1, g1 = verts[i1]
|
||||
if not (ok(g0) and ok(g1)):
|
||||
continue
|
||||
if (c0.z - z) * (c1.z - z) < 0:
|
||||
t = (z - c0.z) / (c1.z - c0.z)
|
||||
pts.append((c0.x + t*(c1.x-c0.x), c0.y + t*(c1.y-c0.y)))
|
||||
return hull_perimeter(pts)
|
||||
|
||||
def scan(z0, z1, allowed, mode, steps=40):
|
||||
"""Max or min nonzero circumference in [z0,z1]."""
|
||||
best_c, best_z = (0.0, None) if mode == "max" else (1e9, None)
|
||||
for i in range(steps + 1):
|
||||
z = z0 + (z1 - z0) * i / steps
|
||||
c = slice_circ(z, allowed)
|
||||
if c <= 0:
|
||||
continue
|
||||
if (mode == "max" and c > best_c) or (mode == "min" and c < best_c):
|
||||
best_c, best_z = c, z
|
||||
return best_c, best_z
|
||||
|
||||
TORSO = ["pelvis", "spine_", "neck_", "clavicle_", "Breast"]
|
||||
HIPZONE = ["pelvis", "spine_01", "thigh_"]
|
||||
|
||||
z_pelvis = bones["pelvis"].z
|
||||
z_neck = bones["neck_01"].z
|
||||
z_thigh = bones["thigh_l"].z
|
||||
z_calf = bones["calf_l"].z
|
||||
z_foot = bones["foot_l"].z
|
||||
|
||||
zmin = min(c.z for c, _ in verts)
|
||||
zmax = max(c.z for c, _ in verts)
|
||||
|
||||
chest, z_chest = scan(z_pelvis + 0.55*(z_neck - z_pelvis), z_neck - 0.05*(z_neck - z_pelvis), TORSO, "max")
|
||||
waist, z_waist = scan(z_pelvis + 0.05, z_chest - 0.03, TORSO, "min")
|
||||
hip, z_hip = scan(z_thigh, z_pelvis + 0.06, HIPZONE, "max")
|
||||
thigh, z_th = scan(z_thigh - 0.6*(z_thigh - z_calf), z_thigh - 0.05*(z_thigh - z_calf), ["thigh_l"], "max")
|
||||
neckc, z_nk = scan(z_neck, z_neck + 0.5*(zmax - z_neck), ["neck_", "Head", "spine_03", "clavicle_"], "min")
|
||||
# upper arm circumference (sleeve): slice perpendicular-ish — arms in T-pose are
|
||||
# horizontal, so slice by X instead of Z for the left arm.
|
||||
def slice_circ_x(x, allowed):
|
||||
def ok(g):
|
||||
return g is not None and any(g.startswith(a) for a in allowed)
|
||||
pts = []
|
||||
for i0, i1 in edges:
|
||||
c0, g0 = verts[i0]
|
||||
c1, g1 = verts[i1]
|
||||
if not (ok(g0) and ok(g1)):
|
||||
continue
|
||||
if (c0.x - x) * (c1.x - x) < 0:
|
||||
t = (x - c0.x) / (c1.x - c0.x)
|
||||
pts.append((c0.y + t*(c1.y-c0.y), c0.z + t*(c1.z-c0.z)))
|
||||
return hull_perimeter(pts)
|
||||
|
||||
x_ua = bones["upperarm_l"].x
|
||||
x_la = bones["lowerarm_l"].x
|
||||
bicep = 0.0
|
||||
xb = None
|
||||
for i in range(21):
|
||||
x = x_ua + (x_la - x_ua) * (0.15 + 0.5 * i / 20)
|
||||
c = slice_circ_x(x, ["upperarm_l"])
|
||||
if c > bicep:
|
||||
bicep, xb = c, x
|
||||
|
||||
d = math.dist
|
||||
m = {
|
||||
"source": SRC,
|
||||
"units": "meters (glTF)",
|
||||
"height_total": zmax - zmin,
|
||||
"chest_circ": chest, "chest_z": z_chest,
|
||||
"waist_circ": waist, "waist_z": z_waist,
|
||||
"hip_circ": hip, "hip_z": z_hip,
|
||||
"thigh_circ": thigh, "thigh_z": z_th,
|
||||
"neck_circ": neckc, "neck_z": z_nk,
|
||||
"bicep_circ": bicep,
|
||||
"shoulder_width": d(bones["upperarm_l"], bones["upperarm_r"]),
|
||||
"arm_len_shoulder_to_wrist": d(bones["upperarm_l"], bones["lowerarm_l"]) + d(bones["lowerarm_l"], bones["hand_l"]),
|
||||
"nape_to_pelvis": z_neck - z_pelvis,
|
||||
"crotch_height": z_thigh,
|
||||
"pelvis_height": z_pelvis,
|
||||
"knee_height": z_calf,
|
||||
"ankle_height": z_foot,
|
||||
"shoulder_z": bones["clavicle_l"].z,
|
||||
}
|
||||
with open(OUT, "w") as f:
|
||||
json.dump({k: (round(v, 4) if isinstance(v, float) else v) for k, v in m.items()}, f, indent=2)
|
||||
print("WROTE", OUT)
|
||||
Reference in New Issue
Block a user