17 Commits
Author SHA1 Message Date
zwitschi cb94438e0d feat(review): Blender screenshots beside the in-game captures, with signoff
Two tools for the comparison the review was missing.

- Tools/blender_screenshots.py renders out of a .blend: one image per mesh object, and
  four turntable angles of the whole scene, framed from the scene's own bounds so adding
  geometry never silently crops a shot. Output lands in Screenshots/blender/, beside the
  in-game captures.
- Tools/build_comparison_md.py writes Screenshots/COMPARISON.md: a table with the in-game
  capture and its Blender render side by side per row, then a signoff table with one row
  per in-game screen. Unmatched shots show an em dash rather than disappearing, because a
  missing Blender render is information too.

The first run already earns its keep: the Blender render of the owner's hot hatch is red
with clear glazing, while the in-game capture of the same asset is near-black, so the
material colour is being lost on import - visible in one glance in the new page.
2026-11-09 16:04:14 +01:00
zwitschi bb8ae3eb4b test(visual): the vehicle shot gets a reference, and the character/car height fixes
The suite caught an incomplete target: demo-vehicle had an empty references list, which the
visual-loop test requires. It now points at Art/blender/vehicle/hatch-reference.png, a render
of the owner's template, so the shot can be compared like the others rather than only judged
by the vision model.
2026-11-09 16:01:34 +01:00
zwitschi a17e982561 fix(placement): character and car stand on the ground, not above it
Both floats had different causes and both were measured.

Character: the player start sat at Z 600 while the capsule settles at Z 561 on this
pavement - the QA self-test has been reporting that settle height all along - so the soles
hung 39 cm in the air. The start now uses the measured 561.

Car: the template's lowest geometry is exhaust and underbody, so grounding the model on its
whole bounding box left the tyres 4 cm above the origin. It is grounded on its WHEELS now.
The staged height is 342 rather than the traced road height of 366, because at 367 the
render still showed a gap: the trace and the visible road mesh disagree by about 25 cm at
that spot, and the number is recorded as empirical rather than derived.

Also adds a foot-contact log to the character at BeginPlay (actor Z, ground, capsule half,
mesh bottom, gap) so the next float is a measurement rather than a look.
2026-11-09 15:59:52 +01:00
zwitschi b50afa4bcd feat(vehicle): the car renders, built from the owner's hot hatch template
The invisible-vehicle defect is closed. Shot 9 now shows the car instead of an empty road.

Tools/vehicle_from_template.py converts Art/blender/vehicle/hot hatch.blend into UE-ready
assets, following two skills:

  vehicle-artist - systems split (body 113k tris, trim 10.5k, glass 20.5k, lights 18.6k,
  wheels 47.8k), one UCX_ hull 1.45 x 3.94 x 0.76 m, two decimated LODs at 39.7k and 13.6k.
  unreal-export - scale normalised to a documented 4.05 m hot hatch before export (the
  template measures 7.26 m, factor 0.5579), then verified after import: body bounds
  1.51 x 4.02 x 1.22 m against the 4.05 m target.

Staged as five plain StaticMeshActors, because the earlier probe proved those render while
the same meshes attached to the vehicle pawn do not.

Open and recorded rather than hidden: judge_screenshot returns verdict=no confidence=0.9 -
the silhouette does not read as a hot hatch from this angle and the car floats slightly.
The likely causes are a system misclassification in the template conversion and an
origin-height error of a few centimetres.
2026-11-09 15:52:42 +01:00
zwitschi 021aa56121 test(vehicle): the pawn is the culprit, the meshes are fine
Decisive experiment: the same SM_Vehicle_Body staged as a plain StaticMeshActor 5.5 m
from the pawn renders, while the panels attached to the vehicle pawn do not - with
visible=1, the right mesh name, a 238 cm extent, MarkRenderStateDirty() and
UpdateBounds() all in place. The asset and its material are fine; the pawn's component
hierarchy is what keeps them out of the renderer.

So the car is now staged as nine plain StaticMeshActors (body, glass, front/rear lights,
mirrors, four wheels) alongside the physics pawn: visible, and not yet attached to the
driving pawn. A visible car that does not drive yet beats an invisible one that does.

Two defects found by the tools while chasing this, both fixed: the demo builder staged the
vehicle twice per run (the scene audit caught the duplicate label), and clear() could leave
actors behind, so it now repeats until nothing is removed.
2026-11-09 15:42:22 +01:00
zwitschi 1cddfbfc68 fix(vehicle): spawn on the road, not 2.5 m under it
Measured rather than guessed: a downward trace from the spawn point reports the road
surface at Z 366 cm, while the vehicle was spawned at Z 120. The car was buried; the only
thing visible in the capture was its shadow falling on the road above it. Spawn is now
Z 470, the camera aims at 430, and it settles at Z 470.

Not fixed, and recorded: with the car above ground the capture still shows road and a
shadow with no car. Panels register, report visible=1 with mesh=SM_Vehicle_Body and a
238 cm extent at the expected position, and MarkRenderStateDirty()/UpdateBounds() run
after registration. The shadow proves a collider is there; the visible geometry is not.

