diff --git a/pipeline/render_moves.py b/pipeline/render_moves.py index 2d6d4c31..746d1d9d 100644 --- a/pipeline/render_moves.py +++ b/pipeline/render_moves.py @@ -26,7 +26,7 @@ def parse_args(): argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] ap = argparse.ArgumentParser() ap.add_argument("--char", required=True) - ap.add_argument("--clips", required=True, help="dir or comma-separated fbx list") + ap.add_argument("--clips", default="", help="dir or comma-separated fbx list") ap.add_argument("--out", required=True) ap.add_argument("--size", type=int, default=512) ap.add_argument("--rotz", type=float, default=90.0, @@ -42,6 +42,9 @@ def parse_args(): help="comma list of mesh-name substrings to tear (punch alpha " "holes in their textures); output dirs get an @torn suffix") ap.add_argument("--torn-seed", type=int, default=7) + ap.add_argument("--self-anim", action="store_true", + help="ignore --clips; render each action already baked into " + "the character file (MIRPAMO retarget output)") ap.add_argument("--strip", default="", help="comma list of mesh-name substrings to REMOVE (clothes " "knocked off); output dirs get an @stripped suffix") @@ -301,17 +304,25 @@ def main(): attach_gear(arm, args.attach) variant_suffix = "@geared" - clips_arg = str(Path(args.clips).expanduser()) - p = Path(clips_arg) - if p.is_dir(): - clip_paths = sorted(p.glob("*.fbx")) + # (clip_path, action): external clip files, or the character's own + # baked actions when --self-anim (MIRPAMO retarget output) + jobs = [] + if args.self_anim: + for act in bpy.data.actions: + jobs.append((Path(act.name + ".glb"), act)) else: - clip_paths = [Path(c).expanduser() for c in args.clips.split(",")] + clips_arg = str(Path(args.clips).expanduser()) + p = Path(clips_arg) + if p.is_dir(): + clip_paths = sorted(p.glob("*.fbx")) + else: + clip_paths = [Path(c).expanduser() for c in args.clips.split(",")] + jobs = [(c, None) for c in clip_paths] out_root = Path(args.out).expanduser() sc = bpy.context.scene - for clip in clip_paths: - action = import_clip_action(str(clip), arm) + for clip, baked in jobs: + action = baked if baked is not None else import_clip_action(str(clip), arm) assign_action(arm, action) f0, f1 = (int(action.frame_range[0]), int(action.frame_range[1])) if args.max_frames > 0: diff --git a/pipeline/strip_skin.py b/pipeline/strip_skin.py new file mode 100644 index 00000000..e3205784 --- /dev/null +++ b/pipeline/strip_skin.py @@ -0,0 +1,45 @@ +# 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() + dest = out / clip.name + 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 + import re + 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) + bpy.ops.export_scene.fbx(filepath=str(dest), use_selection=False, + add_leaf_bones=False, bake_anim=True) + print("STRIPPED", clip.name) +print("DONE")