96 lines
3.1 KiB
Python
96 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Build the compact, animation-only wall-run library used by retarget.py.
|
|
|
|
The source clips are Uisco's authored UE4 Mannequin wall-run pair:
|
|
https://uisco.itch.io/wall-running-animations
|
|
|
|
Usage:
|
|
blender --background --python tools/build_wallrun_library.py -- \
|
|
<wall_run_left.FBX> <wall_run_right.FBX> <output.glb>
|
|
|
|
The two FBX files use the same UE4 mannequin skeleton. This script keeps one
|
|
copy of that skeleton, names the actions explicitly, and exports no mesh.
|
|
"""
|
|
import os
|
|
import sys
|
|
|
|
import bpy
|
|
|
|
|
|
argv = sys.argv
|
|
argv = argv[argv.index("--") + 1:] if "--" in argv else []
|
|
if len(argv) != 3:
|
|
print(__doc__)
|
|
sys.exit(1)
|
|
|
|
LEFT, RIGHT, OUTPUT = argv
|
|
|
|
|
|
def import_clip(path, name):
|
|
before_objects = set(bpy.data.objects)
|
|
before_actions = set(bpy.data.actions)
|
|
bpy.ops.import_scene.fbx(filepath=path)
|
|
objects = [obj for obj in bpy.data.objects if obj not in before_objects]
|
|
arms = [obj for obj in objects if obj.type == "ARMATURE"]
|
|
actions = [action for action in bpy.data.actions if action not in before_actions]
|
|
if len(arms) != 1 or len(actions) != 1:
|
|
raise RuntimeError(
|
|
f"{path}: expected one armature/action, got "
|
|
f"{len(arms)} armatures and {len(actions)} actions"
|
|
)
|
|
actions[0].name = name
|
|
actions[0].use_fake_user = True
|
|
return arms[0], actions[0], objects
|
|
|
|
|
|
def main():
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
left_arm, left_action, left_objects = import_clip(LEFT, "WallRunLeft")
|
|
right_arm, right_action, right_objects = import_clip(RIGHT, "WallRunRight")
|
|
|
|
left_bones = [bone.name for bone in left_arm.data.bones]
|
|
right_bones = [bone.name for bone in right_arm.data.bones]
|
|
if left_bones != right_bones:
|
|
raise RuntimeError("left/right source skeletons do not match")
|
|
|
|
for obj in left_objects + right_objects:
|
|
if obj not in (left_arm, right_arm):
|
|
bpy.data.objects.remove(obj, do_unlink=True)
|
|
bpy.data.objects.remove(right_arm, do_unlink=True)
|
|
|
|
if left_arm.animation_data is None:
|
|
left_arm.animation_data_create()
|
|
left_arm.animation_data.action = None
|
|
for track in list(left_arm.animation_data.nla_tracks):
|
|
left_arm.animation_data.nla_tracks.remove(track)
|
|
for action in (left_action, right_action):
|
|
track = left_arm.animation_data.nla_tracks.new()
|
|
track.name = action.name
|
|
strip = track.strips.new(action.name, 0, action)
|
|
strip.name = action.name
|
|
track.mute = True
|
|
|
|
for action in list(bpy.data.actions):
|
|
if action not in (left_action, right_action):
|
|
bpy.data.actions.remove(action)
|
|
|
|
os.makedirs(os.path.dirname(os.path.abspath(OUTPUT)), exist_ok=True)
|
|
bpy.ops.export_scene.gltf(
|
|
filepath=OUTPUT,
|
|
export_format="GLB",
|
|
export_yup=True,
|
|
export_animations=True,
|
|
export_animation_mode="NLA_TRACKS",
|
|
export_skins=True,
|
|
export_bake_animation=False,
|
|
export_optimize_animation_size=True,
|
|
)
|
|
print(
|
|
f"Exported WallRunLeft/WallRunRight on "
|
|
f"{len(left_arm.data.bones)} bones to {OUTPUT}"
|
|
)
|
|
|
|
|
|
main()
|