Spaces:
Running on Zero
Running on Zero
File size: 5,641 Bytes
8f1bcd9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | """
cli.py
Command-line interface for rig_retarget.
Usage:
python -m rig_retarget.cli \\
--source walk.bvh \\
--dest unirig_character.glb \\
--mapping radical2unirig.json \\
--output animated_character.glb \\
[--fps 30] [--start 0] [--frames 100] [--step 1]
# Calculate corrections only (no transfer):
python -m rig_retarget.cli --calc-corrections \\
--source walk.bvh --dest unirig_character.glb \\
--mapping mymap.json
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
def _parse_args(argv=None):
p = argparse.ArgumentParser(
prog="rig_retarget",
description="Retarget animation from BVH/glTF source onto UniRig/glTF destination.",
)
p.add_argument("--source", required=True, help="Source animation file (.bvh or .glb/.gltf)")
p.add_argument("--dest", required=True, help="Destination skeleton file (.glb/.gltf, UniRig output)")
p.add_argument("--mapping", required=True, help="KeeMap-compatible JSON bone mapping file")
p.add_argument("--output", default=None, help="Output animated .glb (default: dest_retargeted.glb)")
p.add_argument("--fps", type=float, default=30.0)
p.add_argument("--start", type=int, default=0, help="Start frame index (0-based)")
p.add_argument("--frames", type=int, default=None, help="Number of frames to transfer (default: all)")
p.add_argument("--step", type=int, default=1, help="Keyframe every N source frames")
p.add_argument("--skin", type=int, default=0, help="Skin index in destination glTF")
p.add_argument("--calc-corrections", action="store_true",
help="Auto-calculate bone corrections and update the mapping JSON, then exit.")
p.add_argument("--verbose", action="store_true")
return p.parse_args(argv)
def main(argv=None) -> None:
args = _parse_args(argv)
from .io.mapping import load_mapping, save_mapping, KeeMapSettings
from .io.gltf_io import load_gltf, write_gltf_animation
from .retarget import (
calc_all_corrections, transfer_animation,
)
# -----------------------------------------------------------------------
# Load mapping
# -----------------------------------------------------------------------
print(f"[*] Loading mapping : {args.mapping}")
settings, bone_items = load_mapping(args.mapping)
# Override settings from CLI args
settings.start_frame_to_apply = args.start
settings.keyframe_every_n_frames = args.step
# -----------------------------------------------------------------------
# Load source animation
# -----------------------------------------------------------------------
src_path = Path(args.source)
print(f"[*] Loading source : {src_path}")
if src_path.suffix.lower() == ".bvh":
from .io.bvh import load_bvh
src_anim = load_bvh(str(src_path))
if args.verbose:
print(f" BVH: {src_anim.num_frames} frames, "
f"{src_anim.frame_time*1000:.1f} ms/frame, "
f"{len(src_anim.armature.pose_bones)} joints")
elif src_path.suffix.lower() in (".glb", ".gltf"):
# glTF source — load skeleton only; animation reading is TODO
raise NotImplementedError(
"glTF source animation reading is not yet implemented. "
"Use a BVH file for the source animation."
)
else:
print(f"[!] Unsupported source format: {src_path.suffix}", file=sys.stderr)
sys.exit(1)
if args.frames is not None:
settings.number_of_frames_to_apply = args.frames
else:
settings.number_of_frames_to_apply = src_anim.num_frames - args.start
# -----------------------------------------------------------------------
# Load destination skeleton
# -----------------------------------------------------------------------
dst_path = Path(args.dest)
print(f"[*] Loading dest : {dst_path}")
dst_arm = load_gltf(str(dst_path), skin_index=args.skin)
if args.verbose:
print(f" Skeleton: {len(dst_arm.pose_bones)} bones")
# -----------------------------------------------------------------------
# Auto-correct pass (optional)
# -----------------------------------------------------------------------
if args.calc_corrections:
print("[*] Calculating bone corrections ...")
src_anim.apply_frame(args.start)
calc_all_corrections(bone_items, src_anim.armature, dst_arm, settings)
save_mapping(args.mapping, settings, bone_items)
print(f"[*] Updated mapping saved → {args.mapping}")
return
# -----------------------------------------------------------------------
# Transfer
# -----------------------------------------------------------------------
print(f"[*] Transferring {settings.number_of_frames_to_apply} frames "
f"(start={settings.start_frame_to_apply}, step={settings.keyframe_every_n_frames}) ...")
keyframes = transfer_animation(src_anim, dst_arm, bone_items, settings)
print(f"[*] Generated {len(keyframes)} keyframes")
# -----------------------------------------------------------------------
# Write output
# -----------------------------------------------------------------------
out_path = args.output or str(dst_path.with_name(dst_path.stem + "_retargeted.glb"))
print(f"[*] Writing output : {out_path}")
write_gltf_animation(str(dst_path), dst_arm, keyframes, out_path, fps=args.fps, skin_index=args.skin)
print("[✓] Done")
if __name__ == "__main__":
main()
|