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

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -727,6 +727,7 @@ void AGigLineCharacter::Tick(float DeltaSeconds)
// Speed-driven animation state: correct whether the player walked, was pushed,
// or fell, and independent of which input produced the movement.
UpdateBodyAnimation();
}
void AGigLineCharacter::ToggleCamera()
@@ -771,16 +772,34 @@ void AGigLineCharacter::UpdateBodyAnimation()
}
return;
}
if (Wanted == PlayingAnimation)
// PlayAnimation is only an intent: it needs the component in single-node mode and
// a live single-node instance. The body is hidden at construction and only
// unhidden by the camera toggle, so the first call could land on a component that
// was not ready and never retried - the clip name logged, the pose left at bind.
// Driving the mode and the instance explicitly, and re-asserting every frame while
// the instance is missing, makes that failure self-correcting.
if (GetMesh()->GetAnimationMode() != EAnimationMode::AnimationSingleNode)
{
return;
GetMesh()->SetAnimationMode(EAnimationMode::AnimationSingleNode);
}
PlayingAnimation = Wanted;
if (CVarGigLineSetupLogs.GetValueOnGameThread())
if (Wanted != PlayingAnimation || GetMesh()->GetSingleNodeInstance() == nullptr)
{
UE_LOG(LogTemp, Log, TEXT("GigLine: playing body clip %s"), *Wanted->GetName());
PlayingAnimation = Wanted;
GetMesh()->SetAnimation(Wanted);
GetMesh()->Play(true);
if (CVarGigLineSetupLogs.GetValueOnGameThread())
{
static bool bLoggedAnimState = false;
if (!bLoggedAnimState)
{
bLoggedAnimState = true;
UE_LOG(LogTemp, Log, TEXT("GigLine: body clip %s, mode=%d, instance=%s"),
*Wanted->GetName(), static_cast<int32>(GetMesh()->GetAnimationMode()),
GetMesh()->GetSingleNodeInstance() ? TEXT("ok") : TEXT("nil"));
}
}
}
GetMesh()->PlayAnimation(Wanted, true);
}
void AGigLineCharacter::ResetToStart()