Screenshots/README.md now separates the two causes so the next person does not re-fix the
buried spawn.
2026-11-09 15:33:42 +01:00
zwitschi 627737062b 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.
2026-11-09 15:27:58 +01:00
zwitschi 5dd53ac30b fix(vehicle): one chassis body, so the car sits on the road
The vehicle floated 3.7 m above the street because the physics asset had a body per bone
and the four wheel bodies intersected the chassis body, pushing the whole thing up. It is
now a single chassis body on the root bone (452 x 205 x 40 cm) - the same shape Unreal's
vehicle template uses, because Chaos simulates the wheels itself and they must not be
separate rigid bodies.

Settle check, same spawn point:
  before: settled at Z 494 cm
  after:  settled at Z 119 cm

Added -drivetest, which holds full throttle for 3 s and reports speed and distance. It
fails, and the failure names its own cause: BeginPlay logs '0 wheel objects after register'
while the component reports 4 wheel setups, so no wheels exist to receive torque. Recorded
in TODO.md with the next step - check the skeleton bone names against the setups, then give
the front and rear pairs their own wheel subclasses instead of the base class.
2026-11-09 15:19:56 +01:00
zwitschi 4f3ed20e49 feat(vehicle): Chaos vehicle spawns and simulates, physics asset generated from script
The physics asset could not be made from Python (PhysicsAssetFactory exposes no mesh
property), so a small editor-only C++ helper calls FPhysicsAssetUtils::CreateFromSkeletalMesh
- the same code path as the editor's Create Physics Asset action. It produces
PA_Vehicle_Sedan with 6 bodies and assigns it to SK_Vehicle_Sedan.

AGigLineVehicle is a Chaos wheeled pawn that configures its four wheels from the modelled
dimensions (radius 33 cm, wheelbase 278, track 156) and is staged in DemoMap. A run logs:
  spawned at (-12950, -900, 120), 4 wheel setups
  settled at Z 494 cm, simulating=1, speed=0 cm/s

That last line is the defect and it is not hidden: the vehicle rests 3.7 m above the road,
so the generated physics bodies do not match the mesh. Recorded in TODO.md with the next
step - inspect the bodies against the mesh, then either fix the creation params or author
two boxes and four wheel spheres explicitly.
2026-11-09 15:14:49 +01:00
zwitschi 2e0421480c feat(vehicle): skeletal vehicle asset for Chaos, and the block that follows
Chaos drives a skeletal mesh, so the static system split was not drivable on its own. The
builder now also rigs the vehicle: a root bone plus one bone per wheel, every wheel skinned
to its own bone, exported with the character preset. SK_Vehicle_Sedan imports with
SK_Vehicle_Sedan_Skeleton (508 triangles).

Blocked, and recorded as blocked rather than worked around: Chaos needs a PhysicsAsset, and
unreal.PhysicsAssetFactory in 5.8 exposes no mesh property - setting skeletal_mesh fails
with 'Failed to find property'. Bodies cannot be generated from the mesh in a commandlet,
so the asset has to be authored once in the editor or built in C++ with FPhysicsAssetUtils.

Also fixed the build order: the skeletal join destroys the per-system objects, so the
report and LODs read them before the join - the first attempt died with 'StructRNA of type
Object has been removed'.
2026-11-09 14:53:35 +01:00
zwitschi 02cadd4964 feat(vehicle): split into systems, add real LODs, import to Unreal
Follows the vehicle-artist object split, which the first build violated: body, glass,
front and rear lights, mirrors and a single wheel mesh are now separate objects and
separate assets in /Game/Hollywood/Vehicle. A single merged mesh across all systems is
what the skill forbids for a game vehicle, and it also meant glass could not be made
transparent or lights emissive independently.

LODs decimate rather than duplicate: LOD1 38 triangles, LOD2 14, against 132 for LOD0.
The first pass copied the shell unchanged and produced 84-triangle 'LODs' that saved
nothing.

Imports verified with bounds: body 4.76 x 1.76 x 1.28 m, wheel 0.22 x 0.66 x 0.66 m,
which match the modelled dimensions.

Open, recorded: LOD group wiring needs StaticMeshEditorSubsystem.import_lod, which is
unavailable in a headless commandlet, so the LOD meshes sit as their own assets for now.
2026-11-09 14:46:02 +01:00
zwitschi a72bf85b2e feat(vehicle): model the sedan with collision hulls
First 0.3.0 asset. Tools/vehicle_build.py builds a 4.72 x 1.80 x 1.44 m sedan - 2.78 m
wheelbase, 1.62 m track, 0.33 m wheels - as a three-tier silhouette: sill slab, bonnet
and boot shoulders, cabin greenhouse, plus glazing, lamps, bumpers and mirrors. 252
triangles.

Collision is four UCX_-prefixed convex hulls (body, cabin, two bumpers) per the
collision-proxy convention, hidden from render so the preview reads as the car rather
than the colliders.

Two corrections from the preview, both visible in the saved image: the first build was
one tall slab plus a floating cabin and read as a box truck, and the wheels were buried
inside a full-width sill - the sill is now 1.56 m so the wheels stand proud.

