This commit is contained in:
Nicholas Butzke
2026-08-02 02:20:02 -04:00
parent 61669627db
commit 922983429e
226 changed files with 34032 additions and 18521 deletions
+166 -43
View File
@@ -57,6 +57,9 @@ import gltf_fix
import rig_map
import surface_map
from rig_map import RigRoles, build_map, is_cosmetic, is_segment_of, tokens
from retarget_pose import (
authored_world_rotation, build_segment_pairs, heading_inverse
)
argv = sys.argv
argv = argv[argv.index("--") + 1:] if "--" in argv else []
@@ -86,15 +89,64 @@ LIBRARY_CLIP_MAP = {
"Jump_Land": "Land",
"Crouch_Idle_Loop": "CrouchIdle",
"Crouch_Fwd_Loop": "CrouchWalk",
"Roll": "Dash",
"Roll": "Roll",
"Death01": "Death",
"Hit_Chest": "Hit",
"Dance_Loop": "Dance",
"Swim_Fwd_Loop": "Grapple",
"Pistol_Idle_Loop": "PistolIdle",
"Pistol_Shoot": "PistolShoot",
"Pistol_Reload": "PistolReload",
"Sword_Attack": "Throw",
"Sword_Attack": "SwordAttack",
}
LIBRARY_V2_CLIP_MAP = {
"Slide_Start": "SlideStart",
"Slide_Loop": "Slide",
"Slide_Exit": "SlideExit",
"NinjaJump_Start": "WallRunStart",
"NinjaJump_Land": "WallRunExit",
"Idle_Rail_Loop": "WallCling",
"ClimbUp_1m": "WallClimb",
"Shield_Dash": "Dash",
"Sword_Dash": "SwordDash",
"OverhandThrow": "Throw",
"Hit_Knockback": "Knockback",
"Chest_Open": "EmoteStretch",
"Idle_Rail_Call": "EmoteCall",
"Yes": "EmoteYes",
"Idle_No_Loop": "EmoteNo",
}
# The TPS demo set is a combat *walk*, not a run. It used to occupy the
# high-speed Run* slots, which made the runtime accelerate a roughly 1.8 m/s
# step cycle to six times normal playback at the game's 11 m/s ground speed.
# Keep the useful authored front/back steps, but label them honestly.
DIRECTIONAL_CLIP_MAP = {
"strafe_front-cycle": "StrafeWalkForward",
"strafe_back-cycle": "StrafeWalkBackward",
}
# Mesh2Motion's CC0 human add-on library supplies authored forward and lateral
# actions. Never rotate only the legs to manufacture another direction: the
# captured CMU clip below owns backward travel as one coherent full-body move.
MESH2MOTION_CLIP_MAP = {
"Strafe_left": [("StrafeWalkLeft", 0.0), ("RunLeft", 0.0)],
"Strafe_right": [("StrafeWalkRight", 0.0), ("RunRight", 0.0)],
"Run_Anime": [("RunForward", 0.0)],
# A subtle held superhero-flight motion: the leading hands stay extended
# while the feet move less than two degrees across the runtime audit.
"Flying Forward Super": [("Grapple", 0.0)],
}
# Uisco's authored UE4 Mannequin wall-run pair. These are real lateral
# wall-running performances, not a forward sprint relabelled as traversal.
WALLRUN_CLIP_MAP = {
"WallRunLeft": "WallRunLeft",
"WallRunRight": "WallRunRight",
}
CMU_CLIP_MAP = {
"RunBackward": "RunBackward",
}
UP = Vector((0.0, 0.0, 1.0))
@@ -604,13 +656,14 @@ def solve_pose(arm, order, rest_w, desired_rot, hips, hips_head):
def retarget_clip(src_arm, src_roles, tgt_arm, tgt_roles, mapping, action,
clip_name, yaw, scale):
clip_name, yaw, scale, lower_body_yaw_degrees=0.0):
src_rest = world_rest(src_arm)
tgt_rest = world_rest(tgt_arm)
order = bone_order(tgt_arm)
src_rest_rot = {n: m.to_quaternion() for n, m in src_rest.items()}
tgt_rest_rot = {n: m.to_quaternion() for n, m in tgt_rest.items()}
yaw_inv = yaw.inverted()
segment_pairs = build_segment_pairs(mapping, src_roles, tgt_roles)
src_hips = src_roles.hips
tgt_hips = tgt_roles.hips
@@ -619,6 +672,20 @@ def retarget_clip(src_arm, src_roles, tgt_arm, tgt_roles, mapping, action,
assign_action(src_arm, action)
f0, f1 = (int(round(v)) for v in action.frame_range)
source_heading = Quaternion()
if clip_name == "RunBackward":
bpy.context.scene.frame_set(f0)
source_eval = src_arm.evaluated_get(
bpy.context.evaluated_depsgraph_get()
)
source_heading = heading_inverse(
(
source_eval.matrix_world
@ source_eval.pose.bones[src_hips].matrix
).to_quaternion(),
src_rest_rot[src_hips],
UP,
)
baked = bpy.data.actions.new(clip_name)
assign_action(tgt_arm, baked)
@@ -636,14 +703,27 @@ def retarget_clip(src_arm, src_roles, tgt_arm, tgt_roles, mapping, action,
for tgt_name, src_name in mapping.items():
if src_name not in src_eval.pose.bones or tgt_name not in tgt_rest_rot:
continue
pose_rot = (smw @ src_eval.pose.bones[src_name].matrix).to_quaternion()
delta = pose_rot @ src_rest_rot[src_name].inverted()
desired[tgt_name] = (yaw @ delta @ yaw_inv) @ tgt_rest_rot[tgt_name]
target_rotation = authored_world_rotation(
tgt_name,
src_name,
src_eval,
smw,
src_rest_rot,
tgt_rest,
tgt_rest_rot,
yaw,
yaw_inv,
segment_pairs,
source_heading,
)
desired[tgt_name] = target_rotation
hips_head = None
if src_hips in src_eval.pose.bones:
moved = (smw @ src_eval.pose.bones[src_hips].matrix).translation
d = yaw @ ((moved - src_hips_rest) * scale)
d = yaw @ (source_heading @ (
(moved - src_hips_rest) * scale
))
if STRIP_ROOT_MOTION:
d.x = 0.0
d.y = 0.0 # gameplay code moves the body; keep the vertical bob
@@ -1162,31 +1242,7 @@ def main():
fix_unlit_materials(meshes)
roles = RigRoles(arm) # rest positions moved; re-read
library = os.path.join(ANIM_DIR, "_library.glb")
if not os.path.exists(library):
print(f"ERROR: animation library not found: {library}")
sys.exit(1)
before = set(bpy.data.objects)
before_actions = set(bpy.data.actions)
bpy.ops.import_scene.gltf(filepath=library)
new_objects = [o for o in bpy.data.objects if o not in before]
src_arm = next((o for o in new_objects if o.type == "ARMATURE"), None)
if not src_arm:
print("ERROR: no armature in the animation library")
sys.exit(1)
src_roles = RigRoles(src_arm)
mapping = build_map(src_roles, roles, OVERRIDES)
print("\nLibrary rig:\n" + src_roles.describe())
print("\nCharacter rig:\n" + roles.describe())
print(f"\nDriving {len(mapping)} of {len(arm.data.bones)} bones from the library; "
f"{len(arm.data.bones) - len(mapping)} left free for secondary motion.")
yaw = facing_correction(src_arm, src_roles, arm, roles)
src_h = (src_arm.matrix_world @ src_arm.data.bones[src_roles.hips].matrix_local).translation.z
tgt_h = (arm.matrix_world @ arm.data.bones[roles.hips].matrix_local).translation.z
scale = tgt_h / src_h if src_h > 1e-5 else 1.0
print(f"Hips height: library {src_h:.3f} m, character {tgt_h:.3f} m (scale {scale:.3f})")
if not arm.animation_data:
arm.animation_data_create()
@@ -1195,16 +1251,83 @@ def main():
merged = 0
kept_clips = set()
for action in [a for a in bpy.data.actions if a not in before_actions]:
clip = LIBRARY_CLIP_MAP.get(action.name.split(".")[0])
if not clip:
all_library_objects = []
primary_mapping = {}
library_specs = (
("_library.glb", LIBRARY_CLIP_MAP),
("_library_v2.glb", LIBRARY_V2_CLIP_MAP),
("_directional.glb", DIRECTIONAL_CLIP_MAP),
("_mesh2motion.glb", MESH2MOTION_CLIP_MAP),
("_cmu_locomotion.glb", CMU_CLIP_MAP),
("_wallrun.glb", WALLRUN_CLIP_MAP),
)
found_library = False
for filename, clip_map in library_specs:
library = os.path.join(ANIM_DIR, filename)
if not os.path.exists(library):
continue
print(f" {action.name} -> {clip}")
baked = retarget_clip(src_arm, src_roles, arm, roles, mapping, action,
clip, yaw, scale)
add_nla_clip(arm, baked, clip)
kept_clips.add(clip)
merged += 1
found_library = True
before = set(bpy.data.objects)
before_actions = set(bpy.data.actions)
bpy.ops.import_scene.gltf(filepath=library)
new_objects = [o for o in bpy.data.objects if o not in before]
all_library_objects.extend(new_objects)
src_arm = next((o for o in new_objects if o.type == "ARMATURE"), None)
if not src_arm:
print(f"ERROR: no armature in animation library {filename}")
sys.exit(1)
src_roles = RigRoles(src_arm)
mapping = build_map(src_roles, roles, OVERRIDES)
if not primary_mapping:
primary_mapping = mapping
print(f"\n{filename} rig:\n" + src_roles.describe())
print(f"Driving {len(mapping)} of {len(arm.data.bones)} bones; "
f"{len(arm.data.bones) - len(mapping)} left free.")
yaw = facing_correction(src_arm, src_roles, arm, roles)
src_h = (
src_arm.matrix_world
@ src_arm.data.bones[src_roles.hips].matrix_local
).translation.z
tgt_h = (
arm.matrix_world @ arm.data.bones[roles.hips].matrix_local
).translation.z
scale = tgt_h / src_h if src_h > 1e-5 else 1.0
print(
f"Hips height: library {src_h:.3f} m, "
f"character {tgt_h:.3f} m (scale {scale:.3f})"
)
for action in [a for a in bpy.data.actions if a not in before_actions]:
mapped = clip_map.get(action.name.split(".")[0])
if not mapped:
continue
specs = [(mapped, 0.0)] if isinstance(mapped, str) else mapped
for spec in specs:
clip, travel_yaw_degrees = spec[:2]
suffix = (
f" (lower-body heading {travel_yaw_degrees:+.0f} deg)"
if abs(travel_yaw_degrees) > 1e-4 else ""
)
print(f" {action.name} -> {clip}{suffix}")
baked = retarget_clip(
src_arm,
src_roles,
arm,
roles,
mapping,
action,
clip,
yaw,
scale,
travel_yaw_degrees,
)
add_nla_clip(arm, baked, clip)
kept_clips.add(clip)
merged += 1
if not found_library:
print(f"ERROR: no animation libraries found in {ANIM_DIR}")
sys.exit(1)
if merged == 0:
print("ERROR: no clips retargeted")
sys.exit(1)
@@ -1241,12 +1364,12 @@ def main():
bpy.data.actions.remove(a)
print(f"Kept {len(kept_clips)} clips; dropped everything else")
for o in new_objects:
for o in all_library_objects:
bpy.data.objects.remove(o, do_unlink=True)
os.makedirs(os.path.dirname(os.path.abspath(OUTPUT)), exist_ok=True)
sidecar = os.path.splitext(OUTPUT)[0] + ".rig.json"
info = describe_rig(arm, roles, mapping, meshes)
info = describe_rig(arm, roles, primary_mapping, meshes)
with open(sidecar, "w", encoding="utf-8") as f:
json.dump(info, f, indent=2)
print(f"Wrote {os.path.basename(sidecar)}: {len(info['chains'])} cloth chains, "