- 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>
116 lines
3.4 KiB
Python
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)
|