test(camera): -cameratest harness for the spring-arm collision, result inconclusive

0.2.3 is about camera clipping, and nothing had ever exercised the spring arm's
collision test. This adds a harness: find the nearest wall from the spawn, stand the pawn
70 cm off it facing away, wait for the boom, then report the arm length, whether the boom
ends inside geometry, and a back-trace from camera to head.

Inconclusive, and the setup is the suspect rather than the engine: every run reports the
arm at its resting 340 cm, but BOTH traces come back clear, so nothing was between the
pawn and the boom at all. The placement is wrong, not necessarily the collision.

Three wrong turns recorded in TODO.md because each cost a run: the probe accepted the
floor as a wall, then faced the pawn into the wall by negating the impact normal (which
points away from it), then set actor rotation only - which a possessed pawn discards in
favour of control rotation.

Next: look at the placement screenshot before trusting the measurement.
This commit is contained in:
zwitschi committed 2026-11-09 14:33:57 +01:00
1 parent d8b44f1e57
commit a6701aa3d1
3 files changed
+140 -2

No files matched your search

@@ -15,6 +15,8 @@
#include "Engine/World.h"
#include "TimerManager.h"
#include "Containers/Ticker.h"
#include "Camera/PlayerCameraManager.h"
#include "UnrealClient.h"
#include "HAL/IConsoleManager.h"
#include "Kismet/GameplayStatics.h"
#include "Misc/CommandLine.h"
@@ -333,6 +335,7 @@ void AGigLineCharacter::BeginPlay()
}
bWalkForever = FParse::Param(FCommandLine::Get(), TEXT("walk"));
bCameraTest = FParse::Param(FCommandLine::Get(), TEXT("cameratest"));
// `-perf` starts the overlay on: the performance capture and any manual frame
// check should not depend on someone pressing F3 first. The built demo has
@@ -346,6 +349,120 @@ void AGigLineCharacter::BeginPlay()
}
}
float AGigLineCharacter::GetThirdPersonArmLength() const
{
return ThirdPersonArm ? ThirdPersonArm->TargetArmLength : 0.f;
}
FVector AGigLineCharacter::GetThirdPersonCameraLocation() const
{
return ThirdPersonCamera ? ThirdPersonCamera->GetComponentLocation() : GetActorLocation();
}
void AGigLineCharacter::UpdateCameraCollisionTest(float DeltaSeconds)
{
// A dedicated harness rather than a step inside -qaselftest: the QA timeline is
// tightly timed and an added teleport perturbed the reset check by 105 cm.
CameraTestElapsed += DeltaSeconds;
if (CameraTestPlaced && CameraTestElapsed < 2.0f)
{
return;
}
if (!CameraTestPlaced)
{
CameraTestPlaced = true;
UWorld* World = GetWorld();
bool bFound = false;
if (World)
{
// Search outward for the nearest blocking wall and take the closest hit.
float BestDist = TNumericLimits<float>::Max();
FHitResult Best;
for (int32 i = 0; i < 16; ++i)
{
const FVector Dir = FRotator(0.f, i * 22.5f, 0.f).Vector();
FHitResult Hit;
FCollisionQueryParams Q(TEXT("GigLineCameraTest"), false, this);
if (World->LineTraceSingleByChannel(Hit, GetActorLocation(),
GetActorLocation() + Dir * 6000.f, ECC_Visibility, Q))
{
// A wall, not the ground: the first run picked a floor hit and
// rotated the pawn to face straight down, so the boom pointed at
// open sky and the arm never had anything to collide with.
if (FMath::Abs(Hit.ImpactNormal.Z) > 0.5f)
{
continue;
}
if (Hit.Distance < BestDist)
{
BestDist = Hit.Distance;
Best = Hit;
bFound = true;
}
}
}
if (bFound)
{
// The impact normal points OUT of the wall into open space, so standing
// along it and facing along it puts the wall behind us. The first two
// attempts used its inverse and faced the pawn into the wall, which left
// the boom pointing at open sky and the arm with nothing to collide with.
SetActorLocation(Best.ImpactPoint + Best.ImpactNormal * 70.f);
LastWallNormal = Best.ImpactNormal;
const FRotator Facing = Best.ImpactNormal.Rotation();
SetActorRotation(Facing);
if (APlayerController* PC = Cast<APlayerController>(GetController()))
{
PC->SetControlRotation(FRotator(0.f, Facing.Yaw, 0.f));
}
}
}
if (!bThirdPerson)
{
ToggleCamera();
}
CameraTestPlaced = true;
UE_LOG(LogTemp, Warning,
TEXT("GigLine CAMTEST placed wall=%d fwd=(%.2f,%.2f) normal=(%.2f,%.2f) collisionTest=%d"),
bFound ? 1 : 0, GetActorForwardVector().X, GetActorForwardVector().Y,
LastWallNormal.X, LastWallNormal.Y,
ThirdPersonArm && ThirdPersonArm->bDoCollisionTest ? 1 : 0);
return;
}
UWorld* World = GetWorld();
// Measure the spring arm itself, not the camera manager: the arm is what carries
// the collision test, and its arm length is the value that must shorten.
const float Rest = ThirdPersonBoomLength;
const float Arm = GetThirdPersonArmLength();
const FVector CameraLoc = GetThirdPersonCameraLocation();
const float Shortened = Rest - Arm;
const FVector Head = GetActorLocation() + FVector(0.f, 0.f, 60.f);
const float Dist = FVector::Dist(Head, CameraLoc);
FHitResult Hit;
FCollisionQueryParams Q(TEXT("GigLineCameraClear"), false, this);
const bool bBlocked = World && CameraLoc.SizeSquared() > 1.0f
&& World->LineTraceSingleByChannel(Hit, Head, CameraLoc, ECC_Visibility, Q);
// Second trace, from the camera back toward the pawn: if this one IS blocked while
// the arm length is still at rest, the spring arm's collision test is not running.
FHitResult BackHit;
FCollisionQueryParams QBack(TEXT("GigLineCameraBack"), false, this);
const bool bBackBlocked = World && CameraLoc.SizeSquared() > 1.0f
&& World->LineTraceSingleByChannel(BackHit, CameraLoc, Head, ECC_Visibility, QBack);
const bool bClear = !bBlocked || Hit.Distance > Dist - 25.f;
const bool bShortened = Shortened > 30.f;
UE_LOG(LogTemp, Warning,
TEXT("GigLine CAMTEST arm %.0f of %.0f cm (shortened %.0f), cam %.0f cm from head, "
"blocked=%d -> %s"),
Arm, Rest, Shortened, Dist, bBlocked ? 1 : 0,
(bClear || bShortened) ? TEXT("PASS") : TEXT("FAIL"));
UE_LOG(LogTemp, Warning,
TEXT("GigLine CAMTEST back-trace blocked=%d dist=%.0f (arm rest %.0f)"),
bBackBlocked ? 1 : 0, bBackBlocked ? BackHit.Distance : -1.f, Rest);
FScreenshotRequest::RequestScreenshot(TEXT("review-demo-camera-collision"), true, false);
bCameraTest = false;
}
void AGigLineCharacter::ToggleDebugOverlay()
{
static IConsoleVariable* Var = IConsoleManager::Get().FindConsoleVariable(TEXT("gigline.Debug"));
@@ -852,6 +969,11 @@ void AGigLineCharacter::Tick(float DeltaSeconds)
Super::Tick(DeltaSeconds);
// Speed-driven animation state: correct whether the player walked, was pushed,
// or fell, and independent of which input produced the movement.
if (bCameraTest)
{
UpdateCameraCollisionTest(GetWorld() ? GetWorld()->GetDeltaSeconds() : 0.f);
}
WalkForever();
UpdateBodyAnimation();
@@ -200,6 +200,18 @@ protected:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> ToggleDebugAction;
/** Current boom length and camera position of the third-person rig. */
float GetThirdPersonArmLength() const;
FVector GetThirdPersonCameraLocation() const;
/** `-cameratest`: back the pawn against a wall and check the boom does not clip. */
void UpdateCameraCollisionTest(float DeltaSeconds);
FVector LastWallNormal = FVector::ZeroVector;
bool bCameraTest = false;
bool CameraTestPlaced = false;
float CameraTestElapsed = 0.0f;
/** Flips the gigline.Debug console variable that the HUD reads. */
void ToggleDebugOverlay();
+6 -2
View File
@@ -11,9 +11,13 @@ Actionable task list for this project. This file is the single, central place fo
## 0.2.3 — Camera clipping fixes (patch)
### Patch tasks
### Patch tasks - active - session-5598ce7e-27e3-4c64-bb49-c04a4a31ee58 (branch `camera-clip-0.2.3`)
- [ ] Fix remaining camera clipping cases in `0.2.x`.
- [A] Fix remaining camera clipping cases in `0.2.x`. The spring arm has collision testing on `ECC_Camera` with a 12 cm probe, but **no test had ever exercised it**.
- **Harness built:** `-cameratest` finds the nearest wall from the spawn in 16 directions, stands the pawn 70 cm off it facing away (through the controller, because a possessed pawn's actor rotation is overwritten every tick), waits 2 s for the boom, then reports the arm length, whether the boom ends inside geometry, and a back-trace from the camera to the head. It also requests a screenshot so the placement can be checked by eye.
- **Inconclusive so far, and the setup is the suspect, not the engine.** Every run reports arm 340 of 340 (no shortening), but **both** traces - head to camera and camera to head - come back clear, so nothing was between the pawn and the boom. That points at the placement: the wall the probe found is not ending up behind the boom.
- **Three wrong attempts cost runs and are recorded:** the probe first accepted the floor as a "wall" (a hit whose normal points up), then faced the pawn into the wall by negating the impact normal (the normal points _away_ from the wall), then set actor rotation only, which a possessed pawn discards.
- **Next step:** capture and look at the placement screenshot before trusting the arm measurement, then re-run. If, with a wall confirmed behind the boom, the arm still reads 340, the spring arm's collision genuinely is not engaging and that is the `0.2.3` fix.
- [ ] Tag `v0.2.3` and write `CHANGELOG.md` entry; then remove this section.
## 0.3.0 — Chaos vehicle, enter/exit, closed driving route (weeks 10–13)