Commit Graph
106 Commits
Author SHA1 Message Date
zwitschi 61dd28bc89 feat(hero): one-command iteration loop, first judged pass, comparison plates
Tools/hero_iterate.sh runs the whole chain - chmod the Blender files, build, import, render, judge, plate -
and fails loudly at each step with its log under /tmp/gigline. Tools/hero_compare.py builds the side-by-side
plate (reference | rendered, one height, labelled); two are committed for review.

This pass: the stone is warmer in Blender (0.66/0.60/0.48 against the near-white blockout) and the banners are
1.35 x 6.2 m hung from 7.2 m, matching the reference proportion instead of the 1.55 x 5.4 that sat too low and
too fat.

Iteration 1 judge verdict, confidence 0.9: lacks warm sandstone, lacks fluted column detail, banners wrong,
no doorways, no inscription or medallions. Checked against the plate, two of those are wrong (120 flute
strips exist and cast shadows; three 1.2 m deep doorways with steps exist and read black) and three are real.
The split is recorded in TODO.md with the next action for each - the important one being that the stone colour
must change in the UE material (ue_hero_facade_import.py), not in the Blender material, which the render does
not use.
2026-11-10 08:28:06 +01:00
zwitschi 03ef4dc945 feat(hero): first pass from the masonic-temple reference folder
Art/reference/hero-masonic/ (15 images, git-ignored) replaces the single Wikimedia photo as the source of
truth. Two front elevations drive this pass: 02-masonictemple01.jpg, a clean street-level frontal, and
06-el-capitan-theater-frontal-fachade.jpg; 05-hollywoodmasonictemple-1922.jpg records the original masonry.

Changes:

- the blank Hero_Stylobate_LintelBand across the portals is removed - the reference has open doorways with
  steps, and they now read as three dark openings with banners above them;
- rustication(): five ashlar courses from the pavement to the portal heads, 4 cm joints proud of the wall;
- balustrade(): bottom rail, top rail and 38 balusters along the roof edge, where the reference shows a
  cresting and railing.

Still missing against the same references, and recorded: warm sandstone instead of flat pale stone, lattice
grilles in the window bays, the carved inscription, and medallions that do not read at distance.

The blend is read-only after a checkout - chmod u+w first or the save fails silently and the FBX is never
written, which happened once here.

Rows 4-6 re-rendered and pending review.
2026-11-10 08:08:01 +01:00
zwitschi 01e49ff889 docs(hero): the detail is modelled inside the wall - that is why the facade renders plain
Measured the Y positions in the source and the imported mesh:

  blend scene Y        -12.50 .. +0.13
  Hero_Mass            y = -BODY_D/2 - 0.5, so it spans -12.50 .. -0.50
  Hero_Banner_1        -1.08 .. -1.02, i.e. 55 cm BEHIND the wall's front face
  imported bounds      y -13 .. +1250 cm

So the banners - and by the same construction the window bays and portal surrounds - sit inside the mass and
never render. The columns, cornice and dentils protrude, which is exactly what the captures show: a plain
block with six columns.

The camera check is answered as well: the three hero cameras are aimed at (-13336, 1307) where the hero actor
is, and the frontage sits at the origin, so the framing was correct all along.

Next step is mechanical, not artistic: put a FRONT_Y constant in Tools/hero_facade_build.py, place every
elevation detail relative to it, re-export and re-render. The model does not need remodelling before its
detail can be seen.
2026-11-10 07:57:00 +01:00
zwitschi e19cc361ad docs(hero): reference-vs-model gap assessment, and a camera suspicion to check first
Started the hero facade task the way it has to start - by comparing the reference photograph with the
current render rather than with memory of it.

Reference: el-capitan-facade-day-wikimedia-2009-10-27.jpg, north elevation, daylight.

The model (SM_HeroFacade, 4 materials) is much further along than the review captures suggest: six fluted
columns with capitals and abaci, architrave, entablature, cornice with 52 dentils, parapet, motto band with
surround and two medallions, three banners, five window pairs, and three portals with jambs, lintels,
mullions and five steps each.

What it genuinely lacks: surface. Rusticated banding, carved lettering on the motto band, window grilles,
and a balustrade with finials on the parapet.

