MIRPAMO/bpy/common.py
jing a148b4a543 MIRPAMO v0: local Mixamo — autorig + retarget via headless Blender
- autorig: landmark detection from vertex cloud (markers JSON override),
  mixamorig 22-bone skeleton fit, bone-heat weights + nearest-bone rescue
- retarget: world-space quaternion delta transfer, hip-height scale
  compensation, fuzzy/explicit bone mapping (CMU map bundled)
- smoke: procedural test humanoid + synthetic BVH -> rig -> retarget -> verify
- deploy: push to gitea, pull + smoke on m3ultra and m1ultra

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 01:26:13 +10:00

116 lines
3.4 KiB
Python

"""Shared helpers for MIRPAMO's headless-Blender stages.
Every stage script is run as:
blender -b --python bpy/<stage>.py -- <stage args>
"""
import os
import sys
import bpy
def stage_args():
"""Return the argv after the '--' separator (the stage's own args)."""
argv = sys.argv
if "--" in argv:
return argv[argv.index("--") + 1:]
return []
def clean_scene():
bpy.ops.wm.read_factory_settings(use_empty=True)
def import_any(path):
"""Import a mesh/animation file; return the list of new objects."""
path = os.path.abspath(path)
if not os.path.exists(path):
raise FileNotFoundError(path)
ext = os.path.splitext(path)[1].lower()
before = set(bpy.data.objects)
if ext in (".glb", ".gltf"):
bpy.ops.import_scene.gltf(filepath=path)
elif ext == ".fbx":
bpy.ops.import_scene.fbx(filepath=path)
elif ext == ".obj":
bpy.ops.wm.obj_import(filepath=path)
elif ext == ".bvh":
bpy.ops.import_anim.bvh(
filepath=path,
rotate_mode="NATIVE",
update_scene_fps=True,
update_scene_duration=True,
)
else:
raise ValueError(f"unsupported file type: {ext}")
return [o for o in bpy.data.objects if o not in before]
def find_armature(objects):
for o in objects:
if o.type == "ARMATURE":
return o
return None
def find_meshes(objects):
"""Mesh objects, excluding bone custom-shape helpers the glTF importer
creates (e.g. 'Icosphere') — those are viewport display aids, not assets."""
shapes = set()
for o in bpy.data.objects:
if o.type == "ARMATURE":
for pb in o.pose.bones:
if pb.custom_shape:
shapes.add(pb.custom_shape)
return [o for o in objects if o.type == "MESH" and o not in shapes]
def count_fcurves(action):
"""Number of f-curves in an action, across legacy (<=4.x) and slotted
(5.x layered actions) APIs."""
try:
return len(action.fcurves)
except AttributeError:
return sum(len(cb.fcurves)
for layer in action.layers
for strip in layer.strips
for cb in strip.channelbags)
def select_only(objects, active=None):
bpy.ops.object.select_all(action="DESELECT")
for o in objects:
o.select_set(True)
if active is not None:
bpy.context.view_layer.objects.active = active
def export_glb(path, objects=None):
"""Export the given objects (or everything) as a GLB with animations."""
path = os.path.abspath(path)
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
if objects:
select_only(objects, active=objects[0])
use_selection = True
else:
use_selection = False
kwargs = dict(
filepath=path,
export_format="GLB",
export_animations=True,
export_yup=True,
use_selection=use_selection,
)
# Export every action as its own named animation when the option exists
# (property name is stable in 4.2+/5.x, but guard anyway).
props = bpy.ops.export_scene.gltf.get_rna_type().properties.keys()
if "export_animation_mode" in props:
kwargs["export_animation_mode"] = "ACTIONS"
bpy.ops.export_scene.gltf(**kwargs)
print(f"[mirpamo] wrote {path}")
def die(msg):
print(f"[mirpamo] ERROR: {msg}", file=sys.stderr)
sys.exit(1)