big
This commit is contained in:
+166
-43
@@ -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, "
|
||||
|
||||
Reference in New Issue
Block a user