Files

115 lines
5.4 KiB
Python
Raw Permalink Normal View History

# Mako work lane, stage 01: decimate the Tripo original with the HEAD FULLY PROTECTED.
# Body -> 24k tris, hands -> 40k tris, head above the neck line -> UNTOUCHED (full
# Tripo resolution). Jeremy's ask 2026-08-11: a decimated mako that cuts at the neck
# like the Lena head graft did, so the face keeps every tri for later head work.
#
# Differences from tools/rigbait_decimate.py (same region geometry, different intent):
# * NO P_HEAD pass — head verts sit in every protect group instead.
# * Output is a GLB master (packed textures carried), not an FBX rig bait.
# Same as the bait: NO scale / recenter (native ~0.98 m, feet at Z=0); region cuts are
# bbox-relative — neck at 86.5% of height, hands beyond 75.6% of half-span.
#
# blender --background --python 01_decimate_fullhead.py -- <src.glb> <out.glb> <review_dir> [body hand]
import bpy, os, sys, math
from mathutils import Vector
args = sys.argv[sys.argv.index("--") + 1:]
SRC, OUT_GLB, REVIEW = args[0], args[1], args[2]
T_BODY, T_HAND = (int(a) for a in args[3:5]) if len(args) >= 5 else (24000, 40000)
os.makedirs(os.path.dirname(os.path.abspath(OUT_GLB)), exist_ok=True)
os.makedirs(REVIEW, exist_ok=True)
bpy.context.preferences.filepaths.save_version = 0
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"[fullhead] 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_tris0 = sum(1 for p in me.polygons if all(is_head(me.vertices[vi]) for vi in p.vertices))
hand_tris0 = sum(1 for p in me.polygons if all(is_hand(me.vertices[vi]) for vi in p.vertices))
print(f"[fullhead] regions: head_tris={head_tris0} hand_tris={hand_tris0} total={len(me.polygons)}")
if head_tris0 < 20000 or hand_tris0 < 20000:
raise SystemExit(f"[fullhead] region cut looks wrong (head={head_tris0}, hands={hand_tris0})")
# Two passes only. The head is inside every protect group, so no pass ever touches it.
group("P_BODY", lambda v: is_head(v) or is_hand(v)) # pass decimates the body
group("P_HAND", lambda v: not is_hand(v)) # pass decimates the hands (head is not a hand)
targets = {"P_BODY": T_BODY, "P_HAND": T_HAND}
region_now = {"P_BODY": len(me.polygons) - head_tris0 - hand_tris0, "P_HAND": hand_tris0}
for pg in ("P_BODY", "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"[fullhead] after {pg} pass: tris={len(me.polygons)}")
# gate: the head must have survived bit-for-tri
head_tris1 = sum(1 for p in me.polygons if all(is_head(me.vertices[vi]) for vi in p.vertices))
print(f"[fullhead] head tris {head_tris0} -> {head_tris1}")
if head_tris1 < head_tris0 * 0.999:
raise SystemExit(f"[fullhead] HEAD WAS DECIMATED ({head_tris0} -> {head_tris1}) — abort, do not use output")
bpy.ops.object.shade_smooth()
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(REVIEW, name)
bpy.ops.render.render(write_still=True)
h = mxz - mnz
render("qa_front.png", Vector((0, -2.0 * h, 0)), mnz + 0.5 * h)
render("qa_head.png", Vector((0, -0.45 * h, 0)), mnz + 0.93 * h)
render("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)
bpy.ops.export_scene.gltf(
filepath=OUT_GLB, export_format="GLB", export_yup=True, export_apply=False,
export_animations=False, export_skins=False, export_morph=False)
print(f"[fullhead] wrote {OUT_GLB} ({os.path.getsize(OUT_GLB)/1e6:.1f} MB), "
f"final tris={len(me.polygons)} (head {head_tris1} + budgets {T_BODY}+{T_HAND})")
print("[fullhead] DONE")