And one thing to check before any modelling: the three hero captures show a plain block with six columns and
nothing else, while the mesh carries banners, windows and portals and is staged at (-13336, 1307, 0). The
likely explanation is that the cameras frame Frontage_SM_ElCapitanTheatre at the origin, or the wrong side
of the hero - in which case the rejection is describing a camera error. One render aimed at Hero_Facade from
Y -4000 decides it, and three new tasks are recorded with that first.
2026-11-10 07:55:14 +01:00
zwitschi fc1e832664 docs(todo): remove the vehicle blocks that leaked into the hero facade section
A shared heading string made one edit land in two sections, so the hero-facade task carried a copy of the
vehicle's bisect evidence. The section is back to its own two steps.

Also re-ran the gates on the tree as it stands after today's ground and placement work:

  Tools/ue_audit_scene.py  593 mesh actors, every mesh has a material, building X ranges as expected,
                           no duplicate actor labels, 0 failures
  prettier --check         all Markdown clean
  unittest                 99 tests pass
2026-11-09 22:25:49 +01:00
zwitschi fe219b038c test(vehicle): collision is not the cause either - defer the rig to its own session
Third fix tried and failed: the terrain staged in the probe map with collision disabled entirely. The pawn
still rises (Z 315 -> 421), exactly as with collision enabled.

With that, the terrain's convex hull, its collision channel and its collision itself are all eliminated, and
the failure survives in a map whose only difference is the presence of a 279 m static mesh.

The player is unaffected throughout - re-checked after every attempt, still standing on Sidewalk_North_07
at 157 cm - so nothing in the walkable demo regressed.

Deferred rather than guessed, per AGENTS.md: the task is marked deferred with the three failed fixes, the
probe workflow and switches a fresh session inherits, and the next question framed precisely - why a map
containing a collisionless 279 m static mesh changes a physics body's behaviour, which points at streaming,
memory layout or the broadphase rather than at collision authoring.

Generated probe levels deleted; the tools stay.
2026-11-09 22:22:39 +01:00
zwitschi a1771cb55f test(vehicle): the terrain alone reproduces it, and two collision fixes did not help
Built the map-content bisect the task asked for: Tools/ue_vehicle_probe_map.py duplicates ReviewMap,
optionally adds one demo mesh and the pawn, and saves it as VehicleProbe.

  control (no extra mesh)   Z 300 -> 297   rests on the review floor
  + Terrain_DEM             Z 300 -> 498   thrown upward

So Terrain_DEM alone reproduces the fault in a map that works. Two fixes were tried:

- Tools/ue_clear_simple_collision.py cleared the auto-generated convex hulls from all 76 site/street
  meshes - the terrain had ONE convex element over 279 m - and set them to complex-as-simple. Still rises.
- The terrain component set to QueryOnly, so traces hit it and physics ignores it. Still rises.

Neither changed the behaviour, so the ejection is neither the convex hull nor the collision channel. The
next probe is the complex triangles themselves: give Terrain_DEM no collision at all in the probe map and
see whether the pawn falls. If it falls, the triangle winding is pushing bodies out of the ground mesh -
the same inverted-winding family that made the sidewalk unstandable earlier.

Both tools have README rows; the pawn is parked again.
2026-11-09 22:20:28 +01:00
zwitschi 387a4530de test(vehicle): clamping the velocity to zero does not stop the rise - the transform is written
The decisive clamp: zero linear and angular velocity every frame for two seconds.

  t=0.25 Z=3220 vZ=0 | t=0.75 Z=3683 vZ=0 | t=1.25 Z=4197 vZ=0

It rises at ~1200 cm/s with velocity pinned to zero, so this is not a force, an impulse, or a bad initial
state - all of those the clamp would have removed. Something calls a transform setter on the vehicle every
frame.

