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

@@ -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()