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
+98
View File
@@ -0,0 +1,98 @@
# Framed closeup renders of a body GLB, for judging geometry and texture separately.
#
# blender --background --python tools/_render_body_closeup.py -- \
# --glb <a.glb> --out <a.png> [--yaw 0] [--region torso|hip|full] [--clay] [--wire]
#
# Why not tools/_glb_render.py: that one frames the whole-scene bounding box, and every body
# GLB here carries a stray 1 m-radius Icosphere at the origin, so the figure ends up small in
# frame. This one frames an explicit anatomical band on the BODY mesh only.
# --clay drops all materials for a neutral grey surface, which is the only honest way to judge
# silhouette and shading artifacts — a painted-on bra shadow otherwise reads as a mesh dent.
import bpy, sys, math, argparse
from mathutils import Vector
REGIONS = { # (z_lo, z_hi) in metres on a 1.777 m body
"torso": (1.00, 1.52),
"hip": (0.78, 1.12),
"full": (0.00, 1.80),
}
def main():
argv = sys.argv[sys.argv.index("--") + 1:]
ap = argparse.ArgumentParser()
ap.add_argument("--glb", required=True)
ap.add_argument("--out", required=True)
ap.add_argument("--yaw", type=float, default=0.0)
ap.add_argument("--region", default="torso", choices=sorted(REGIONS))
ap.add_argument("--clay", action="store_true")
ap.add_argument("--wire", action="store_true")
ap.add_argument("--no-shadow", action="store_true",
help="disable cast shadows — for measurement renders; in a T-pose the arm "
"throws sharp finger-shadow bands across the flank that edge metrics "
"misread as surface creases")
ap.add_argument("--res", type=int, default=900)
a = ap.parse_args(argv)
bpy.ops.wm.read_factory_settings(use_empty=True)
bpy.ops.import_scene.gltf(filepath=a.glb)
meshes = [o for o in bpy.data.objects if o.type == 'MESH']
body = max(meshes, key=lambda o: len(o.data.vertices))
for o in meshes: # hide the stray Icosphere and anything else
if o is not body:
o.hide_render = True
if a.clay:
body.data.materials.clear()
m = bpy.data.materials.new("Clay")
m.use_nodes = True
bsdf = m.node_tree.nodes["Principled BSDF"]
bsdf.inputs["Base Color"].default_value = (0.62, 0.60, 0.58, 1.0)
bsdf.inputs["Roughness"].default_value = 0.45
body.data.materials.append(m)
if a.wire:
body.modifiers.new("Wire", 'WIREFRAME').thickness = 0.0012
z_lo, z_hi = REGIONS[a.region]
pts = [body.matrix_world @ v.co for v in body.data.vertices]
band = [p for p in pts if z_lo <= p.z <= z_hi] or pts
ctr = Vector((0.0, 0.0, (z_lo + z_hi) / 2))
xs = [p.x for p in band]
ctr.y = sum(p.y for p in band) / len(band)
span = max(max(xs) - min(xs), z_hi - z_lo)
w = bpy.data.worlds.new("W")
w.color = (0.22, 0.22, 0.24)
bpy.context.scene.world = w
# three-point-ish lighting: a key sun plus a softer fill, so volume reads without the
# single-sun hotspot that flattens a bust into one bright blob
key = bpy.data.objects.new("Key", bpy.data.lights.new("Key", 'SUN'))
key.data.energy = 3.0
key.data.use_shadow = not a.no_shadow
key.rotation_euler = (math.radians(62), 0, math.radians(35 + a.yaw))
bpy.context.collection.objects.link(key)
fill = bpy.data.objects.new("Fill", bpy.data.lights.new("Fill", 'SUN'))
fill.data.energy = 1.1
fill.rotation_euler = (math.radians(75), 0, math.radians(a.yaw - 110))
bpy.context.collection.objects.link(fill)
cam = bpy.data.objects.new("Cam", bpy.data.cameras.new("Cam"))
cam.data.lens = 85
bpy.context.collection.objects.link(cam)
yaw = math.radians(a.yaw)
dist = span * 3.1
cam.location = ctr + Vector((math.sin(yaw) * dist, -math.cos(yaw) * dist, 0.0))
cam.rotation_euler = (ctr - cam.location).to_track_quat('-Z', 'Y').to_euler()
bpy.context.scene.camera = cam
scn = bpy.context.scene
scn.render.engine = 'BLENDER_EEVEE' if bpy.app.version >= (4, 2) else 'BLENDER_EEVEE_NEXT'
scn.render.resolution_x = scn.render.resolution_y = a.res
scn.render.filepath = a.out
bpy.ops.render.render(write_still=True)
print("CLOSEUP_OK", a.out)
main()