feat(review): deterministic walking capture, and the defect it exposed

The shot actor can now photograph a moving pawn: a per-shot ShotFollow flag re-places
the camera every frame at a fixed offset in the pawn's frame, and the walk is driven
per shot (SetReviewWalk) with the pawn returned to spawn after a follow shot, so the
fixed-camera shots later in the list still find it in place. With that, the capture
stopped depending on where a 4.5 m/s pawn happened to be.

Two further bugs fixed on the way:
- the engine's screenshot folder is cleared before each run. The collect step copies
  every review-*.png it finds in Saved/, so a shot retired from the camera list kept
  reappearing with byte-identical content from an older run - which is how a walk shot
  that was never captured looked like a captured one.
- UpdateBodyAnimation no longer calls SetAnimation every frame. Re-setting the same
  clip restarts it, which freezes the body on frame 0.

What the deterministic capture then exposed: the walk clip is applied but does not
advance - every walking frame shows the same pose (arms forward, legs together). The
stride shot is retired again rather than shipped, and TODO.md names the next step:
log GetCurrentAssetTime across frames and check the imported PlayRate. The harness
stays wired for when the clip plays.

Shots 6 and 7 were re-rendered after these changes and still pass; 7/7 verify.
This commit is contained in:
zwitschi committed 2026-11-09 12:11:24 +01:00
1 parent f84093adba
commit eeeabe56b1
42 files changed
+160 -68

No files matched your search

@@ -258,6 +258,16 @@ void AGigLineCharacter::BeginPlay()
bWalkForever = FParse::Param(FCommandLine::Get(), TEXT("walk"));
}
void AGigLineCharacter::SetReviewWalk(bool bEnable)
{
bWalkForever = bEnable;
}
void AGigLineCharacter::ReturnToStartForReview()
{
ResetToStart();
}
void AGigLineCharacter::WalkForever()
{
if (bWalkForever && GetCharacterMovement() && GetCharacterMovement()->IsMovingOnGround())
@@ -773,8 +783,6 @@ void AGigLineCharacter::UpdateBodyAnimation()
UAnimSequence* Wanted = bMoving ? WalkAnimation.Get() : IdleAnimation.Get();
if (!Wanted)
{
// Logged once: a missing clip presents as an A-posed body, which looks like
// an animation problem rather than a missing asset reference.
static bool bWarnedMissingClip = false;
if (!bWarnedMissingClip)
{
@@ -787,32 +795,32 @@ void AGigLineCharacter::UpdateBodyAnimation()
return;
}
// 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)
{
GetMesh()->SetAnimationMode(EAnimationMode::AnimationSingleNode);
}
if (Wanted != PlayingAnimation || GetMesh()->GetSingleNodeInstance() == nullptr)
{
PlayingAnimation = Wanted;
GetMesh()->SetAnimation(Wanted);
GetMesh()->Play(true);
if (CVarGigLineSetupLogs.GetValueOnGameThread())
// Only the mode setter is idempotent; calling SetAnimation unconditionally
// restarts the clip every frame, which freezes the body on frame 0 - that is
// why the walking capture showed a still figure with both legs together.
if (GetMesh()->GetAnimationMode() != EAnimationMode::AnimationSingleNode)
{
static bool bLoggedAnimState = false;
if (!bLoggedAnimState)
GetMesh()->SetAnimationMode(EAnimationMode::AnimationSingleNode);
}
if (Wanted != PlayingAnimation)
{
GetMesh()->SetAnimation(Wanted);
PlayingAnimation = Wanted;
GetMesh()->Play(true);
if (CVarGigLineSetupLogs.GetValueOnGameThread())
{
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"));
UE_LOG(LogTemp, Log, TEXT("GigLine: body clip %s, mode=%d"),
*Wanted->GetName(), static_cast<int32>(GetMesh()->GetAnimationMode()));
}
}
else
{
// Same clip but the instance was recreated after a mode change: resume it
// once rather than re-setting the animation.
GetMesh()->Play(true);
}
}
}