test(vehicle): a review shot, and the judge that proves the car is missing

Adds demo-vehicle to the camera list and the visual targets, plus shot 9 in
Screenshots/README.md - marked FAIL, because that is what it shows.

The vehicle is staged and runs: spawned at (-12950,-900,120), 9 visual panels attached,
panel 0 visible with a 238 cm extent, settled at Z 119 cm. The capture shows an empty road
with a soft shadow. Two checks disagree in a useful way: visual_loop --verify PASSES the
frame (it only tests size, blankness and tonal range, so a well-exposed empty road passes),
while judge_screenshot returns verdict=no confidence=1.0.

Strongest clue, recorded in TODO.md: a downward trace from the vehicle finds no ground at
all (ground at Z -9999), so the car sits over a surface without collision or inside
geometry - the shadow and the panels are not in the same place.

Diagnostics added along the way: mesh/visibility/bounds at BeginPlay, per-panel world
positions and extents, and the ground trace. The visual panels were also unhidden from the
physics mesh: SetVisibility(false, ...) hid them whether or not propagation was requested.
This commit is contained in:
zwitschi committed 2026-11-09 15:27:58 +01:00
1 parent 5dd53ac30b
commit 627737062b
42 files changed
+172 -44

No files matched your search

@@ -36,6 +36,68 @@ AGigLineVehicle::AGigLineVehicle()
}
}
void AGigLineVehicle::BuildVisuals()
{
// One component per authored system: body, glass, front and rear lights, mirrors and
// the four wheels. Each points at the static asset imported from the same Blender
// build, so the render matches what the model tool produced.
struct FPanelSpec
{
const TCHAR* Path;
FVector Offset;
FRotator Rotation;
};
const float HalfBase = 139.0f; // wheelbase 278 cm / 2
const float HalfTrack = 81.0f; // track 162 cm / 2
const FPanelSpec Specs[] = {
{TEXT("/Game/Hollywood/Vehicle/SM_Vehicle_Body.SM_Vehicle_Body"), FVector::ZeroVector, FRotator::ZeroRotator},
{TEXT("/Game/Hollywood/Vehicle/SM_Vehicle_Glass.SM_Vehicle_Glass"), FVector::ZeroVector, FRotator::ZeroRotator},
{TEXT("/Game/Hollywood/Vehicle/SM_Vehicle_Light_Front.SM_Vehicle_Light_Front"), FVector::ZeroVector, FRotator::ZeroRotator},
{TEXT("/Game/Hollywood/Vehicle/SM_Vehicle_Light_Rear.SM_Vehicle_Light_Rear"), FVector::ZeroVector, FRotator::ZeroRotator},
{TEXT("/Game/Hollywood/Vehicle/SM_Vehicle_Mirrors.SM_Vehicle_Mirrors"), FVector::ZeroVector, FRotator::ZeroRotator},
{TEXT("/Game/Hollywood/Vehicle/SM_Vehicle_Wheel.SM_Vehicle_Wheel"), FVector(HalfBase, HalfTrack, -16.f), FRotator::ZeroRotator},
{TEXT("/Game/Hollywood/Vehicle/SM_Vehicle_Wheel.SM_Vehicle_Wheel"), FVector(HalfBase, -HalfTrack, -16.f), FRotator::ZeroRotator},
{TEXT("/Game/Hollywood/Vehicle/SM_Vehicle_Wheel.SM_Vehicle_Wheel"), FVector(-HalfBase, HalfTrack, -16.f), FRotator::ZeroRotator},
{TEXT("/Game/Hollywood/Vehicle/SM_Vehicle_Wheel.SM_Vehicle_Wheel"), FVector(-HalfBase, -HalfTrack, -16.f), FRotator::ZeroRotator},
};
int32 Index = 0;
for (const FPanelSpec& Spec : Specs)
{
if (UStaticMesh* Mesh = LoadObject<UStaticMesh>(nullptr, Spec.Path))
{
UStaticMeshComponent* Component = NewObject<UStaticMeshComponent>(this,
*FString::Printf(TEXT("Panel_%d"), Index));
// Attach to the actor root rather than to the physics mesh, so the visuals are
// independent of the simulated component's own state.
Component->SetupAttachment(GetRootComponent());
Component->SetStaticMesh(Mesh);
Component->SetRelativeLocation(Spec.Offset);
Component->SetRelativeRotation(Spec.Rotation);
Component->SetCollisionEnabled(ECollisionEnabled::NoCollision);
Component->SetMobility(EComponentMobility::Movable);
Component->RegisterComponent();
Panels.Add(Component);
++Index;
}
}
UE_LOG(LogTemp, Warning, TEXT("GigLine vehicle: %d visual panels attached"), Panels.Num());
for (int32 PanelIndex = 0; PanelIndex < Panels.Num(); ++PanelIndex)
{
const UStaticMeshComponent* Panel = Panels[PanelIndex];
if (!Panel)
{
continue;
}
const FVector Where = Panel->GetComponentLocation();
const FBoxSphereBounds B = Panel->Bounds;
UE_LOG(LogTemp, Warning,
TEXT("GigLine vehicle: panel %d at (%.0f,%.0f,%.0f) visible=%d extent=%.0f"),
PanelIndex, Where.X, Where.Y, Where.Z,
Panel->IsVisible() ? 1 : 0, B.BoxExtent.GetMax());
}
}
void AGigLineVehicle::ConfigureWheels()
{
UChaosWheeledVehicleMovementComponent* Movement =
@@ -92,6 +154,17 @@ void AGigLineVehicle::BeginPlay()
{
Super::BeginPlay();
// The skeletal mesh is the physics driver only: its imported geometry is unusable
// (1 cm bounds), so it is hidden and the visible car comes from the static panels.
if (USkeletalMeshComponent* PhysicsMesh = GetMesh())
{
// Do not hide the physics mesh at all. It has no usable geometry (1 cm bounds), so
// it renders nothing either way, and every attempt to hide it also hid the visual
// panels - with and without propagation. Leaving it visible avoids the fight.
PhysicsMesh->SetCastShadow(false);
}
BuildVisuals();
// Wheel objects do not exist until the component is registered, so the per-wheel
// flags set in the constructor went nowhere - which is why the drive test measured
// zero speed with throttle held. Apply them again now that they exist, and give the
@@ -106,6 +179,16 @@ void AGigLineVehicle::BeginPlay()
Movement->TransmissionSetup.bUseAutomaticGears = true;
Movement->TransmissionSetup.FinalRatio = 3.5f;
Movement->SetThrottleInput(0.0f);
if (const USkeletalMeshComponent* BodyComp = GetMesh())
{
const USkeletalMesh* Asset = BodyComp->GetSkeletalMeshAsset();
UE_LOG(LogTemp, Warning,
TEXT("GigLine vehicle: mesh=%s visible=%d hidden=%d bounds=%s"),
Asset ? *Asset->GetName() : TEXT("NONE"),
BodyComp->IsVisible() ? 1 : 0,
BodyComp->bHiddenInGame ? 1 : 0,
*BodyComp->Bounds.BoxExtent.ToString());
}
UE_LOG(LogTemp, Warning,
TEXT("GigLine vehicle: %d wheel objects after register, torque %.0f"),
Movement->Wheels.Num(), Movement->EngineSetup.MaxTorque);
@@ -158,6 +241,17 @@ void AGigLineVehicle::Tick(float DeltaSeconds)
if (!bSettleReported && SettleElapsed > 3.0f)
{
bSettleReported = true;
if (UWorld* World = GetWorld())
{
FHitResult Hit;
FCollisionQueryParams Q(TEXT("GigLineVehicleGround"), false, this);
const bool bGround = World->LineTraceSingleByChannel(Hit, GetActorLocation(),
GetActorLocation() - FVector(0.f, 0.f, 2000.f), ECC_Visibility, Q);
UE_LOG(LogTemp, Warning,
TEXT("GigLine vehicle: ground at Z %.0f cm, actor %.0f cm, height above ground %.0f cm"),
bGround ? Hit.ImpactPoint.Z : -9999.f, GetActorLocation().Z,
bGround ? GetActorLocation().Z - Hit.ImpactPoint.Z : -9999.f);
}
const USkeletalMeshComponent* Body = GetMesh();
UE_LOG(LogTemp, Warning,
TEXT("GigLine vehicle: settled at Z %.0f cm, simulating=%d, speed=%.0f cm/s"),
@@ -29,6 +29,18 @@ public:
virtual void BeginPlay() override;
virtual void Tick(float DeltaSeconds) override;
/**
* Visual panels, as static mesh components.
*
* The skeletal vehicle asset imported with no usable geometry (runtime bounds of
* 1 cm), so the car was invisible in the scene while its physics body still cast a
* shadow - the "no vehicle in scene" report. The static assets are correct (body
* 476 x 176 x 128 cm), so the visible car is assembled from them and the skeletal
* mesh stays as the physics/animation driver only.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Visual")
TArray<TObjectPtr<UStaticMeshComponent>> Panels;
/** True once the vehicle reports a forward speed above a walking pace. */
bool IsMoving() const;
@@ -36,6 +48,9 @@ private:
/** Configure the Chaos wheels from the modelled dimensions. */
void ConfigureWheels();
/** Attach the visible panels from the imported static systems. */
void BuildVisuals();
float DriveElapsed = 0.f;
bool bDriveReported = false;
FVector DriveStart = FVector::ZeroVector;