# 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 -- [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")