"""Shared helpers for MIRPAMO's headless-Blender stages. Every stage script is run as: blender -b --python bpy/.py -- """ 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)