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:
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,14 @@ protected:
|
||||
void ResetToStart();
|
||||
void TogglePause();
|
||||
|
||||
public:
|
||||
/** Review harness: hold or release forward input from the screenshot actor. */
|
||||
void SetReviewWalk(bool bEnable);
|
||||
|
||||
/** Review harness: put the pawn back where it spawned, after a walking capture. */
|
||||
void ReturnToStartForReview();
|
||||
|
||||
protected:
|
||||
/** Swaps between the first-person and third-person rigs (V). */
|
||||
void ToggleCamera();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -50,6 +50,18 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
float SettleSeconds = 0.35f;
|
||||
|
||||
/**
|
||||
* Optional per-shot "follow the player" flag, same index as ShotLocations.
|
||||
*
|
||||
* A fixed camera cannot photograph a walking pawn: at 4.5 m/s it covers the
|
||||
* distance between two captures, so three attempts framed it at a different
|
||||
* distance each run and two missed it entirely. With this flag the camera is
|
||||
* re-placed every frame at a fixed offset in the pawn's frame, which is what
|
||||
* makes a stride shot deterministic.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
TArray<bool> ShotFollow;
|
||||
|
||||
/** Quit the game after the last shot (disable to keep the game open). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
bool bQuitWhenDone = true;
|
||||
@@ -57,6 +69,9 @@ public:
|
||||
private:
|
||||
void NextShot();
|
||||
|
||||
/** Re-places the active camera at the follow offset behind the player pawn. */
|
||||
void UpdateFollowCamera();
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<class APlayerController> PlayerController;
|
||||
|
||||
|
||||
Reference in new issue
Block a user