Files
animation/tools/rigbait_decimate.py
T
jeremy 3ba86b2ea8 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>
2026-08-06 15:55:43 -07:00

105 lines
4.8 KiB
Python

# Rig-bait decimate: make an AccuRig-loadable FBX from a raw ~1.9M-tri Tripo GLB.
# Successor to male_mesh_decimate.py (July male lane) for the rig-graft lane
# (.agents/plans/rig-graft-lane-2026-08-04.md). Differences from July, both Jeremy calls:
# * NO scale / recenter — the source GLBs stay at Tripo's native ~0.98 m, feet at Z=0.
# * Hands budget 40k (was 10k): the bait's hand density bounds how crisply per-finger
# weights transfer back onto the 170k-tri full-res hands in the graft step.
# Region cuts are bbox-relative (works at any scale): head above 86.5% of height,
# hands beyond 75.6% of half-span — same proportions the July tool used at 1.85 m.
# The bait is DISPOSABLE: it exists to carry a skeleton out of AccuRig. Never ship it.
#
# blender --background --python tools/rigbait_decimate.py -- <src.glb> <out_dir> <basename> [body head hand]
import bpy, os, sys, math
from mathutils import Vector
args = sys.argv[sys.argv.index("--") + 1:]
SRC, OUT, BASE = args[0], args[1], args[2]
T_BODY, T_HEAD, T_HAND = (int(a) for a in args[3:6]) if len(args) >= 6 else (24000, 14000, 40000)
os.makedirs(OUT, exist_ok=True)
bpy.ops.wm.read_factory_settings(use_empty=True)
bpy.ops.import_scene.gltf(filepath=SRC)
body = max((o for o in bpy.data.objects if o.type == "MESH"), key=lambda o: len(o.data.vertices))
me = body.data
bpy.ops.object.select_all(action="DESELECT")
body.select_set(True)
bpy.context.view_layer.objects.active = body
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
bb = [body.matrix_world @ Vector(c) for c in body.bound_box]
mnz, mxz = min(v.z for v in bb), max(v.z for v in bb)
half_span = max(abs(v.x) for v in bb)
NECK_Z = mnz + 0.865 * (mxz - mnz)
HAND_X = 0.756 * half_span
print(f"[bait] {BASE}: height={mxz-mnz:.3f} half_span={half_span:.3f} neck_z={NECK_Z:.3f} hand_x={HAND_X:.3f}")
# anti-facet shading fixes (shading only, geometry untouched)
try: bpy.ops.mesh.customdata_custom_splitnormals_clear()
except Exception: pass
if "sharp_edge" in me.attributes:
me.attributes.remove(me.attributes["sharp_edge"])
def is_head(v): return v.co.z > NECK_Z
def is_hand(v): return abs(v.co.x) > HAND_X
def group(name, pred):
g = body.vertex_groups.new(name=name)
g.add([v.index for v in me.vertices if pred(v)], 1.0, "REPLACE")
return g
head_tris = sum(1 for p in me.polygons if all(is_head(me.vertices[vi]) for vi in p.vertices))
hand_tris = sum(1 for p in me.polygons if all(is_hand(me.vertices[vi]) for vi in p.vertices))
print(f"[bait] regions: head_tris={head_tris} hand_tris={hand_tris} total={len(me.polygons)}")
if hand_tris < 20000 or head_tris < 20000:
raise SystemExit(f"[bait] region cut looks wrong (head={head_tris}, hands={hand_tris}) — check pose/thresholds")
group("P_BODY", lambda v: is_head(v) or is_hand(v))
group("P_HEAD", lambda v: not is_head(v))
group("P_HAND", lambda v: not is_hand(v))
targets = {"P_BODY": T_BODY, "P_HEAD": T_HEAD, "P_HAND": T_HAND}
region_now = {"P_BODY": len(me.polygons) - head_tris - hand_tris,
"P_HEAD": head_tris, "P_HAND": hand_tris}
for pg in ("P_BODY", "P_HEAD", "P_HAND"):
other_now = len(me.polygons) - region_now[pg]
dec = body.modifiers.new("dec", "DECIMATE")
dec.ratio = min(1.0, (targets[pg] + other_now) / max(1, len(me.polygons)))
dec.vertex_group = pg
dec.invert_vertex_group = True
dec.delimit = {"UV"}
bpy.ops.object.modifier_apply(modifier="dec")
me = body.data
region_now[pg] = targets[pg]
print(f"[bait] after {pg} pass: tris={len(me.polygons)}")
bpy.ops.object.shade_smooth()
# QA renders: whole body + hand closeup (the region the budgets exist to protect)
def render(name, loc, look_z):
cam = bpy.data.objects.get("cam")
if cam is None:
camd = bpy.data.cameras.new("cam"); cam = bpy.data.objects.new("cam", camd)
bpy.context.scene.collection.objects.link(cam)
bpy.context.scene.camera = cam
cam.location = loc
cam.rotation_euler = (math.radians(90), 0, 0)
cam.location.z = look_z
sc = bpy.context.scene
sc.render.engine = "BLENDER_WORKBENCH"
sc.display.shading.light = "STUDIO"
sc.display.shading.color_type = "TEXTURE"
sc.render.resolution_x = 800; sc.render.resolution_y = 800
sc.render.filepath = os.path.join(OUT, name)
bpy.ops.render.render(write_still=True)
h = mxz - mnz
render(f"{BASE}_qa_front.png", Vector((0, -2.0 * h, 0)), mnz + 0.5 * h)
render(f"{BASE}_qa_hand.png", Vector((HAND_X + 0.08 * h, -0.35 * h, 0)), mnz + 0.72 * h)
bpy.ops.object.select_all(action="DESELECT"); body.select_set(True)
for g in list(body.vertex_groups): body.vertex_groups.remove(g)
fbx_out = os.path.join(OUT, f"{BASE}.fbx")
bpy.ops.export_scene.fbx(filepath=fbx_out, use_selection=True, path_mode="COPY", embed_textures=True)
print(f"[bait] wrote {fbx_out} ({os.path.getsize(fbx_out)/1e6:.1f} MB), final tris={len(me.polygons)}")
print("[bait] DONE")