Reports every dimension to JSON so the Chaos vehicle config can read the wheelbase, track
and radius instead of restating them.
2026-11-09 14:43:20 +01:00
zwitschi 73b5bd6cd1 Merge pull request #37: v0.2.3 — camera collision fixes
The spring arm ticks with the rig now, so its collision probe runs and the boom shortens (340 -> 288 cm against a wall spawned behind the pawn).
2026-11-09 14:41:49 +01:00
zwitschi 82cafc2f29 fix(camera): prove the boom shortens, and record how to repeat it
With the spring arm activated the collision now works: a 2 m wall spawned 4 m behind the
pawn shortens the boom from 340 cm to 288 cm - 12 cm clear of the wall, matching the probe
size. -cameratest logs it, writes a placement screenshot, and the measurement is recorded
as a release check for 0.2.3.

Two traps documented because each one made a working mechanism look broken:
  - a spawned static-mesh actor has no collision unless it is created deferred and given
    its mesh and collision before FinishSpawning (three runs reported blocks trace=0)
  - TargetArmLength is not rewritten when the boom shortens, so reading the property
    reports the requested length; the camera's distance from the head is the measurement

Writes the 0.2.3 changelog entry and prunes the section from TODO.md.
2026-11-09 14:40:18 +01:00
zwitschi 3e4dbd1014 fix(camera): activate the spring arm with the third-person rig
Real defect, found by reading the code rather than by the harness: the spring arm was
created with SetAutoActivate(false), and ToggleCamera only activated the camera. A
deactivated spring arm does not tick, and its collision probe runs in that tick, so the
boom could never shorten no matter what stood behind the pawn. ToggleCamera and BeginPlay
now activate the arm alongside the camera.

The harness that was meant to prove the behaviour is still not working, and that is
recorded rather than glossed: -cameratest spawns a wall behind the pawn, but neither a
spawned static-mesh actor nor a runtime blocking volume blocks a trace (both report
blocks trace=0), so every 'arm 340 of 340' line so far was measured with no wall present
and is not evidence either way.
2026-11-09 14:37:03 +01:00
zwitschi a6701aa3d1 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.
2026-11-09 14:33:57 +01:00
zwitschi d8b44f1e57 Merge pull request #36: v0.2.2 — the character pass
Clothing, hair cards, character blueprints and render-vs-blueprint comparison, raised sun, walk cycle starting at maximum stride. All eight review shots approved.
2026-11-09 14:26:22 +01:00
121 changed files with 2179 additions and 57 deletions

