ship(mako): mako_base_v01 — fullhead body (24k body / full-res 158k head) as male Ariki default
Decimate body-only (01), AccuRig manual rig, index-exact graft (02, mesh moved 0mm), QuatSkin via ariki-game converter (HEAD_SAFE=1, body-only normals fix). Ships as Ariki_Male_Mako.glb, lineage MALE-MAKO-FULLHEAD-V1. First mako ship. Height 1.818m provisional. Ceremony per REGISTRY.md; irreplaceable AccuRig FBX preserved in rig/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
# 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")
|
||||
Reference in New Issue
Block a user