foitin/pipeline/strip_skin.py
m3ultra b033879963 strip_skin: export GLB not FBX — Blender 5 FBX exporter flattens slotted actions
This was the entire 'MIRPAMO retarget is mushy' mystery: the retarget math
was fine, the converted clips were flat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 14:39:58 +10:00

68 lines
2.5 KiB
Python

# Blender headless: convert with-skin Mixamo FBX clips to anim-only FBX
# (deletes meshes, keeps armature+action). MIRPAMO's retarget mapper needs
# anim-only sources.
#
# Usage: Blender -b --python pipeline/strip_skin.py -- --clips "a.fbx,b.fbx" --out dir
import argparse
import sys
from pathlib import Path
import bpy
def parse_args():
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
ap = argparse.ArgumentParser()
ap.add_argument("--clips", required=True)
ap.add_argument("--out", required=True)
return ap.parse_args(argv)
args = parse_args()
out = Path(args.out).expanduser()
out.mkdir(parents=True, exist_ok=True)
for clip in args.clips.split(","):
clip = Path(clip).expanduser()
# GLB out: Blender 5's FBX exporter flattens slotted actions to identity;
# the glTF exporter is slot-native and MIRPAMO imports glb clips fine.
dest = (out / clip.name).with_suffix(".glb")
if dest.exists():
print("SKIP", clip.name)
continue
bpy.ops.wm.read_factory_settings(use_empty=True)
bpy.ops.import_scene.fbx(filepath=str(clip), use_anim=True)
for o in list(bpy.data.objects):
if o.type == "MESH":
bpy.data.objects.remove(o, do_unlink=True)
# normalise numbered Mixamo namespaces (mixamorig7: -> mixamorig:) so
# downstream mappers (MIRPAMO retarget) match by exact name.
# NOTE: bone rename does NOT fix up slotted-action fcurve paths in
# Blender 5 — rewrite them explicitly or the action drives nothing.
import re
def all_fcurves(act):
if hasattr(act, "fcurves") and len(getattr(act, "fcurves", [])):
return list(act.fcurves)
out = []
for layer in act.layers:
for strip in layer.strips:
for cb in strip.channelbags:
out.extend(cb.fcurves)
return out
for o in bpy.data.objects:
if o.type == "ARMATURE":
for b in o.data.bones:
b.name = re.sub(r"^mixamorig\d+:", "mixamorig:", b.name)
for act in bpy.data.actions:
for fc in all_fcurves(act):
if '"' in fc.data_path:
bone = fc.data_path.split('"')[1]
fixed = re.sub(r"^mixamorig\d+:", "mixamorig:", bone)
if fixed != bone:
fc.data_path = fc.data_path.replace('"%s"' % bone, '"%s"' % fixed)
bpy.ops.export_scene.gltf(filepath=str(dest), export_format="GLB",
export_animations=True)
print("STRIPPED", clip.name)
print("DONE")