Our own source contains exactly one direct transform write outside the character (the shot actor's camera),
so the writer is engine-side: the Chaos vehicle component driving UpdatedComponent from a vehicle state that
was never created, or a skeletal-mesh transform update.

Also diffed the two maps, since the pawn works in ReviewMap and not in DemoMap: kill_z, gravity, lights and
partitioning are identical; DemoMap adds 593 static mesh actors, a height fog and a post-process volume,
ReviewMap adds a shot actor and the character. Nothing in either list can write a transform, so the map
difference is not in its actors or settings.
2026-11-09 22:11:13 +01:00
zwitschi 331e2016be test(vehicle): the Chaos rig is fine - DemoMap is what breaks it
Ran the bisect the task asked for. The same pawn, unchanged, in two maps:

  ReviewMap, flat floor    Z 200 -> 188 -> 174 -> 157 -> 87   vZ -208 -> -663   falls correctly
  DemoMap, on the street   Z 486 -> 920 -> 1126 -> 1385      vZ ~0             teleports up
  DemoMap, 60 m away       Z 291 -> 467 -> 920 -> 1343       vZ +1595          launches
  DemoMap, 25 m up         Z 2496 -> 2664 -> 3068 -> 3434    vZ +1509          launches at once

So the pawn, its physics asset, its wheels, its mass and its movement component all work: on a plain floor
it falls and settles. Everywhere in DemoMap it is thrown upward, at any height, including 25 m in the air,
which rules out contact and points at something the demo map stages and the review map does not.

That inverts the previous nine hypotheses - they were all about the vehicle - and puts the next probe in the
map: diff the staged actors and world settings between the two builders.

Pawn parked; the bisect numbers are in the task.
2026-11-09 22:07:38 +01:00
zwitschi 358673bcfc test(vehicle): the body is genuinely dynamic, so the teleport is applied to a normal actor
The dynamics probe rules out the last structural explanation:

  simulate=1  instance=yes  profile=PhysicsActor

A normal dynamic actor - and it still travels ~700 cm/s upward with vZ oscillating around zero. So the
teleport is applied to a genuinely dynamic body, which is why not one of the force-shaped settings
(gravity, mass, penetration, spawn height) changed anything.

Recorded with it: the bisect plan for the next pass, because guessing at Chaos internals has stopped being
the cheap option. Spawn the pawn in ReviewMap's empty floor to separate asset from world, and spawn a stock
AWheeledVehiclePawn beside it to separate our setup from the engine's.

Pawn parked; captures unchanged.
2026-11-09 21:58:37 +01:00
zwitschi 0f059a5c93 test(vehicle): bones match, terrain has no lid, and stopping the mover does not stop the climb
Three more measured facts:

  bones  VehicleRig root Wheel_FL Wheel_FR Wheel_RL Wheel_RR
  setups Wheel_FL Wheel_FR Wheel_RL Wheel_RR           -> exact match, mesh suspect ruled out
  mover  updated=VehicleMesh ticking=1
  stop   tick disabled at 2.5 s with the actor at Z 2001; at 3.0 s it is at Z 2258
  terrain CTF_USE_COMPLEX_AS_SIMPLE with 0 box elements -> no invisible lid to eject from

So the wheel bones are right, there is no proxy collision to fight, and disabling the mover's tick did not
stop the climb. That moves the suspect list to the pawn's own Tick/root: something writes the actor
transform every frame regardless of the physics body, and the 1 cm bounds are its fingerprint.

Pawn parked again; the diagnostic stays.
2026-11-09 21:41:36 +01:00
zwitschi 6d025491ef docs(vehicle): evidence, ruled-out list and three ordered suspects
The previous edit to this section never applied - a substring that no longer existed - so the task
still described only the first launch observation. It now records everything measured this session:

  box 452x205x40 centre 0,0,-18 | bounds extent 1,1,1
  velocity -24,-182,-28 cm/s | mass 1500 | sim=1 | setups=4 wheels=0

The conclusion is that the body is nearly still while the actor is nine metres up, so this is a
transform problem rather than a force one, and the suspects are ordered: the component hierarchy, the
Chaos movement component's transform updates, and then the skeletal mesh's bone layout - which would
explain CreateVehicle() bailing before it ever builds wheels.

Ruled out with a run each: spawn penetration at four heights, panel collision, chassis box dimensions,
world gravity, and mass.
2026-11-09 21:34:36 +01:00
zwitschi 4498274328 test(vehicle): park the Chaos pawn and give the physics its own task
The pawn was found again after a cleanup removed its staging, and the diagnostic answers the
zero-wheel question with a bigger one: with the pawn present the actor is launched to Z 1993 cm - 18 m
above the ground it stands at 150 - and still reports 0 wheels.

  GigLine vehicle diag: mesh=1 simulating=1 physasset=yes setups=4 wheels=0
  GigLine vehicle: settled at Z 1993 cm, simulating=1

Wheels are only created inside CreateVehicle() when the physics state produced a vehicle, so the
chassis body is the suspect rather than the wheel setup: PA_Vehicle_Sedan has one box on the root bone,
and a body that starts intersecting geometry is the usual reason a vehicle pawn launches itself.

Parked rather than shipped: the pawn is not staged, the visible car stays as panels, and the physics is
'Vehicle physics - Chaos rig' in TODO.md with the evidence and three concrete steps. The demo no longer
carries an 18 m flying car in its captures.
2026-11-09 20:50:52 +01:00
zwitschi f65c6e8d45 docs(review): approve 11 and 12, split the hero facade into its own task
Owner verdicts: rows 11 (shadow compare) and 12 (vehicle staging) approved. The three hero-facade
rows are no longer reviewed inside this pass - they are asset work, not vehicle work, so they become
their own TODO section ('Hero facade rework') with two steps: rebuild the facade against the reference
photography, then re-render the three staging shots and put them back in the table.

The table rows point at that task instead of repeating 'rework needed'.

Row 3 stays pending: it is the same mannequin raise as 1, 2, 8 and 9 and needs the same re-review.
2026-11-09 20:12:45 +01:00
zwitschi 4132dc77e3 docs(site): the road fans past the sidewalk where the centreline curves
Found the mechanism behind the 2 m disagreement. street_mesh.py builds the road across -10.5..+10.5 m
and the north sidewalk +10.5..+15.8 m, so along the straight the two bands meet exactly at the gutter.
The strips are generated along a curved centreline, and the fan-out at the bend pushes the road's outer
edge to -13.8 m while the sidewalk occupies -17.3..-11.8 m - a 2 m overlap where the road surface is
366 cm and the sidewalk 158 cm.

That is the surface a downward trace at the player's Y kept finding, and the reason the scene read as
the road hovering above the pavement.

The fix is a clamp in strip() so a row's outer offset cannot pass the nominal gutter, or trimming the
road's ends - one function, then re-export, re-import and the 14 s probe confirms it. Recorded in
MEMORY.md and TODO.md. The demo reads correctly meanwhile because both objects stand on the sidewalk.
2026-11-09 19:10:34 +01:00
zwitschi 16eaf5a07d docs(todo): the road and sidewalk disagree by 2 m, and where to look next
Records the numbers rather than another attempt: at the player's X the sidewalk slice tops at 169 cm
and Road_Hollywood at 366 cm, in the same map, so the player lands on the road.

The code in Art/blender/street/street_mesh.py builds both surfaces from one sampler - road as
dem_z + 0.02, sidewalk as dem_z + kerb * t - so a 2 m gap cannot come from the assembly logic. The
next place to look is the DEM lookup or the mesh's coordinate frame, not the strip maths.

Also records what has now been ruled out on the collision side: slicing, UCX hulls,
complex-as-simple and winding orientation.
2026-11-09 18:20:50 +01:00
zwitschi 76859b64cf docs(placement): the pavement is at 158 cm, not 380 - and my attempt to use it broke the floor
Measured in the source geometry (Art/blender/site/site-2026-11-06.blend, metres): at the
player's street section the north sidewalk tops out at 1.58 m and the road at 1.45 m. The 380 cm
that has driven every placement decision came from the UE mesh's bounding box, which spans 279 m
of sloping street and takes the highest point anywhere - 2.2 m above the surface where the player
stands. That is the float, quantified at last.

Moving the invisible collision slab to a 158 cm top did not work: with an 8 cm slab the player
fell to Z -4410, and with a 50 cm slab it fell to the same place, so the slab was not colliding
at all after the change. I could not identify why within this pass, so the change is reverted and
the 380 stopgap restored - verified again by trace (mesh=Cube at Z 380, actor Z 470).

Recorded in TODO.md with the two numbers and the next step: find why the slab stops colliding when
it moves down, then set it to 158 and the player start to 247.
2026-11-09 17:57:54 +01:00
zwitschi c94ec7bbb9 test(placement): the player stands on Road_Hollywood, not on the sidewalk
Three spawn heights - 455, 395, 469 - produced identical captures, and the in-game probe names
the surface every time: mesh=Road_Hollywood at Z 366. The north sidewalk exists, carries
collision and its top is Z 380, so the character stands 14 cm BELOW the pavement and reads as
floating against the kerb; its shadow falls on the road because the road is genuinely lower.

That is why lowering the spawn changed nothing: the capsule falls through to the road whatever
height it starts at. The question left is the player start's XY footprint, not its Z - the
sidewalk is right there but the capsule never lands on it.

Player start set to 469 (sidewalk top 380 + capsule half 88 + 1) so that, once the footprint is
fixed, the height is already right.
2026-11-09 17:18:23 +01:00
zwitschi ca73563060 test(review): ground-level cameras, and a null result that locates the float
Two cameras were added at pavement height - one on the feet, one on the wheels - because the
wide shots cannot settle whether anything touches the surface.

The feet shot settled it, and not the way I expected. Lowering the player start from 455 to 395
changed NOTHING in the capture: same framing, same float. The capsule falls onto the collider and
always rests at Z 456 with its feet at 369, while the rendered pavement is about a metre lower.

So the float is not a spawn-height problem, and no amount of tuning player_z will fix it: the
sidewalk's collision extends above its visible top, and the player stands on an invisible ledge.
The earlier 'top surface under player: Z 366' measurement was not wrong - it was measuring the
collider, which is the thing that is in the wrong place.

Next: measure the sidewalk mesh's rendered top against its collider and fix the asset, not the
spawn.

Also pulled the wheel camera back; it had been inside the bodywork.
2026-11-09 17:14:26 +01:00
zwitschi f1d6283384 test(visual): a shadow-gap measurement, and the spawn height cleared
Tools/shadow_gap.py measures the vertical pixel gap between an object's lowest pixels and the
top of its shadow, as a fraction of the object's height, and calls it contact or floating. It
classifies a shadow by per-channel scaling - a shadow darkens every channel by the same
proportion, an object does not - which separates the two where an absolute delta cannot. Four
unit tests cover contact, floating, an empty box and the border median.

On the real spawn view it returns 'unknown': the crop contains two surfaces (pale wall behind,
dark road below) and one global background cannot classify both. Recorded as the next step, not
presented as working.

The spawn height is measured and cleared: tracing down from 30 m, the topmost surface under the
player is Z 366 and the capsule's feet are at 369 - 3 cm, the capsule skin. If the character
still reads as floating, the gap is between the rendered pavement and its collision surface, not
the spawn Z, and that is what to measure next.
2026-11-09 17:07:23 +01:00
zwitschi dde451fd69 test(review): floating is a judging criterion now, and the judge applies it
The spawn-view expectation states that nothing may float and that a shadow more than a hand's
width below an object means the object is floating. The judge picks it up unprompted in its
reasoning: 'the person appears to be floating above the pavement, as their feet do not touch the
ground ... shadows do not align correctly with the objects'.

That is the criterion working. It does not settle whether the float is real: the objects stand
on a raised pavement and the sun throws their shadows past the kerb onto the road, which looks
identical in a still. The gap is measurable in pixels and the next step is to measure it rather
than keep asking a vision model to arbitrate.

Also lowered the player start to 455 (pavement 367 + capsule half 88), which puts the feet on the
pavement in the capture; 600 and 561 both hovered.
2026-11-09 17:02:30 +01:00
zwitschi 1da209a902 fix(placement): both heights now come from a measurement after settling
The probe was armed from BeginPlay but the delay was lost when I removed the dead block, so
it never ran. Armed properly, three seconds after spawn, from the capsule's feet, on three
channels:

  GigLine foot Visibility after settling: actor Z 456, feet 369, hit Z 366

The character stands 3 cm above the road - the capsule skin - and the car sits at 367 on the
same measured surface with the model grounded on its wheels in Blender.

Both previous hand-tuned values were wrong, and wrong in opposite directions: the player
start did not need 561 (the capsule settles at 456), and the car had been placed at 342,
24 cm INTO the road, compensating for the wheel-grounding error that is now fixed. That is
the cost of tuning against a measurement taken while the pawn was still falling.
2026-11-09 16:44:51 +01:00
zwitschi f4c1fed99b test(placement): measure the foot contact after the pawn settles, not at spawn
The earlier 'ground 366 against an actor at 561' reading was taken in BeginPlay, while the
pawn is still at its spawn height and falling - so it described the surface far below a
falling capsule, not a float. That number drove two hand-tuned Z values (561 for the player
start, 342 for the car) on the strength of a measurement taken at the wrong moment.

The probe now runs three seconds after spawn, traces from the capsule's feet rather than the
actor origin, and reports three channels with the actor and component each hits. A
multi-channel probe at spawn showed every channel hitting the same StaticMeshActor at 366,
which is consistent with the falling-pawn explanation rather than with missing sidewalk
collision.

Not yet read back: the last two game runs produced no log output at all, so the settled
numbers are unverified and recorded as such.
2026-11-09 16:40:54 +01:00
zwitschi 15cc6cbdd0 fix(vehicle): the glazing reads as glass
The windows material was metallic 1.0 with roughness 0.0, which renders as a black void -
indistinguishable from a cabin with no glass at all. It is now tinted translucent: opacity
0.85, roughness 0.08, dark blue-grey base. The windscreen and side glass read as glass.

Opacity 0.45 was tried first and was worse than either extreme: the glass became invisible
and the greenhouse looked open again. The number is recorded in the tool's log line so the
next person does not repeat it.

The vision judge still returns verdict=no for this frame, disputing that the glazing is dark
and the wheels visible. The image shows both. It is an advisory helper - AGENTS.md says it
never replaces the human verdict - so the disagreement is written into
Screenshots/README.md rather than being resolved by iterating against the model.
2026-11-09 16:21:41 +01:00
zwitschi bb12ce8c33 fix(vehicle): the car is red in the engine, not black
Root cause was in the template, not the import path: 'car paint' is a legacy mix-shader
graph. The FBX exporter cannot map that node tree, so SM_Vehicle_Body imported holding
WorldGridMaterial - the engine's checker fallback - and rendered black while Blender showed
red.

Two changes:
  - Tools/vehicle_from_template.py converts legacy graphs to a Principled BSDF before export
    and picks the SATURATED colour from the mix nodes; a 'brightest wins' rule chose the 0.8
    grey diffuse beside the 0.49 red and produced a grey car.
  - Tools/ue_vehicle_materials.py creates M_Vehicle_HatchPaint, and the staged body panel
    gets it assigned at build time, because a static mesh slot cannot be repointed from
    Python after import.

Judge on the new capture: verdict=yes confidence=0.9 - 'a red hatch car with a red body and
dark wheels parked on a road'. The comparison table in Screenshots/README.md now shows both
columns agreeing on colour.

Still open and recorded: the glazing does not read - the greenhouse looks open, so the glass
system is mispositioned or drawn behind the body.
2026-11-09 16:13:35 +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 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 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 6c4e0701fb release: v0.2.2 — the character pass
Writes the 0.2.2 entry and prunes the section from TODO.md. The entry records that this
ships as a patch by the owner's decision even though it adds asset categories, which the
policy otherwise puts in a minor release - so the numbering is not mistaken for scope.

Contents: clothing as three separate skinned meshes, hair cards oriented from the scalp
normal, ComfyUI character blueprints and the render-vs-blueprint comparison, procedural
textures, materials built from scratch, the raised sun, and the walk cycle starting at
maximum stride.

Verified: 8/8 shots, all approved; mean deltaE 31.9 -> 21.8 against the blueprint; 95
tool tests. Known limitations listed rather than omitted - garments are shells over body
faces, the baked diffuse cannot be used because the mesh UVs do not map to it, and the
character has no face detail.
2026-11-09 14:26:13 +01:00
zwitschi 5de4183456 docs(review): shots 6, 7 and 8 reflect the clothed, haired character
The appearance changed in 0.2.2 and the README still described an unclothed body with
splayed fingers, which is a defect in its own right: a shot whose README section is
stale is not a review.

Shots 6, 7 and 8 now describe the clothing, the dark card hair and the raised sun, and
their approvals are reset to pending because the appearance changed. 8/8 shots verify.
2026-11-09 14:23:47 +01:00
zwitschi bea6aa3e05 light: raise the sun so the character leaves building shadow (needs approval)
The skin was never grey. A red test colour reached the render and the slot was confirmed
holding M_Character_Skin, so the material and the assignment were both fine - the body
was simply standing in building shadow and lit almost entirely by the blue skylight
(intensity 2.6). Warm skin under blue fill reads blue-grey.

Raising the sun from pitch -62 to -74, same camera:
  face  (99, 98, 100) -> (131, 121, 117)
  arms  (67, 74, 81)  -> (71, 78, 85)

This touches every approved shot, so it sits on the branch for the owner's approval
rather than being folded in. Also records that the Unreal commandlet silently drops
unknown switches, which is why a --unlit-test flag never took effect.
2026-11-09 14:20:07 +01:00
zwitschi 7c57bf318b fix(hair): dark constant colour, strand sheet used only as an opacity mask
The hair read blond and thin because the strand sheet's RGB was driving base colour; the
sheet is a pale highlight map. Colour is now a dark constant (0.030/0.020/0.014) and the
texture supplies only alpha. Sampled in the render: hair (31, 26, 21).

Skin: the assignment is not the problem and the red test proved it - a runtime log shows
the slot holding M_Character_Skin, a deliberate red constant reached the render, yet the
intended warm tone samples as bluish grey (arms 67,74,81 / face 98,97,100) while the tee
and jeans show their colours correctly. Recorded in TODO.md with the next suspects rather
than guessed at.

The temporary material log is removed again.
2026-11-09 13:55:19 +01:00
zwitschi 878d5a1f4e fix(clothing): close the coverage holes, and bind materials without crashing
Solidify thickness went from 1 cm to 2 cm and the shell is fattened 8 mm along its own
normals afterwards. That closed the shoulders, hips and knees where the body was
showing through the cloth.

Evaluation against the blueprint, same shot and box before and after:
  mean deltaE     31.9 -> 21.8
  palette deltaE  25.4 -> 19.7
  aspect delta     1.24 -> 1.22

Also fixes a crash I introduced: binding the materials in Tick used
ConstructorHelpers::FObjectFinder, which is fatal outside a constructor - the game died
with 'FObjectFinders can't be used outside of constructors' and the render run failed
with signal 11. The binding now lives in BeginPlay and uses LoadObject.

Not fixed, and recorded: the arms and face still render neutral grey, so
M_Character_Skin itself is suspect rather than the assignment.
2026-11-09 13:50:21 +01:00
zwitschi 86e9c5b693 feat(clothing): tee, jeans and shoes as separate skinned meshes
Follows the character-artist clothing workflow literally: duplicate the body's own
faces for the garment base, solidify for cloth thickness, and keep each piece a
separate mesh. Attached as leader-pose components beside the hair, so no second rig.

- Tools/character_clothing_build.py builds three garments from the body mesh, trimmed
  geometrically (height band plus a width limit) instead of by dominant bone: the bone
  filter left holes wherever a face's strongest bone was outside the allowed list.
- Tools/ue_character_clothing_import.py imports them as CharacterTee / CharacterJeans /
  CharacterShoes, each arriving with its own single material slot.
- The pawn attaches all three, hidden with the body in first person.

Three traps recorded in MEMORY.md, each of which cost a build: MPFB weights its helper
geometry to groups that dominate every vertex, Blender refuses to apply a modifier to a
mesh with shape keys, and object-mode polygon selection is discarded on entering edit
mode - the delete wiped the whole garment.

Known defects, in TODO.md: the garments have coverage holes at shoulders, hips and
knees, and the hair reads light and wispy since its material was repointed to the
masked strand sheet.
2026-11-09 13:41:39 +01:00
zwitschi 85a8437cbc feat(character): ComfyUI blueprints and a render-vs-blueprint evaluation
Two tools that turn character work into something measurable.

- Tools/comfyui_character_blueprint.py generates a four-view sheet (front, side, back,
  head) plus a contact sheet from one description, with fixed seeds and a JSON record
  of prompts and prompt ids. First sheet: 'GigLine walker', lean pedestrian in a navy
  tee, jeans and sneakers.
- Tools/character_match.py compares a rendered character against a sheet: figure
  aspect, fill and coverage deltas, mean CIE76 colour distance, dominant-palette
  distance, and a side-by-side montage. The deltas stay separate on purpose - a single
  similarity score between a photoreal sheet and a low-detail body would be a number
  with no meaning.
- comfyui_generate.py grew wait_for_image() so both tools share the polling path
  instead of duplicating it.
- 13 unit tests for the colour maths, masks and palette; 95 tool tests pass.

First real result, recorded rather than smoothed over: aspect delta 1.24, fill delta
0.12, mean deltaE 31.9, palette deltaE 25.4. The montage shows the reason - the model
is a nude body and the blueprint wears a tee and jeans, so clothing dominates every
metric. Figure segmentation is explicit (--render-box): auto-detection picked backdrop
blobs on every attempt, and a background plate is the proper fix.
2026-11-09 13:36:28 +01:00
zwitschi 1fc5835327 fix(hair): cards follow the scalp normal, three overlapping rows
The first card build anchored every card on a fixed outward vector, so the silhouette
was a spiked crown. Each card is now oriented from the scalp normal at its anchor: the
length runs along the downward tangent, the width across it, and a small tip lift gives
the cut volume. Three overlapping rows (7/9/10) replace the single ring; overlap is
what stops cards reading as separate spikes.

26 cards, 401 verts, 224 quads, 468 tris, still all quads on one head bone.

Recorded as still open: the skin renders darker than the baked diffuse suggests, and
the generated pore and strand textures remain unwired because the material-editing
tool segfaults in the headless commandlet.
2026-11-09 13:25:40 +01:00
zwitschi 39f07b0f5f feat(hair): hair cards as their own skinned mesh, attached as a second component
Follows the character-artist skill on two points it is explicit about: hair for a
game character is a card system, and swappable parts stay separate meshes.

- Tools/character_hair_build.py builds SM_Char_Hair: a scalp cap plus 18 cards in
  three rings, quads only, every vertex weighted 1.0 to the head bone, UV-mapped for
  the strand sheet from Tools/generate_character_textures.py. 321 verts / 404 tris.
- Tools/ue_character_hair_import.py imports it as its own skeletal mesh, which is the
  point: joined into the body, Unreal collapses the second material slot and the hair
  renders with the skin material.
- The pawn carries it as a second USkeletalMeshComponent on SetLeaderPoseComponent, so
  it follows the head bone with no second rig and hides with the body in first person.

Still wrong and recorded: the cards point outward, so the silhouette reads as a spiked
crown rather than a short cut. The next pass orients them tangentially along the
skull with a second overlapping row. Tools/ue_character_materials.py also segfaults in
the headless commandlet when editing a material graph, so the generated textures are
not wired yet and the hair keeps its Blender base colour.
2026-11-09 13:18:40 +01:00
zwitschi 54c579ab1a feat(tex): ComfyUI tool, procedural character textures, hair cap behind a flag
Tools/comfyui_generate.py drives the local ComfyUI instance for generated textures
and writes a sidecar with model, prompt, seed and workflow, so a generated asset can
be regenerated or rejected instead of being an unexplained PNG. Models list and
generation both verified against 127.0.0.1:8188.

Stable Diffusion is not usable for flat skin swatches, and that is worth recording:
three attempts produced an anatomical close-up twice and a leather-scale surface once.
Skin detail is generated procedurally instead - tileable octave noise for pores, plus
a strand sheet for hair - in Tools/generate_character_textures.py, with sidecars.

Hair geometry exists (cap conforming to the head, weighted to the head bone, second
material slot in Blender) but Unreal's import collapses the second material slot, so
the cap renders with the skin material and reads as a bald skull. It is behind
--hair and off; the next step is a separate skeletal mesh attached as a second
component.
2026-11-09 13:09:39 +01:00
zwitschi 4b9ad22108 fix(walk): the cycle starts at maximum stride, so the capture shows one
The stride shot regressed twice for two different reasons, and only the second was
the clip. The walk cycle was authored on a sine, which is zero at both the start and
the middle of the cycle - exactly the two frames where the legs pass together - so a
deterministic capture at any phase near those frames read as a still. The probe
proved it: frames 0 and 15 carried identical hip rotations while 7 and 22 were the
stride.

The cycle now starts at maximum stride (cosine). Length, amplitudes, keys and loop
are unchanged; only the phase moved. demo-third-person-walk is back as shot 8 and
shows a genuine stride with the curled hands from 0.2.1.

8/8 shots verify.
2026-11-09 12:55:53 +01:00
zwitschi e9f31f271e release: v0.2.1 — rig fixes changelog entry, TODO pruning
Writes the 0.2.1 entry (fingers, derived arm angle, base keys) and removes the
section from TODO.md. The stride-capture regression is listed under known
limitations rather than omitted: the shot is retired, not shipped.
2026-11-09 12:52:01 +01:00
zwitschi 03af8146a6 fix(rig): curl the fingers, derive the arm angle
Two defects from the 0.2.0 review, both fixed and both measured.

- Fingers splayed because the bind is an A-pose hand. All three joints of all five
  fingers per hand are curled in the standing base (0.28/0.34/0.22 rad), with the
  curl axis probed on the rig first. The front shot now shows relaxed hands.
- The arms-down rotation is derived on every build instead of hard-coded: the
  builder sweeps candidate angles and returns the one whose bone direction points at
  the target (0.60 rad, direction dot 1.000 this build). The old 0.55 was a
  hand-swept constant.
- Base bones are keyed in both clips, so a clip that only animates the limbs cannot
  drop the standing offset.

Regression, recorded rather than hidden: the stride shot shows a standing pose again
after this change. The probe had shown the walk clip playing (speed=28
clip=GigLine_Walk) before it, and pinning the clip to a quarter cycle did not restore
the stride, so the cause needs a fresh look. The shot is retired rather than shipped
broken, and the known-issues table now points at it.
2026-11-09 12:48:40 +01:00