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

@@ -1,5 +1,8 @@
#include "GigLineShotActor.h"
#include "GigLineCharacter.h"
#include "Kismet/GameplayStatics.h"
#include "Camera/CameraActor.h"
#include "Camera/CameraComponent.h"
#include "Engine/World.h"
@@ -32,6 +35,12 @@ void AGigLineShotActor::Tick(float DeltaSeconds)
if (bWaitingForSettle)
{
// A follow shot tracks the pawn while it settles, otherwise the subject
// walks out of the frame between placement and capture.
if (CurrentShot < ShotFollow.Num() && ShotFollow[CurrentShot])
{
UpdateFollowCamera();
}
SettleCountdown -= DeltaSeconds;
if (SettleCountdown <= 0.f)
{
@@ -53,11 +62,40 @@ void AGigLineShotActor::Tick(float DeltaSeconds)
{
bWaitingForScreenshot = false;
UE_LOG(LogTemp, Log, TEXT("GigLineShotActor: captured %s"), *FScreenshotRequest::GetFilename());
// Walking shots move the pawn; put it back before the fixed-camera shots run.
if (ShotFollow.IsValidIndex(CurrentShot) && ShotFollow[CurrentShot])
{
if (AGigLineCharacter* Character = Cast<AGigLineCharacter>(UGameplayStatics::GetPlayerPawn(this, 0)))
{
Character->ReturnToStartForReview();
}
}
++CurrentShot;
NextShot();
}
}
void AGigLineShotActor::UpdateFollowCamera()
{
if (!CurrentCamera)
{
return;
}
const APawn* Pawn = UGameplayStatics::GetPlayerPawn(this, 0);
if (!Pawn)
{
return;
}
// Offset in the pawn's frame: to its right, slightly above, looking at the
// pelvis. Right-of-pawn gives a profile view, which is where a stride reads.
const FVector Right = Pawn->GetActorRightVector();
const FVector Origin = Pawn->GetActorLocation();
const FVector CameraLocation = Origin + Right * 420.f + FVector(0.f, 0.f, 90.f);
const FVector Target = Origin + FVector(0.f, 0.f, 40.f);
CurrentCamera->SetActorLocation(CameraLocation);
CurrentCamera->SetActorRotation((Target - CameraLocation).Rotation());
}
void AGigLineShotActor::NextShot()
{
if (!PlayerController)
@@ -93,6 +131,20 @@ void AGigLineShotActor::NextShot()
Cam->SetFieldOfView(ShotFieldOfView[CurrentShot]);
}
}
// Follow shots ignore their recorded transform after this point: the camera is
// rebuilt from the pawn every frame until the capture. They also own the walking:
// the pawn only holds forward input while a follow shot is being taken, so the
// fixed-camera shots later in the list still find it at the spawn point.
const bool bFollowThisShot = ShotFollow.IsValidIndex(CurrentShot) && ShotFollow[CurrentShot];
if (AGigLineCharacter* Character = Cast<AGigLineCharacter>(UGameplayStatics::GetPlayerPawn(this, 0)))
{
Character->SetReviewWalk(bFollowThisShot);
}
if (bFollowThisShot)
{
UpdateFollowCamera();
}
PlayerController->SetViewTarget(CurrentCamera);
SettleCountdown = SettleSeconds;