No files matched your search

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+22
View File
@@ -8,6 +8,28 @@ Format: [Keep a Changelog](https://keepachangelog.com/) style, versions follow [
Nothing yet.
## [0.2.3] — 2026-11-09
Camera collision worked in the code and never in the game.
### Fixed
- **The third-person boom never shortened.** The spring arm was created with `SetAutoActivate(false)` and `ToggleCamera()` activated only the camera. A deactivated spring arm does not tick, and its collision probe runs in that tick, so the boom could not shorten no matter what stood behind the pawn. Both `ToggleCamera` and `BeginPlay` now activate the arm with the rig.
### Added
- **`-cameratest`** — backs the pawn against a spawned 2 m wall, waits for the boom, and reports the shortening, whether the boom ends inside geometry, and a back-trace from camera to head. It writes `review-demo-camera-collision.png` so the placement can be checked by eye.
### Verified
- With a wall 4 m behind the pawn the boom shortens from 340 cm to **288 cm** — 12 cm clear of the wall, matching the probe size. Recorded as a release check.
- 8 review shots verify; 95 tool tests pass.
### Notes for future work
- **A spawned static-mesh actor has no collision unless it is deferred.** Setting the mesh and collision profile after `SpawnActor` produced a wall that blocked nothing (`blocks trace=0`) on three consecutive attempts. `SpawnActorDeferred` → set mesh and collision → `FinishSpawning` builds the physics state correctly.
- **`TargetArmLength` is not rewritten when the boom shortens.** The spring arm applies the shortened length to the camera, so the measurement must be the camera's distance from the head; reading the property reports the requested length and looks like a failure.
## [0.2.2] — 2026-11-09
The character becomes a character: clothed, haired, and measured against a generated blueprint instead of judged by eye.
+1
View File
@@ -42,6 +42,7 @@ Measured at 1440p on the development workstation: Bazzite, Ryzen 7 7700X, 64 GB
| `0.1.0` | Placement vs references | PASS (with deliberate simplifications) | Section below, 2026-11-08 | 2026-11-08 |
| `0.1.0` | Collision and packaging | PASS — no blocking collision, no invisible walls, no seam gaps (see audit note) | `-qaselftest` on the packaged build, 2026-11-08 | 2026-11-08 |
| `0.1.0` | 60 FPS at 1440p | PASS — 2.42 ms avg (414 fps), p99 3.01 ms at 2560×1440, TSR on, VSync off, no FG | `-qaselftest` perf capture, `Docs/perf/0.1.0-walk-frametimes.csv` | 2026-11-08 |
| `0.2.3` | Third-person camera does not clip through geometry | PASS — with a 2 m wall spawned 4 m behind the pawn the boom shortens from 340 cm to **288 cm** (12 cm clear of the wall, matching the probe size). `-cameratest` logs it and captures the placement | `UnrealEditor-Cmd <uproject> /Game/Hollywood/DemoMap -game -thirdperson -cameratest`, `Saved/Logs/GigLine.log` | 2026-11-09 |
| `0.2.0` | 60 FPS at 1440p | PASS — 2.64 ms avg (379 fps), p50 2.54, p95 3.39, p99 3.76, max 4.05 ms at 2560×1440, TSR on, VSync off, no FG | `-qaselftest` perf capture, `Docs/perf/0.2.0-walk-frametimes.csv` | 2026-11-09 |
| `0.2.0` | Debug overlay cost | PASS — same route with the F3 overlay drawn: 2.77 ms avg (361 fps), p50 2.64, p95 3.52, p99 3.87, max 4.43 ms; about **5 %** heavier, still 6× the floor. The overlay is on for `-perf`/`-qaselftest`, off in the built demo | `Docs/perf/0.2.0-walk-frametimes-overlay.csv` | 2026-11-09 |
| `0.1.0` | Fresh checkout build | PASS — clean clone of `main` (`4bc41e9`), `git lfs pull` + `check-lfs.sh` OK (130 LFS files), both `GigLineEditor` and `GigLine` targets built with the documented engine | `/tmp/fresh-check`, 2026-11-08 | 2026-11-08 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -14,5 +14,19 @@ public class GigLine : ModuleRules
"InputCore",
"EnhancedInput"
});
// Editor-only: the vehicle physics asset is generated from the skeletal mesh with
// FPhysicsAssetUtils, which lives in the Developer/PhysicsUtilities module. Chaos
// Vehicles also need the physics vehicle module at runtime.
if (Target.bBuildEditor)
{
PrivateDependencyModuleNames.AddRange(new string[]
{
"UnrealEd",
"PhysicsUtilities",
});
}
PrivateDependencyModuleNames.Add("ChaosVehicles");
}
}
@@ -15,6 +15,9 @@
#include "Engine/World.h"
#include "TimerManager.h"
#include "Containers/Ticker.h"
#include "Camera/PlayerCameraManager.h"
#include "Engine/StaticMeshActor.h"
#include "UnrealClient.h"
#include "HAL/IConsoleManager.h"
#include "Kismet/GameplayStatics.h"
#include "Misc/CommandLine.h"
@@ -311,6 +314,11 @@ void AGigLineCharacter::BeginPlay()
// `-walk` holds forward input from the first frame. The screenshot run uses it
// with `-thirdperson` so a capture can show the walk clip mid-stride: an idle
// shot cannot prove the animation set, and a still cannot show a stride.
if (ThirdPersonArm)
{
ThirdPersonArm->SetActive(bThirdPerson);
}
// Bind the character materials here rather than in the constructor: the slot
// assignment made there never reached the render. LoadObject, not FObjectFinder,
// because FObjectFinder may only be used inside a constructor - putting one in
@@ -332,7 +340,23 @@ void AGigLineCharacter::BeginPlay()
}
}
// Foot-contact diagnostic: how far the body's lowest point sits above the ground.
if (UWorld* World = GetWorld())
{
FHitResult Hit;
FCollisionQueryParams Q(TEXT("GigLineFoot"), false, this);
const bool bGround = World->LineTraceSingleByChannel(Hit, GetActorLocation(),
GetActorLocation() - FVector(0.f, 0.f, 3000.f), ECC_Visibility, Q);
const float CapsuleHalf = GetCapsuleComponent() ? GetCapsuleComponent()->GetScaledCapsuleHalfHeight() : -1.f;
const float MeshBottom = GetMesh() ? (GetMesh()->Bounds.Origin - GetMesh()->Bounds.BoxExtent).Z : 0.f;
UE_LOG(LogTemp, Warning,
TEXT("GigLine foot: actor Z %.0f, ground %.0f, capsule half %.0f, mesh bottom %.0f, gap %.0f cm"),
GetActorLocation().Z, bGround ? Hit.ImpactPoint.Z : -9999.f, CapsuleHalf, MeshBottom,
bGround ? MeshBottom - Hit.ImpactPoint.Z : -9999.f);
}
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 +370,102 @@ 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)
{
// Deterministic on purpose. Finding a wall in the street wasted four runs: the
// probe hit the floor, then faced the pawn into the wall, then the actor rotation
// was overwritten by the controller, and even when placement looked right both
// traces came back clear. A spawned wall behind the pawn removes every one of
// those variables.
CameraTestElapsed += DeltaSeconds;
UWorld* World = GetWorld();
if (!World)
{
return;
}
if (!CameraTestPlaced)
{
CameraTestPlaced = true;
// A 6 m cube, 1.6 m behind the pawn: close enough that a 340 cm boom must hit
// it, far enough that the capsule is clear of it.
const FVector Behind = -GetActorForwardVector();
const FVector Where = GetActorLocation() + Behind * 400.f - FVector(0.f, 0.f, 90.f);
FActorSpawnParameters Params;
Params.ObjectFlags |= RF_Transient;
// Static mesh cube, 2 m thick, 4 m behind. The earlier failures had the pawn
// INSIDE a 6 m cube centred 1.6 m back, so every trace started inside geometry
// and reported no hit - the wall existed, the measurement was inside it.
// Deferred spawn: the mesh and collision must be set BEFORE the actor is
// registered, otherwise the physics state is built for an empty component and
// the wall exists visually while blocking nothing (every earlier run reported
// "blocks trace=0" for exactly that reason).
CameraTestWall = World->SpawnActorDeferred<AStaticMeshActor>(
AStaticMeshActor::StaticClass(), FTransform(Where), this, nullptr,
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
if (CameraTestWall)
{
UStaticMeshComponent* Comp = CameraTestWall->GetStaticMeshComponent();
Comp->SetMobility(EComponentMobility::Movable);
Comp->SetStaticMesh(LoadObject<UStaticMesh>(
nullptr, TEXT("/Engine/BasicShapes/Cube.Cube")));
Comp->SetCollisionProfileName(TEXT("BlockAll"));
CameraTestWall->SetActorScale3D(FVector(6.f, 2.f, 6.f));
CameraTestWall->FinishSpawning(FTransform(Where));
}
UE_LOG(LogTemp, Warning, TEXT("GigLine CAMTEST wall spawned behind pawn at (%.0f,%.0f,%.0f)"),
Where.X, Where.Y, Where.Z);
return;
}
if (CameraTestElapsed < 2.0f)
{
return;
}
const float Rest = ThirdPersonBoomLength;
const float Arm = GetThirdPersonArmLength();
const FVector CameraLoc = GetThirdPersonCameraLocation();
const FVector Head = GetActorLocation() + FVector(0.f, 0.f, 60.f);
const float Dist = FVector::Dist(Head, CameraLoc);
// The spring arm shortens internally and applies the result to the camera; it does
// NOT rewrite TargetArmLength, so the arm length is always the requested one. The
// measurement that matters is the camera's distance from the head.
const bool bShortened = (Rest - Dist) > 40.f;
// Prove the spawned wall has collision at all before blaming the arm.
FHitResult WallHit;
FCollisionQueryParams QW(TEXT("GigLineCameraWall"), false, this);
const bool bWallBlocks = World->LineTraceSingleByChannel(
WallHit, GetActorLocation(), GetActorLocation() - GetActorForwardVector() * 600.f,
ECC_Visibility, QW);
UE_LOG(LogTemp, Warning, TEXT("GigLine CAMTEST spawned wall blocks trace=%d dist=%.0f"),
bWallBlocks ? 1 : 0, bWallBlocks ? WallHit.Distance : -1.f);
UE_LOG(LogTemp, Warning,
TEXT("GigLine CAMTEST wall behind pawn: boom %.0f cm (requested %.0f, shortened %.0f) -> %s"),
Dist, Rest, Rest - Dist,
bShortened ? TEXT("PASS (boom shortened)") : TEXT("FAIL (no shortening)"));
FScreenshotRequest::RequestScreenshot(TEXT("review-demo-camera-collision"), true, false);
if (CameraTestWall)
{
CameraTestWall->Destroy();
CameraTestWall = nullptr;
}
bCameraTest = false;
}
void AGigLineCharacter::ToggleDebugOverlay()
{
static IConsoleVariable* Var = IConsoleManager::Get().FindConsoleVariable(TEXT("gigline.Debug"));
@@ -852,6 +972,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();
@@ -866,6 +991,15 @@ void AGigLineCharacter::ToggleCamera()
bThirdPerson = !bThirdPerson;
FirstPersonCamera->SetActive(!bThirdPerson);
ThirdPersonCamera->SetActive(bThirdPerson);
// The ARM must be active too, not only the camera. It is created inactive to keep
// the first-person path free, but a deactivated spring arm does not tick - and its
// collision probe runs in that tick. Leaving it off was why the boom never
// shortened: with a 6 m wall spawned 1.6 m behind the pawn the camera still sat at
// its full 340 cm.
if (ThirdPersonArm)
{
ThirdPersonArm->SetActive(bThirdPerson);
}
GetMesh()->SetHiddenInGame(!bThirdPerson);
if (HairMesh)
{
@@ -200,6 +200,21 @@ 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);
UPROPERTY()
TObjectPtr<class AStaticMeshActor> CameraTestWall;
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();
@@ -0,0 +1,288 @@
#include "GigLineVehicle.h"
#include "ChaosWheeledVehicleMovementComponent.h"
#include "Components/SkeletalMeshComponent.h"
// From the Blender report (Art/blender/vehicle/report.json). Kept as named constants so a
// model change is a compile-time mismatch to fix, not a number buried in a setup call.
namespace GigLineVehicleDims
{
constexpr float WheelRadiusCm = 33.0f;
constexpr float WheelBaseCm = 278.0f;
constexpr float TrackCm = 156.0f;
}
AGigLineVehicle::AGigLineVehicle()
{
PrimaryActorTick.bCanEverTick = true;
if (USkeletalMeshComponent* Body = GetMesh())
{
static ConstructorHelpers::FObjectFinder<USkeletalMesh> MeshFinder(
TEXT("/Game/Hollywood/Vehicle/SK_Vehicle_Sedan.SK_Vehicle_Sedan"));
if (MeshFinder.Succeeded())
{
Body->SetSkeletalMesh(MeshFinder.Object);
}
Body->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
Body->SetCollisionProfileName(TEXT("PhysicsActor"));
Body->SetSimulatePhysics(true);
}
if (UChaosWheeledVehicleMovementComponent* Movement =
Cast<UChaosWheeledVehicleMovementComponent>(GetVehicleMovementComponent()))
{
ConfigureWheels();
}
}
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();
// Runtime-created components need their render state forced: without this the
// panels registered and reported visible but never reached the renderer.
Component->MarkRenderStateDirty();
Component->UpdateBounds();
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 mesh=%s"),
PanelIndex, Where.X, Where.Y, Where.Z,
Panel->IsVisible() ? 1 : 0, B.BoxExtent.GetMax(),
Panel->GetStaticMesh() ? *Panel->GetStaticMesh()->GetName() : TEXT("NONE"));
}
}
void AGigLineVehicle::ConfigureWheels()
{
UChaosWheeledVehicleMovementComponent* Movement =
Cast<UChaosWheeledVehicleMovementComponent>(GetVehicleMovementComponent());
if (!Movement)
{
return;
}
// One entry per wheel bone, in the order the rig was built. The front pair steers;
// the rear pair drives, which is the simplest configuration that reads as a car.
struct FWheelSpec
{
const TCHAR* Bone;
bool bSteer;
bool bDrive;
};
static const FWheelSpec Specs[] = {
{TEXT("Wheel_FL"), true, false},
{TEXT("Wheel_FR"), true, false},
{TEXT("Wheel_RL"), false, true},
{TEXT("Wheel_RR"), false, true},
};
Movement->WheelSetups.Empty();
for (const FWheelSpec& Spec : Specs)
{
FChaosWheelSetup Setup;
// FChaosWheelSetup has only the class, the bone and an offset in 5.8 - there is
// no per-setup disable flag, and steering/drive flags live on the wheel object.
Setup.WheelClass = UChaosVehicleWheel::StaticClass();
Setup.BoneName = FName(Spec.Bone);
Movement->WheelSetups.Add(Setup);
}
// Radius and stance for every wheel; the per-wheel subclass would normally carry
// these, so they are applied after the setups exist.
for (int32 Index = 0; Index < Movement->WheelSetups.Num(); ++Index)
{
UChaosVehicleWheel* Wheel = Movement->Wheels.IsValidIndex(Index)
? Movement->Wheels[Index] : nullptr;
if (!Wheel)
{
continue;
}
Wheel->WheelRadius = GigLineVehicleDims::WheelRadiusCm;
Wheel->WheelWidth = 22.0f;
Wheel->bAffectedBySteering = Specs[Index].bSteer;
Wheel->bAffectedByEngine = Specs[Index].bDrive;
}
}
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();
// Where is the road, really? A downward trace from the vehicle found nothing, and the
// capture shows the car is not on screen at all - so measure the surface from above
// before assuming the spawn height is right.
if (UWorld* World = GetWorld())
{
const FVector From = GetActorLocation() + FVector(0.f, 0.f, 1000.f);
FHitResult Hit;
FCollisionQueryParams Q(TEXT("GigLineVehicleRoad"), false, this);
const bool bHit = World->LineTraceSingleByChannel(Hit, From,
From - FVector(0.f, 0.f, 4000.f), ECC_Visibility, Q);
UE_LOG(LogTemp, Warning,
TEXT("GigLine vehicle: surface below spawn at Z %.0f cm (hit=%d, actor at %.0f)"),
bHit ? Hit.ImpactPoint.Z : -9999.f, bHit ? 1 : 0, GetActorLocation().Z);
}
// 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
// engine a torque curve to actually make force.
if (UChaosWheeledVehicleMovementComponent* Movement =
Cast<UChaosWheeledVehicleMovementComponent>(GetVehicleMovementComponent()))
{
ConfigureWheels();
Movement->EngineSetup.MaxTorque = 500.0f;
Movement->EngineSetup.MaxRPM = 6000.0f;
Movement->EngineSetup.EngineIdleRPM = 900.0f;
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);
}
UE_LOG(LogTemp, Warning, TEXT("GigLine vehicle: spawned at (%.0f, %.0f, %.0f), %d wheel setups"),
GetActorLocation().X, GetActorLocation().Y, GetActorLocation().Z,
GetVehicleMovementComponent()
? Cast<UChaosWheeledVehicleMovementComponent>(GetVehicleMovementComponent())
->WheelSetups.Num()
: 0);
}
void AGigLineVehicle::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
// One settled-state report: proves the physics body exists, is simulating, and has
// come to rest on the road rather than falling through it. Everything else about the
// driving behaviour is measured later; this is the "is it real" check.
// `-drivetest`: hold full throttle from 1 s to 4 s and report the result. A vehicle
// that spawns is not a vehicle that drives.
if (FParse::Param(FCommandLine::Get(), TEXT("drivetest")))
{
DriveElapsed += DeltaSeconds;
if (UChaosVehicleMovementComponent* Movement = GetVehicleMovementComponent())
{
if (DriveElapsed > 1.0f && DriveElapsed < 4.0f)
{
Movement->SetThrottleInput(1.0f);
}
else if (DriveElapsed >= 4.0f && !bDriveReported)
{
Movement->SetThrottleInput(0.0f);
bDriveReported = true;
const float SpeedCm = Movement->GetForwardSpeed();
const float TravelledM = FVector::Dist2D(GetActorLocation(), DriveStart) / 100.f;
UE_LOG(LogTemp, Warning,
TEXT("GigLine vehicle DRIVETEST speed=%.0f cm/s (%.1f km/h), travelled %.1f m -> %s"),
SpeedCm, SpeedCm * 0.036f, TravelledM,
(SpeedCm > 200.f && TravelledM > 1.0f) ? TEXT("PASS") : TEXT("FAIL"));
}
}
}
if (DriveStart.IsZero() && DriveElapsed > 1.0f)
{
DriveStart = GetActorLocation();
}
SettleElapsed += 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"),
GetActorLocation().Z,
Body && Body->IsSimulatingPhysics() ? 1 : 0,
GetVehicleMovementComponent() ? GetVehicleMovementComponent()->GetForwardSpeed() : 0.f);
}
}
bool AGigLineVehicle::IsMoving() const
{
const UChaosVehicleMovementComponent* Movement = GetVehicleMovementComponent();
return Movement && FMath::Abs(Movement->GetForwardSpeed()) > 50.0f;
}
@@ -0,0 +1,60 @@
#pragma once
#include "CoreMinimal.h"
#include "WheeledVehiclePawn.h"
#include "GigLineVehicle.generated.h"
class UChaosWheeledVehicleMovementComponent;
/**
* The one drivable vehicle in the demo.
*
* A skeletal mesh with a root bone and four wheel bones, driven by Chaos. Every
* dimension it configures — wheel radius, wheelbase, track — comes from the Blender
* build's report rather than being typed twice: the numbers are repeated here as
* constants with the report path in the comment so a change to the model shows up as a
* mismatch instead of a silent drift.
*
* Model: `Art/blender/vehicle/gigline-sedan.blend`, report `Art/blender/vehicle/report.json`
* (4.72 x 1.80 x 1.44 m, wheelbase 2.78 m, track 1.62 m, wheel radius 0.33 m).
*/
UCLASS()
class GIGLINE_API AGigLineVehicle : public AWheeledVehiclePawn
{
GENERATED_BODY()
public:
AGigLineVehicle();
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;
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;
float SettleElapsed = 0.f;
bool bSettleReported = false;
};
@@ -0,0 +1,71 @@
#include "GigLineVehicleBuilder.h"
#if WITH_EDITOR
#include "AssetToolsModule.h"
#include "Engine/SkeletalMesh.h"
#include "PhysicsAssetUtils.h"
#include "PhysicsEngine/PhysicsAsset.h"
#include "PhysicsEngine/BodySetup.h"
#include "PhysicsEngine/SkeletalBodySetup.h"
#include "Engine/SkeletalMesh.h"
#include "UObject/Package.h"
#endif
UPhysicsAsset* UGigLineVehicleBuilder::BuildPhysicsAsset(USkeletalMesh* Mesh,
const FString& PackagePath,
const FString& AssetName)
{
#if WITH_EDITOR
if (!Mesh)
{
UE_LOG(LogTemp, Warning, TEXT("GigLine vehicle: no skeletal mesh given"));
return nullptr;
}
const FString FullName = PackagePath / AssetName;
UPackage* Package = CreatePackage(*FullName);
if (!Package)
{
UE_LOG(LogTemp, Warning, TEXT("GigLine vehicle: could not create package %s"), *FullName);
return nullptr;
}
UPhysicsAsset* PhysicsAsset = NewObject<UPhysicsAsset>(Package, *AssetName,
RF_Public | RF_Standalone | RF_Transactional);
// ONE body on the chassis bone, which is what Unreal's own vehicle template uses.
// The first attempt let FPhysicsAssetUtils build a body per bone and the car settled
// 3.7 m above the road: the four wheel bodies were intersecting the chassis body and
// pushing the whole vehicle up. Chaos drives the wheels itself, so they must not be
// separate rigid bodies here.
const FBoxSphereBounds Bounds = Mesh->GetBounds();
const FVector Extent = Bounds.BoxExtent;
const FName ChassisBone(TEXT("root"));
USkeletalBodySetup* Body = NewObject<USkeletalBodySetup>(PhysicsAsset, ChassisBone,
RF_Public | RF_Transactional);
Body->BoneName = ChassisBone;
// A box slightly under the mesh bounds: a body that is exactly the bounds sits on the
// ground plane and reads as floating because the mesh has wheels below the sills.
FKBoxElem Box;
Box.X = Extent.X * 1.9f;
Box.Y = Extent.Y * 1.8f;
Box.Z = Extent.Z * 0.55f;
Box.Center = FVector(0.f, 0.f, -Extent.Z * 0.25f);
Body->AggGeom.BoxElems.Add(Box);
PhysicsAsset->SkeletalBodySetups.Add(Body);
UE_LOG(LogTemp, Warning,
TEXT("GigLine vehicle: chassis body %.0f x %.0f x %.0f cm on bone '%s'"),
Box.X, Box.Y, Box.Z, *ChassisBone.ToString());
PhysicsAsset->MarkPackageDirty();
UE_LOG(LogTemp, Warning, TEXT("GigLine vehicle: physics asset %s with %d bodies"),
*FullName, PhysicsAsset->SkeletalBodySetups.Num());
return PhysicsAsset;
#else
UE_LOG(LogTemp, Warning,
TEXT("GigLine vehicle: physics asset generation is editor-only"));
return nullptr;
#endif
}
@@ -0,0 +1,34 @@
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "GigLineVehicleBuilder.generated.h"
class UPhysicsAsset;
class USkeletalMesh;
/**
* Script-callable helpers for vehicle setup.
*
* Chaos Vehicles need a PhysicsAsset, and Unreal's `PhysicsAssetFactory` exposes no mesh
* property to Python, so the asset cannot be generated from a commandlet. The body does
* the job with `FPhysicsAssetUtils::CreateFromSkeletalMesh`, which is the same code path
* the editor's "Create Physics Asset" action uses.
*
* Editor only: it is compiled into the editor target and never shipped.
*/
UCLASS()
class GIGLINE_API UGigLineVehicleBuilder : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* Build (or rebuild) a physics asset for `Mesh` at `PackagePath`/`AssetName`.
*
* @return the created asset, or nullptr with the reason written to the log.
*/
UFUNCTION(BlueprintCallable, Category = "GigLine|Vehicle")
static UPhysicsAsset* BuildPhysicsAsset(USkeletalMesh* Mesh, const FString& PackagePath,
const FString& AssetName);
};
+42
View File
@@ -0,0 +1,42 @@
# Blender vs in-game
Generated by `Tools/build_comparison_md.py` — do not edit by hand.
- In-game captures: `Screenshots/demo/` (9 files)
- Blender renders: `Screenshots/blender/` (22 files), made by `Tools/blender_screenshots.py`
Left is what the engine draws, right is what Blender has. A gap between them is the import problem, not a taste problem: scale, material colour, missing parts and split systems all show up here before they show up in a review.
## Side by side
| In-game | Blender | Shot |
| --- | --- | --- |
| <img src="demo/review-demo-crossing.png" width="520"> | — | `review-demo-crossing.png` |
| <img src="demo/review-demo-north-buildings.png" width="520"> | — | `review-demo-north-buildings.png` |
| <img src="demo/review-demo-retail-frontage.png" width="520"> | — | `review-demo-retail-frontage.png` |
| <img src="demo/review-demo-spawn-view.png" width="520"> | — | `review-demo-spawn-view.png` |
| <img src="demo/review-demo-street-level.png" width="520"> | — | `review-demo-street-level.png` |
| <img src="demo/review-demo-third-person-front.png" width="520"> | <img src="blender/character-angle-035.png" width="520"> | `review-demo-third-person-front.png` |
| <img src="demo/review-demo-third-person-walk.png" width="520"> | <img src="blender/character-angle-035.png" width="520"> | `review-demo-third-person-walk.png` |
| <img src="demo/review-demo-third-person.png" width="520"> | <img src="blender/character-angle-035.png" width="520"> | `review-demo-third-person.png` |
| <img src="demo/review-demo-vehicle.png" width="520"> | <img src="blender/vehicle-angle-035.png" width="520"> | `review-demo-vehicle.png` |
4 of 9 in-game captures have a Blender counterpart; the rest are street and building shots whose meshes come from the Geo pipeline rather than a .blend.
## Signoff
One row per in-game screen. `—` in Blender means there is nothing to compare against.
| # | In-game screen | Blender counterpart | Verdict | Notes |
| --- | --- | --- | --- | --- |
| 1 | `review-demo-crossing.png` | — | ☐ | |
| 2 | `review-demo-north-buildings.png` | — | ☐ | |
| 3 | `review-demo-retail-frontage.png` | — | ☐ | |
| 4 | `review-demo-spawn-view.png` | — | ☐ | |
| 5 | `review-demo-street-level.png` | — | ☐ | |
| 6 | `review-demo-third-person-front.png` | `character-angle-035.png` | ☐ | |
| 7 | `review-demo-third-person-walk.png` | `character-angle-035.png` | ☐ | |
| 8 | `review-demo-third-person.png` | `character-angle-035.png` | ☐ | |
| 9 | `review-demo-vehicle.png` | `vehicle-angle-035.png` | ☐ | |
Verdicts: ☐ pending · ☑ approved · ☒ rejected. The authoritative per-shot review stays in [README.md](README.md); this page exists to compare Blender against the engine.
+1
View File
@@ -26,6 +26,7 @@ Every shot is embedded below, in order, so the whole review happens in this file
| 6 | [Third-person body](#6-third-person-body) | ☑ 2026-11-09 | ☑ 2026-11-09 |
| 7 | [Third-person front](#7-third-person-front) | ☑ 2026-11-09 | ☑ 2026-11-09 |
| 8 | [Third-person walk](#8-third-person-walk) | ☑ 2026-11-09 | ☑ 2026-11-09 |
| 9 | [Vehicle](#9-vehicle) | ☐ FAIL 2026-11-09 | ☐ pending |
**Verified** is what `--verify` reports: the frame is not blank, is the right size, and has tonal range. **Approved** is the owner's verdict, recorded here. Each shot also has an **expectation** in [`Tools/visual_targets.json`](../Tools/visual_targets.json) describing the content it must show.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loaded 100 of 121 files, more files were not shown because too many files have changed in this diff. Show more