fix(anim): the clip now drives the body — two causes, both measured

The body stood in an A-pose while the log said the clip was playing. Two
independent defects, found by querying the imported asset instead of guessing:

1. PlayAnimation needs the component in AnimationSingleNode with a live
   single-node instance. The body is hidden at construction and only unhidden by
   the camera toggle, so the first call could land before the component was ready,
   and the Wanted == PlayingAnimation guard then returned forever. UpdateBodyAnimation
   now sets the mode, calls SetAnimation + Play, and re-asserts while the instance
   is missing.
2. The arms-down offsets lived only in the clip keys. The exporter writes clip data
   relative to the rest pose, so they arrived as a ±20 degree swing around an
   A-posed bind - which is exactly what the pose probe measured. apply_base_pose now
   bakes them into the rest pose before any clip is authored, the same trick
   mpfb_character_export.py --pose idle uses. The table is applied with inverted
   signs: as written, the FBX axis mapping produced arms raised in a V.

Shot 6 confirms it: the arms hang at the sides. Still open and recorded - the idle
pose reads awkwardly (arms forward, hands splayed) and the walk clip has no shot in
motion; both are marked in TODO.md and Screenshots/README.md.

Instrumentation removed; the diagnostics that remain are behind
gigline.SetupLogs.
This commit is contained in:
zwitschi committed 2026-11-09 10:56:43 +01:00
1 parent d0f94dff4e
commit a1f5989cbb
39 files changed
+115 -59

No files matched your search

+31 -3
View File
@@ -91,10 +91,37 @@ def pose_bones(arm):
return arm.pose.bones
def apply_base_pose(arm):
"""Bake the arms-down pose into the REST pose before authoring any clip.
The first two attempts added the arms-down as an offset inside every key. That
is not what the export sees: the exporter writes clip data relative to the rest
pose, so an offset that lives only in the keys arrives in Unreal as a small
delta around an A-posed bind - the body stood in an A with a ±20 degree arm
swing, which is exactly what the Unreal pose probe measured. Baking the pose
into the rest pose first is the same trick `mpfb_character_export.py --pose
idle` uses, and it makes bind and clips agree.
"""
bpy.context.view_layer.objects.active = arm
bpy.ops.object.mode_set(mode="POSE")
# The sign is inverted relative to the original `--pose idle` table: the first
# attempt applied BASE as written and the body came out with its arms raised in
# a V, i.e. the FBX axis mapping flips this rotation. Verified on the imported
# asset rather than guessed - see MEMORY.md, 2026-11-09.
for name, eul in BASE.items():
bone = arm.pose.bones.get(name)
if bone is None:
continue
bone.rotation_mode = "XYZ"
bone.rotation_euler = (-eul[0], -eul[1], -eul[2])
bpy.ops.pose.armature_apply()
bpy.ops.object.mode_set(mode="OBJECT")
log("base pose applied to rest: arms down before any clip is authored")
def key_euler(pb, frame, x=0.0, y=0.0, z=0.0):
"""Key a pose-bone rotation on top of the arms-down base."""
bx, by, bz = BASE.get(pb.name, (0.0, 0.0, 0.0))
pb.rotation_euler = (bx + x, by + y, bz + z)
"""Key a pose-bone rotation; the arms-down base is already in the rest pose."""
pb.rotation_euler = (x, y, z)
pb.keyframe_insert(data_path="rotation_euler", frame=frame)
@@ -174,6 +201,7 @@ def main(argv=None) -> int:
bpy.context.scene.render.fps = FPS
clear_animation(arm)
apply_base_pose(arm)
idle = clip_idle(arm, CLIPS["Idle"])
walk = clip_walk(arm, CLIPS["Walk"])