116 Commits
Author SHA1 Message Date
zwitschi 0d7ceae474 docs(memory): record the vehicle settle fix and the failed root-bone experiment 2026-11-10 21:33:31 +01:00
zwitschi 319096d0d9 fix(vehicle): settle the chassis on the road (float 33 m -> 6 cm)
Two root causes, both in how the chassis box was authored:

1. The box centre was -Extent.Z*0.25, below the root bone, so the box sat under the
   ground and the suspension pushed the whole vehicle up. The skeletal mesh imports
   ROLLED (height along Y, width along Z) while the armature is unrolled, so the box is
   now built with swapped axes (length from mesh X, width from mesh Z, height from mesh
   Y) and centred at the rolled mesh's mid-height (Bounds.Origin.Y). The vehicle now
   settles at Z 6 cm instead of floating at 33 m.

2. A left-over diagnostic stopped the movement component at 2.5 s whenever the pawn
   ticked, which cut the drive test (throttle runs 1-4 s). It is now gated on the vehicle
   actually having zero wheels.

Remaining: the drive test still reports ~0 forward speed, so the throttle does not yet
move the car - the wheels rest ~2 cm above the ground because the suspension rest
position is measured against the root bone while the box sits at the centre of mass.
2026-11-10 21:33:14 +01:00
zwitschi 84291a5270 fix(vehicle): raise the chassis box above the ground (float 33m -> 7.8m)
Box.Center was -Extent.Z*0.25 (below the root bone); the box then sat under the ground
and the suspension pushed the chassis up. Raising it to +Extent.Z*0.35 cuts the float
from ~33 m to 7.8 m, confirming the direction. Still not settled - the box centre and/or
the root bone (grounded at Z 0 by normalise_scale) need to move to the car's centre of
mass.
2026-11-10 20:59:53 +01:00
zwitschi 63ba9d91ce feat(vehicle): build the skeletal mesh from the hot hatch
vehicle_from_template.py now authors the physics mesh from the real hot hatch: apply
the mirror/nodes modifiers, derive the 4 wheel centres, add a root + 4 wheel bones,
weight and export vehicle_skeletal.fbx. A rotation orients the car so it imports
unrolled (length +X, width +Y, height +Z); wheel bones land at front +X, rear -X,
left/right +/-Y. The vehicle still floats (rises to 33 m) - recorded in MEMORY.
2026-11-10 20:47:54 +01:00
zwitschi d8cbb0c95b docs(vehicle): the visual and physics bodies are different vehicles
SM_Vehicle_Body (hot hatch) is 150x400x122 (length along Y); SK_Vehicle_Sedan is
476x228x144 (length along X). They are built by different scripts and do not share
a forward axis - the likely real cause of the reverse drive. MaxTorque sign was
also a no-op. Next: reconcile the two meshes into one asset with one forward axis.
2026-11-10 19:58:51 +01:00
zwitschi dd36d399ad chore: add skills 2026-11-10 19:57:56 +01:00
zwitschi 911a7059ae docs(vehicle): revert the axis fix; the reverse is in the Chaos solver
The axis_forward:X / Blender-Z axle fix changed the reference pose but not the drive
(-2737 cm/s), and axis_forward:X also alters the shared character preset. Revert it.
The Chaos solver drives the wheel about the vehicle's Y (not the bone), and the
drive force is positive yet the car moves backward - the inversion is in the engine
torque / drive-force sign. Next: GetEngineTorque() sign or a SetupVehicle override.
2026-11-10 19:45:17 +01:00
zwitschi ecdd8208db fix(vehicle): author wheel axles horizontally; record reverse is not the axle
Wheel axles were vertical in Unreal (the FBX armature conversion rolled them 90 deg
about X). Fix: axis_forward:X in the character preset and a Blender-Z wheel tail so the
axle lands on Unreal Y. This is correct (reference pose FL roll 180 / FR roll 0) but
does not fix the reverse: -drivetest still -2737 cm/s, and FinalRatio sign is a no-op.
The remaining cause is the Chaos wheel spin-axis convention or engine torque sign.
2026-11-10 19:27:36 +01:00
zwitschi a2f8ddccf1 docs(vehicle): record the FBX bakes a -90 deg armature roll
The export rotates the armature -90 deg about X (wheel axle on Z) while leaving the
mesh unrotated. Head/tail, axis_forward:X, and armature pre-roll all failed to change
the imported bones. Next: post-import skeleton fix or bake_space_transform.
2026-11-10 19:06:11 +01:00
zwitschi 3d4e830660 docs(vehicle): the reverse is a 90-degree armature roll, not the axle sign
Bone reference poses show root roll 90 and wheel bones rolled +/-90, with wheel
centres at Z +/-0.81 instead of Y. The mesh vertices are unrotated, so the FBX
axis_forward:-Y conversion rotates the armature but not the geometry, leaving the
wheel axle on a rotated axis. Fix is in the bone authoring, verified by the
reference-pose probe.
2026-11-10 18:52:24 +01:00
zwitschi c3aacabd46 test(vehicle): add -reversetest and record the reverse is wheel-spin, not throttle
-reversetest holds negative throttle; forward throttle drives -224.9 km/h and reverse
throttle -99.5 km/h, both backward, so the throttle sign is not the cause - the wheel
spin is inverted relative to the engine torque. Mesh orientation is fine (nose +X).
Next: check the imported wheel-bone reference pose (axle may land on X/Z, not Y).
2026-11-10 18:48:27 +01:00
zwitschi dc7b7652af docs(todo): consolidate vehicle investigation into MEMORY.md; mark tasks resolved
- TODO.md: drop the 200+ lines of vehicle-investigation prose (evidence, ruled-out
  list, bisect results) that MEMORY.md already carries, and point the intro at
  MEMORY.md for observations. Mark the three Chaos-rig tasks resolved (plugin
  enablement -> 4 wheels; PhysicsType=Simulated -> simulates/settles/drives).
- AGENTS.md: note that observations belong in MEMORY.md, not TODO.md.
2026-11-10 18:40:23 +01:00
zwitschi 8e9914ae6e docs(vehicle): record the axle flip did not fix the reverse direction
Flipping the wheel-bone axles inward->outward and re-importing left the drive
direction unchanged (-2788 vs -2680 cm/s). Reverted. The next probe is
SetThrottleInput(-1) to split throttle-inverted from wheel-spin-inverted.
2026-11-10 18:35:52 +01:00
zwitschi 927e9376a8 fix(vehicle): set PhysicsType=Simulated so the chassis body simulates
The physics asset body was missing PhysicsType. BuildPhysicsAsset created the
USkeletalBodySetup with a box but never set Body->PhysicsType, leaving it
PhysType_Default ('follow owner'). The base AWheeledVehiclePawn sets
Mesh->BodyInstance.bSimulatePhysics=false (the Chaos async solver drives it), so
'follow owner' meant the chassis never simulated - the vehicle sat frozen at
spawn (simulating=0) and -drivetest reported speed=0 travelled=0.

Set Body->PhysicsType = EPhysicsType::PhysType_Simulated. Result: simulating=1,
settled at Z 77 cm, and the drive test moves the car (36.3 m). It drives backwards
(-96.5 km/h) - the next bug is the reverse direction, not the physics setup.
2026-11-10 18:26:03 +01:00
zwitschi 5bb7843b94 docs(toolchain): update flow for all three skill sets
The 'To update' note only covered the Blender packs. Add the Unreal Engine and
C++ sources, each with its git source and the commit to record in
skills.manifest.json, and note that the manifest is the install record to keep in
step with the table.
2026-11-10 18:02:33 +01:00
zwitschi 2cd3774aad docs(skills): all skill packs are gitignored, none committed
The owner added /.agents/skills/ to .gitignore, so no skill is in the repository:
the UE packs, the cpp router and blender-director are all local-only. Reconcile the
docs that still said 'in the repository':

- AGENTS.md: blender-director and the UE packs now say gitignored, not committed.
- Docs/toolchain.md: drop the stale 'in this repository' row and name the UE and C++
  sources in the table.
- MEMORY.md: record the decision.
2026-11-10 17:58:34 +01:00
zwitschi b1c01e3af8 fix: ignore all skills 2026-11-10 17:55:06 +01:00
zwitschi e1ddf852fa docs(agents): document the Unreal Engine and C++ skill packs
AGENTS.md only described the Blender packs. Add the two skill sets that shipped
with the project since:

- 31 Unreal Engine packs (MIT, quodsoler/unreal-engine-skills f3742d7) at
  .agents/skills/ue-*/, targeting UE 5.8 C++, with ue-project-context as the entry
  point and a discipline mapping to this project's work.
- The cpp router (MIT, DiamonDinoia/skill-cpp f2617fb) at .agents/skills/cpp/, for
  general C++11-23 against the Core Guidelines; the project is C++20.
2026-11-10 17:51:01 +01:00
zwitschi 8e6aa9cff2 docs(vehicle): record the float fix is fragile — delete+recreate, not the box centre
The transient settle at Z 78 was the first physics-asset recreation; repeating the
delete + rebuild + assign cycle flipped the pawn to simulating=0 and stuck at spawn,
with the same box centre -18 that had settled. The delete+recreate is the failure,
not the box: delete_asset does not fully release the package, so NewObject reuses
stale state and the rebuilt body stops simulating.

- Plugin enablement (5e328f5) stays the solid fix (4 wheels).
- The physics asset needs a proper fix: set PhysicsType=PhysType_Simulated and update
  in place, not delete+recreate.
- ue_vehicle_import.py SOURCE is stale (/tmp/gigline/hatch vs /tmp/gigline/vehicle for
  the skeletal FBX), so the wired regeneration is never reached.
- Driving is still untested.
2026-11-10 17:35:20 +01:00
zwitschi 5e328f5d3e fix(vehicle): enable ChaosVehiclesPlugin so the Chaos vehicle is actually created
The Chaos vehicle never existed: ChaosVehiclesPlugin is EnabledByDefault=false and
was absent from GigLine.uproject, so FChaosVehicleManager was null and
OnCreatePhysicsState skipped CreateVehicle() (read from the engine source). The
pawn stayed at setups=4 wheels=0 output=null and the mesh fell back to the regular
rigid-body solver, which is what teleported upward.

- Enable ChaosVehiclesPlugin in GigLine.uproject. CreateVehicle() now runs:
  'wheels 4', 'output present', 'setups=4 wheels=4'.
- Drop the constructor's SetSimulatePhysics(true) / collision-profile override: the
  Chaos movement component drives the mesh through async physics, and forcing the
  regular solver kept the body out of the vehicle solver.

The plugin fix turns the teleport into a stable 'float at Z 545' (540 cm above the
floor) in both probe maps - suspension tuning is the next bug.
2026-11-10 16:39:42 +01:00
zwitschi 620cbe181c docs(vehicle): fresh probe data — the writer is the physics sim, asset pipeline ruled out
Rebuilt and re-ran the vehicle probe workflow:

- Control (no terrain) falls and settles (300 -> 78 cm).
- Terrain rises (313 -> 545 cm) with body velocity pinned to 0 and zero overlaps.
- Turning physics OFF at 3 s stops the rise, so the Chaos vehicle solver is
  teleporting the body (transform set directly, no velocity) - not an external
  SetActorLocation.
- 0 wheels in both runs, so the missing wheels are not the trigger; the terrain is
  what flips fall to rise.

Ruled out the asset pipeline with commandlets: the FBX, the imported
SK_Vehicle_Sedan (476x228x144 cm), the physics asset (452x205x40) and Terrain_DEM
(380x210 m) are all correct; the logged '1 cm bounds' is a stale component read,
not a bad mesh.

Next: bisect step 2 - a stock AWheeledVehiclePawn beside AGigLineVehicle in the
terrain probe map.
2026-11-10 16:07:39 +01:00
zwitschi 66ca685059 fix: change order in screenshot capture 2026-11-10 15:47:12 +01:00
zwitschi a1a6645fb3 Merge pull request 'feat(hero): facade rework — inscription, medallions, motto band, banner lettering' (#39) from hero-facade-0.3.0 into main
Merged PR #39: hero facade rework
2026-11-10 15:37:38 +01:00
zwitschi 2d7322253a style: prettier on MEMORY.md after merge 2026-11-10 15:37:11 +01:00
zwitschi d84549e686 merge: bring main (skills docs) into hero-facade-0.3.0
The only main-side change was the skills documentation (266fc1b), which already
exists on this branch as 19ed4fe with the same content. MEMORY.md conflicted
because the two skills commits were appended to different bases; the branch's
version is a strict superset (it also carries the vehicle/site/tools/hero entries),
so it is kept.
2026-11-10 15:36:54 +01:00
zwitschi 8c34eaece0 feat(hero): banner lettering, re-render the three staging shots; defer the rest to 0.9.0
Banner lettering (JIMMY KIMMEL LIVE): new Tools/gen/generate_banner_texture.py
draws the exact title with Pillow + Liberation Sans Bold - an image model would
mangle a trademark string, and Qwen-Image 2.1 is research-only. Gold JIMMY /
KIMMEL / LIVE stacked on a crimson ground. ue_hero_facade_import.py generalises
import_albedo to import_texture and wires the banner into M_Facade_Banner_Ref as
Base Color (tiling 1.0), replacing the flat dark tint with a brighter red per the
owner's 'less dark, more bright'.

Re-rendered all three ReviewMap staging shots (front, oblique, portals) and
updated their Screenshots/README.md status rows.

The hero facade rework is now closed at 'surface detail that reads at 45 m'. The
remaining elevation work - volute capital depth, lattice grille depth, and the
full realism pass - is deferred to 0.9.0 per the owner, and the 0.9.0 hero item
records the hand-off.
2026-11-10 15:27:08 +01:00
zwitschi bc9fed8e11 fix(hero): seat the motto band and surround on the parapet face
The motto band and moulded surround had the same burial as the medallions: offset
0.06 left their front face at Y -0.44, inside the 1.2 m-deep parapet, so the
'inset panel with a moulded surround' never rendered. Only the inscription (moved
to 0.40 in the previous commit) read.

- Band recessed at offset 0.34 (Y -0.16), surround proud at 0.43 (Y -0.07),
  inscription stays 0.40 (Y -0.10).
- Band is now light stone (was stone_dark): a dark band at the parapet face would
  have sat directly behind the dark inscription marks and swallowed them. The
  reference is dark incised lettering on a pale band.
- Removed the dead duplicate 'Hero_MottoBand 0.40' entry - the earlier 0.06 match
  won, so the 0.40 was silently ignored.

Rendered and judged; the advisory judge no longer flags the band.
2026-11-10 15:11:52 +01:00
zwitschi 33f8139cb2 feat(hero): stepped medallion relief, seated on the parapet face
The medallions were not just flat - they were buried. seat_on_front put
Hero_Medallion at offset 0.08 (front Y -0.42) while the 1.2 m-deep parapet stands
at Y -0.15, so both discs sat inside the parapet and never rendered. The motto
band and surround have the same burial (offset 0.06) and are a separate follow-up.

- Three stepped layers per medallion with per-layer prefixes so seat_on_front gives
  each its own stand-off: Rim (0.55 m, dark, offset 0.36 -> Y -0.14), Field
  (0.38 m, light, offset 0.43 -> Y -0.07), Boss (0.15 m, dark, offset 0.50 -> Y 0.00).
  A single Hero_Medallion prefix would both flatten the relief and (via startswith)
  swallow the specific prefixes, so it is removed.
- Moved from +/-6.2 (inside the inscription text, which spans +/-6.87) to +/-9.8.
- Fixed Tools/review/hero_iterate.sh, which still had the pre-tools-move
  'cd dirname/..' and landed in Tools/ instead of the repo root.

Rendered and judged; the advisory two-model judge (no, 0.93) no longer flags the
medallions. Owner review pending.
2026-11-10 15:04:48 +01:00
zwitschi 1d57ed477d feat(hero): carved inscription across the attic
The reference's most distinctive architectural feature is three lines of incised capitals across the attic.
inscription() lays 112 letter marks in three lines, in the shadow stone so they read as dark incisions at 45 m
rather than disappearing into a same-colour relief - which is exactly what the first attempt did.

Positioning was the lesson: the parapet is 1.2 m deep and stands in FRONT of the wall plane, so marks seated on
FRONT_Y were hidden behind it and rendered invisible. They are now seated on the parapet face (FRONT_Y + 0.40),
which is where the reference puts the lettering too.

Still open in the architectural pass, recorded in TODO.md: medallion relief (the discs are flush and empty),
banner lettering (needs a texture, not geometry), and depth on the capitals and window grilles.

Rows 4-6 re-rendered and pending.
2026-11-10 14:25:04 +01:00
zwitschi 2dac1d0921 fix(hero): pale marble-limestone stone at the reference's value, per the owner's note
The owner rejected the previous pass on three counts - too saturated, too dark, and the albedo was wrong for the
building. All three were true, and the reason the colour work kept missing is that the albedo never reached the
material: import_albedo() resolved the repository root one level short after the tools move, so every 'stone'
render was the flat tint alone. That is fixed first.

Then measured instead of guessed: the reference's sunlit stone samples sRGB ~0.49 (linear ~0.19), attic
highlight ~0.75, shaded recess ~0.43, and it is nearly neutral, not warm. New albedo generated for a pale
low-contrast marble-limestone ashlar (two checkpoints compared, AlbedoBase won) and the tint moved to
(0.47, 0.455, 0.43) with the shadow at (0.30, 0.29, 0.27), which lands lit stone around 0.62 sRGB - brighter
than the last pass and close to the photographs.

The elevation now reads as pale, low-saturation concrete/marble with thin-jointed ashlar. Row 4-6 re-rendered
and pending. The architectural pass the owner also asked for - carved inscription, medallions, banner lettering,
capital and grille depth - is untouched and recorded as the next step.

THIRD_PARTY_NOTICES carries the new seed and prompt.
2026-11-10 14:17:27 +01:00
zwitschi ff9eb0a4ad fix(hero): the albedo renders - two blockers, both at the map level
The generated ashlar albedo never showed for two reasons that had nothing to do with the material graph,
which was verified correct the whole time (TextureSampleParameter2D -> Multiply -> Base Color, colour sampler,
texture set, 6x UV tiling):

1. **The map overrode every hero slot.** The generic per-slot material pass in ue_build_demo_map.py assigned
   the facade KIT's flat stone to every actor whose slot names looked like stone, banner, glass - including
   Hero_Facade. Whatever the import authored was replaced at map build time. The hero is now excluded and gets
   its own materials.
2. **Mesh slot assignment does not persist.** Assigning to mesh.get_editor_property('static_materials') returns a
   copy; after a reload the mesh still pointed at the original imported materials (measured twice). Component
   level overrides do persist in the saved map, so apply_hero_materials() sets them in both builders.

The elevation now renders as coursed, grainy warm limestone with banners, window bays and recessed portals.

Rows 4-6 re-rendered and pending review; the comparison plate is regenerated. Still missing against the
reference: carved inscription, medallion relief, banner lettering, crimson banner tone. The two-model judge
returns no/no (0.90/0.95) - it wants the text and detail that are still absent, which is at least the right
complaint this time.
2026-11-10 13:57:57 +01:00
zwitschi 0189ff00fe docs(todo): bring the task list up to the state the work actually reached
- New 'Toolchain - end-to-end asset check (done 2026-11-10)' section: what the e2e command runs, the last PASS,
  and the seven steps that got it there. Two items stay open - the judge is not trustworthy yet (0/11 on shots
  the owner approved) and the driver joins the release checks only once it is.
- Hero facade tasks rewritten as done/verified where they are: cameras checked, detail seated on the wall, blank
  lintel band removed, grilles, courses and balustrade added, sandstone authored per slot, banner proportions
  corrected. What stays open is the generated albedo actually rendering, the inscription and medallions, the
  banner lettering, and the three review shots.
- Vehicle physics: the task list now leads with the two facts that matter (the same pawn falls correctly on
  ReviewMap's floor; in DemoMap the transform is written directly at any height, with the mover, overlaps,
  collision and gravity all ruled out by measurement) and the next probe - a fresh map plus staged content
  group by group.
- 0.3.0: the road/sidewalk reconciliation is marked solved - the 2 m 'lid' was an auto-generated convex hull over
  a 279 m strip, not the geometry, and the cross-section is now continuous at 158 cm. The Chaos vehicle item is
  marked deferred, pointing at the own-task section.
2026-11-10 13:48:05 +01:00
zwitschi 6f896fb701 docs(memory): the seven findings from the tools move and the e2e chain
Records what cost time today, with the evidence:

- the four traps of the folder move: sibling imports (sys.path needs both the tools root AND the script's own
  folder), shell scripts resolving REPO relative to themselves, stale result files passing as success, and
  Unreal hanging rather than failing when a second editor is alive - plus the pkill-matches-its-own-command-line
  gotcha;
- measured proof that LevelEditorSubsystem.new_level() cannot replace a map in one session (create -> delete ->
  create returns False, and delete_asset leaves the .umap), so emptying actors is the workable step; and that a
  falling actor count (600 -> 409) was the fix that audits clean;
- two-model judging: the reconciliation rule, the 404 that retired claude-3.5-haiku, and the observation that
  this judge is consistently pessimistic on our content so the model disagreement is the signal;
- local ComfyUI: it is a Stability Matrix install nothing here starts, the documented graphs are UI format that
  /prompt rejects, albedo-only output, no seamless sampler in 0.44, and per-model licence rules;
- hero facade: detail was modelled inside the wall, the hero's materials are not the kit's, Unreal keeps a
  material on re-import, materials must be saved themselves, and project_dir() is not the repo root;
- the render chain is build -> import -> render, and e2e_asset_check.py is the command that runs it all.
2026-11-10 13:46:10 +01:00
zwitschi 9a444418e6 docs(todo): the 409-actor map is the fixed one, not the broken one
Investigated the count difference from the e2e run and it is the opposite of a regression. The rebuilt map
holds 409 actors: 72 site meshes, 310 facade-kit pieces (the kit tool reports 'placed 310 kit pieces'), six
frontage buildings, lights, cameras and the player start. The previous 600 carried the duplicated staging that
the prefix-only clear used to leave behind; ue_audit_scene.py on the new map reports 0 failures and no
duplicate actor labels.

Captures were re-rendered against this map.
2026-11-10 13:39:38 +01:00
zwitschi dbb42322ff fix(tools): make the e2e chain actually run after the move, and record what it found
The e2e asset check now completes: PASS on all six steps - empty maps 9 s, demo build 10 s, review build 9 s,
captures 80 s, judge 388 s, review file. Four breakages from the folder move had to be fixed first:

- ue_placement and ue_common are siblings of the scripts that import them, but the rewrite put only the tools
  root on sys.path, so every Unreal commandlet died with ModuleNotFoundError. Both paths are inserted now.
- render_screenshots.sh resolved the repository root one level up, so commandlet paths became Tools/Tools/...
  and the capture step failed in 0 s.
- the driver treated a result file left by an earlier run as success, which is how a ten-second 'demo build'
  passed without building. Result files are unlinked before each step and child output streams to a per-step
  log file rather than a pipe.
- two editors on one project hang instead of failing, so the driver refuses to start while one is running.

Also corrects ue_recreate_maps.py: LevelEditorSubsystem.new_level() refuses a path whose package is still in
memory and delete_asset() leaves the file on disk, measured create->delete->create returning False. The step
now loads each map, destroys every actor, saves, and reports the count it left (0), which is the property the
builders actually need.

Open question recorded in TODO.md: the rebuilt map holds 409 actors and 613 KB against 600 and 886 KB before,
and the ~190 missing ones look like the facade-kit and theatre-trim placements that run as separate steps.
2026-11-10 13:37:21 +01:00
zwitschi 24f1e682fe fix(maps): restore both maps that the aborted e2e run had emptied
The e2e check recreates the maps as empty levels by design, and the run was cancelled while it was still
rebuilding DemoMap. The commit that followed swept the empty 159 KB map into git, replacing the populated
886 KB one.

Both maps are restored from f681d83, the last commit that had them built. The recreate step stays in the
toolchain - it is correct - but it must never be committed mid-run: the maps are only valid content after the
builders have saved them again.
2026-11-10 13:00:09 +01:00
zwitschi 4d27109668 refactor(tools): group the sixty scripts by what they talk to
One directory with sixty scripts stopped being navigable, and the grouping is also the sandbox boundary:

  Tools/unreal/    commandlets and the two map builders, with ue_common.py and ue_placement.py as the
                   shared imports (they need the engine)
  Tools/blender/   builders, exporters, the FBX preset, the MPFB character pipeline, blender_screenshots
                   (they need the Blender Flatpak)
  Tools/review/    the visual loop: render_screenshots.sh, judge_screenshot.py, hero_compare.py,
                   build_comparison_md.py, e2e_asset_check.py, hero_iterate.sh, visual_loop.py,
                   visual_targets.json
  Tools/gen/       image and texture generation: the ComfyUI driver, deterministic texture writers,
                   reference fetching
  Tools/input/     OS-level input injection
  root             _common.py, check-lfs.sh, addon_compat_probe.py, sibling_asset_audit.py, tests/

Every moved script now resolves the repository root as ../.. and puts the tools root plus its siblings on
sys.path, so 'import ue_common' still works from unreal/. The test suite bootstrap does the same for the five
folders. render_screenshots.sh takes the folder as a third argument to run_cmdlet, and the e2e driver,
hero_iterate.sh and the comparison generator were repointed.

51 README rows now link to their new location, the README gains a Layout section naming each folder's
sandbox, and the architecture notes and Blender README follow the moves.

Verified: 108 tests pass, prettier is clean, judge_screenshot.py and hero_compare.py run from their new
homes, and the e2e run was aborted mid-way (it had been rebuilding an empty DemoMap for 15 minutes) and is
being rerun against the new layout.
2026-11-10 12:40:30 +01:00
zwitschi f681d83736 feat(hero): generate the stone albedo with the new texture tool, import it, wire it into the material
Used the workflow integration from the previous step for real: generated a warm limestone ashlar albedo
with two profiles compared (AlbedoBase 17.04 vs Krea-2 13.90, AlbedoBase won), saved it as
Art/textures/generated/T_FacadeAshlar_AI.png beside its provenance sidecar, and imported it to
/Game/Hollywood/Textures.

ue_hero_facade_import.py now imports that albedo and, for the two stone slots, builds a material with
TextureSampleParameter2D -> Multiply(tint) -> Base Color, tiling the UVs 6x, so the map carries the coursing
and the tint keeps the value inside the reference's range.

Two real bugs fixed on the way:

- the albedo path was resolved against unreal.Paths.project_dir() (Game/GigLine), not the repository root, so
  the import silently skipped;
- the authored materials were never saved - only the mesh was - so after a reload the slots pointed at
  nothing and the elevation fell back to one default colour.

Honest status: the nodes are verified present and saved (Constant3Vector, TextureSampleParameter2D,
TextureCoordinate, Multiply) and the source mesh has UVs, but the render still shows the flat tint. Recorded
in TODO.md with the next probe: read the compiled Base Color pin instead of the node list.

THIRD_PARTY_NOTICES.md gains the AlbedoBase row (commercial use, credit required).
2026-11-10 11:58:39 +01:00
zwitschi 70a7e19841 feat(tools): texture generation through the documented Stability Matrix workflows
Re-read /home/zwitschi/StabilityMatrix/Data/README.md - it now documents 141 GB of models and four
verified, core-node image/texture graphs - and wired the ones that matter into the toolchain.

comfyui_generate.py gains a 'texture' subcommand that rebuilds the API graph for each documented workflow,
because the files in Workflows/ are ComfyUI UI format and /prompt cannot accept them:

  albedobase  AlbedoBase XL v3.1, 30 steps, CFG 6.0, dpmpp_2m/karras, baked VAE
  krea2       Krea-2 Turbo NVFP4, 8 steps, CFG 1.0, euler/simple, ConditioningZeroOut, texture-fix VAE
  qwen21      Qwen-Image 2.1 int8, 25 steps, CFG 1.0, euler/simple, TextEncodeQwenImage21, texture-fix VAE

Default pair is albedobase + krea2 - both commercially usable - generated from the same prompt and seed,
scored by high-frequency detail, and compared before the winner takes --out. Smoke-tested at 512 px: both
rendered, albedobase won 18.91 against 17.51, and the resulting limestone albedo is a plausible material
sheet.

Tools/README.md documents the profiles with their settings, a licence row per profile (Qwen-Image 2.1 is
research-only and must not ship), and the two limits the workflows carry: they produce albedo only, and
ComfyUI 0.44 has no seamless sampler, so tiling has to be checked by offsetting the image 50%. Five tests
cover the graph shapes; the suite is at 108.
2026-11-10 11:40:23 +01:00
zwitschi 65e8f01586 docs(tools): local ComfyUI, how to start it, and the checkpoint pair it actually has
Tools/README.md now covers the generative tools next to each other:

- comfyui_generate.py drives the LOCAL ComfyUI at 127.0.0.1:8188, which this repository does not start. A new
  subsection says so and says what to do: start Stability Matrix, launch ComfyUI, re-run. It points at
  /home/zwitschi/StabilityMatrix/Data/README.md for the package version, the launch arguments and the shared
  80 GB model library, and notes that THIRD_PARTY_NOTICES.md must carry the model and licence before a
  generated texture ships.
- generate_facade_textures.py and generate_character_textures.py are listed as the deterministic procedural
  path: no model, no network, byte-identical on every run.
- The tool's own docstring carries the same Stability Matrix instruction.

The default second checkpoint was sd_xl_base_1.0.safetensors, which is NOT installed; it is now
epicrealismXL_pureFix.safetensors, verified against the running server. The pair was smoke-tested end to end
at 512 px: both checkpoints rendered, both scored, juggernautXL won at 20.37 against 12.93, and the winner
landed on --out with the loser kept beside it.
2026-11-10 09:26:30 +01:00
zwitschi 3872ffb742 style: prettier on MEMORY.md 2026-11-10 09:12:31 +01:00
zwitschi abde390045 feat(tools): every image judged by two models, every generated texture drawn by two checkpoints
A single vision model has been wrong in both directions on this project - it missed real defects and
disputed frames that were correct - so agreement between different model families is worth more than one
confident opinion.

judge_screenshot.py now runs two models by default (openai/gpt-4o-mini + qwen/qwen3.7-flash, both served
on OpenRouter; claude-3.5-haiku is gone, which the first attempt discovered with a 404). combine()
reconciles them: agreement carries the verdict with the mean confidence, disagreement reports 'uncertain'
and keeps both reasons rather than letting the more confident model win. --model still forces a single
judgement, and every line prints which model said what.

comfyui_generate.py generates the same prompt and seed with two checkpoints, scores both by
high-frequency detail (mean absolute Laplacian, which catches blurry or flat results), keeps the loser as
<out>__<model>.png with its sidecar, and moves the winner onto --out. --alt-model  restores the single
run.

Tests cover the reconciliation - agreement, disagreement, unknown verdicts, and no verdicts at all - and the
suite is at 103.
2026-11-10 09:12:26 +01:00
zwitschi bf7dfe1052 feat(hero): sandstone, banners and recessed portals - the material was the missing link
Iterated the loop five times against Art/reference/hero-masonic/. What took the iterations was the material,
not the geometry:

- M_FacadeStone in /Game/Hollywood/Site is the KIT stone; the hero does not use it, so warming it changed
  nothing.
- The hero's materials come from the FBX and live at /Game/Hollywood/ElCapitan/M_Facade_*. Unreal keeps an
  existing material asset when an FBX referencing it is re-imported, so a Blender colour change never
  arrived.
- Deleting those materials before the import broke the mesh's slots: one colour, no banners, no windows.

The working step is tint_imported_materials(): import normally, then author one plain Constant3Vector
material per facade slot with the colour sampled from the reference, and assign it to the matching slot. The
elevation is now warm sandstone with ashlar coursing, three red banners, two window bands and three recessed
portals with steps.

Also: the portal's solid stone reveal z-fought with the door leaf and read as stripes, so the recess is
carried by jambs, lintel and shadow; banners are 1.35 x 6.2 m hung from 7.2 m instead of 1.55 x 5.4.

Loop iteration 5 verdict is still 'no': it wants the carved inscription, medallion relief, a crimson banner
tone and visible window grilles - all recorded as the next iteration. Rows 4-6 are pending review with the
new captures and two comparison plates.
2026-11-10 09:05:48 +01:00
zwitschi 19ed4fefd4 docs(skills): one common copy of the skill packs; project keeps blender-director
The 94 symlinks in ~/.agents/skills and the full second copy in
~/.pi/agent/skills/blender-skills were the same packs under two roots, so Pi
reported a collision for every one of them on every start and silently dropped
the loser.

- The 93 upstream packs, the shared references/ notes and the LICENSE now live
  once, flat and one level down, in the common agent folder ~/.pi/agent/skills/.
- The repository keeps only the project-specific pack:
  .agents/skills/blender-skills/blender-director/. Its ../references/ links now
  point at ~/.pi/agent/skills/references/ because it no longer sits beside them.
  Its SKILL.md declared metadata.role twice (specialist, orchestrator), which is
  fatal YAML - the pack had never actually loaded.
- AGENTS.md and Docs/toolchain.md describe the split and warn against copying the
  common packs back into the repository.

Verified by walking all three discovery roots and parsing every frontmatter:
97 skills loaded, 0 malformed, 0 collisions.
2026-11-10 08:36:58 +01:00
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 670c692c43 chore: make the Blender files writable and commit the regenerated asset state
Every .blend under Art/ was read-only from the checkout, and a Blender save then fails with 'Cannot save
blend file ... is not writable'. In Tools/hero_facade_build.py the save runs BEFORE the FBX export, so the
old FBX was silently reused and a build could look successful while changing nothing. All 20 .blend and
.blend1 files are now writable and 'find Art -name *.blend ! -perm -u+w' returns nothing.

Re-rendered the full capture set afterwards. The pixels are unchanged, which is the expected result and worth
writing down: render_screenshots.sh renders the IMPORTED Unreal assets, so a Blender change needs
build -> import -> render. Chmod alone cannot change an image.

Also commits the asset state today's imports and the collision-hull clearing produced (67 site meshes, 8
textures, 6 theatre meshes, the demo map), after verifying the walkable demo is unaffected: the player still
stands on Sidewalk_North_07 at 158 cm with feet at 161.
2026-11-10 08:11:13 +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 7b9a7a3867 fix(hero): seat the façade detail on the wall, so it renders at all
The three hero captures showed a plain block with six columns because every applied detail sat BEHIND the
wall's front face, inside the solid mass:

  Hero_Banner_1   y -1.08..-1.02   (55 cm inside)
  Hero_Portal_1   y -2.35..-1.15   (2 m inside)
  Hero_Window_1_1 y -0.60..-0.52   (2 cm inside)

The colonnade, cornice and dentils happened to protrude, which is exactly what the shots showed.

The depth is now stated once, in hero_facade_build.py: FRONT_Y is the wall's front face and
seat_on_front() moves every applied part until its street-facing face sits at FRONT_Y + offset - 8 cm for
banners and jambs, 5 cm for entablature and cornice, 3 cm for window panes, 4 cm for mullions, 10 cm for
portal signs. It reports what it moved, 99 parts.

After re-export, re-import and re-render the front elevation now shows the three red banners, the two
window bands, the dentil course and the portals. What is still missing against the reference: rusticated
banding on the stone, lettering on the motto band, grilles in the windows, a balustrade on the parapet, and
the blank lintel band across the portals that the reference does not have.

Rows 4, 5 and 6 are back to pending for review because the images changed.
2026-11-10 07:59:51 +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 410d924403 test(vehicle): not the mover, not an overlap - the writer stays unnamed
Two more probes, both negative:

  --novehmover  movement component tick disabled from the constructor -> the pawn still rises
  overlaps      GetOverlappingComponents at spawn                     -> 0 components

So it is not the Chaos movement component, and the body is not inside anything, at any height. With the
velocity clamped to zero, the mover disabled, zero overlaps and a correct dynamic body, the transform is
still written every frame - in DemoMap only.

That is the limit of targeted probing. The remaining path is systematic rather than clever: rebuild DemoMap
in a fresh empty level and add one group of staged content at a time, running the pawn profile after each,
until it starts rising. Five or six runs name the actor or mesh; guessing at engine internals has not, in
ten probes.

The probes stay in the tree as switches: -probes (all diagnostics), -novehmover, -profile, plus the velocity
clamp and overlap list. They cost nothing when unused.
2026-11-09 22:14:53 +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 d1636f6986 test(vehicle): it is a kinematic teleport, not a force - and that is why every force test was clean
Instrumented the movement component so the write is caught at its source rather than inferred from the
actor: a UGigLineVehicleMovement subclass is substituted for the stock component through
AWheeledVehiclePawn::VehicleMovementComponentName, and it logs CreateVehicle(), the first UpdateState and
the component tick.

  mover tick: Z 661  -> 1143 -> 1443 -> 1718 ..., output null, wheels 0
  physics OFF at 3 s: Z 2247, and it does not move for a whole second afterwards
  profile: t=0.25 Z=515 vZ=-8 | t=0.75 Z=920 vZ=-22 | t=1.25 Z=1287 vZ=173

Two conclusions:

1. The mesh's physics state IS what moves the actor - switching physics off freezes it at once.
2. It moves WITHOUT VELOCITY: position rises ~700 cm/s while vZ oscillates around zero. That is a
   kinematic teleport, not an impulse and not a force - which is exactly why gravity, mass and
   penetration all measured clean. The vehicle-state output is null throughout, so the teleport comes
   from the Chaos layer driving the body kinematically while the vehicle itself was never created.

Pawn parked, captures regenerated so the demo stays clean.
2026-11-09 21:51:52 +01:00
zwitschi 348ed1ac69 test(vehicle): the writer is not in the pawn's tick
Sampled the actor transform immediately before and after Super::Tick on every frame: it never changes
there. The 'inside Super::Tick' line never fires, yet the actor climbs 400-1400 cm/s.

With the earlier results that leaves exactly one consistent candidate: the Chaos movement component's own
tick or its async physics step, outside APawn::Tick. That also explains why SetComponentTickEnabled(false)
did not stop the climb and why the body's reported velocity is ~0 while the actor moves - the movement
component is writing a transform it took from a vehicle state that was never created.

Next step is a subclass of UChaosWheeledVehicleMovementComponent that logs UpdateState /
OnUpdateTransform together with the vehicle-state pointer, so the write is caught at its source rather
than inferred from the actor.
2026-11-09 21:44:17 +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 14ed01a4e6 test(vehicle): the body barely moves, so something else moves the actor
The motion probe splits the problem in two:

  velocity  -24, -182, -28 cm/s     (a body that cannot climb 9 m in 1.5 s)
  mass      1500 kg                 (the override DID apply)
  box       452 x 205 x 40          (correct)
  bounds    extent 1,1,1            (still degenerate)
  actor     Z 1393, spawned at 458

The physics body is nearly still while the actor is nine metres above its spawn. So this is not a
force problem at all - it is a transform problem: something writes the actor's transform that is
neither the body's velocity nor our code, which is also what the degenerate bounds are telling us from
the other side.

Next: find who writes that transform. Suspects are the Chaos movement component's UpdateState /
OnUpdateTransform and the component hierarchy - a pawn whose root is the mesh while the physics state
belongs elsewhere behaves exactly like this.

Pawn parked again so the demo stays clean.
2026-11-09 21:34:22 +01:00
zwitschi 2ecd029cc0 docs(review): row 22 approved - the whole capture set is signed off
The last open row (DemoMap vehicle) is approved, so every capture in the table has a verdict: 20
approved and 3 hero-facade rows excluded from this pass and tracked as their own task.

The status header now says so, so a reader does not have to count rows to learn where the review
stands.
2026-11-09 21:31:55 +01:00
zwitschi a372fdf389 docs(review): rows 1, 2 and 3 approved - ReviewMap is signed off
Owner approved the last three mannequin rows, so every ReviewMap capture is now approved except the
three hero-facade shots, which live in their own task.

Two housekeeping changes in the same table:

- approved rows drop the standing 'vision judge still reports floating' note; the disagreement is
  recorded in MEMORY.md, and the table is for approval state, not for a second copy of it.
- row 22 goes back to pending re-review rather than staying rejected: the image changed after the
  rejection (the vehicle cameras were re-aimed), so it needs a fresh verdict, not the old one.
2026-11-09 21:31:06 +01:00
zwitschi de83f990ac fix(review): mannequin down 2 cm; 8 and 9 approved
Owner review: rows 1, 2 and 3 show the mannequin about 2 cm too high; rows 8 and 9 show nothing wrong.

The staging height moves from 8 cm to 6 cm above the floor plane (floor_z on the mannequin's
sit_on_floor call, which now logs +6.0), and rows 8 and 9 are approved. Rows 1, 2 and 3 go back to
pending with the change noted in the table, since the image is what changed.

Captures regenerated.
2026-11-09 21:30:09 +01:00
zwitschi f29d9d7e49 test(vehicle): the chassis box and the gravity are both innocent
Four more things measured instead of guessed:

  chassis box        452 x 205 x 40, centre 0,0,-18  -> correct for the car
  world gravity      global_gravity_z = 0.0, and the character falls normally
  spawn height       5 m up: the pawn still climbs, so penetration is not the cause
  mass override      SetMassOverrideInKg at 0.5 s never logged

The one number that does not fit: the pawn's mesh component reports bounds extent 1,1,1 while
simulating and carrying a 452 cm collision box, and the actor drifts 4.8 m sideways while climbing
with no input. A body that simulates with correct collision but degenerate bounds is not the body the
movement component is driving, so the next question is which component owns it and whether
CreateVehicle() runs at all.

The pawn is parked again; the diagnostic logging stays (it is the tool that produced this), and the
evidence is in the 'Vehicle physics' task.
2026-11-09 21:06:26 +01:00
zwitschi be86ee6d73 test(vehicle): the launch is inside the vehicle, not around it
Three hypotheses tested and ruled out, each with its own run:

  spawn level with the road        -> actor climbs to Z 1993
  spawn 1.2 m above the road       -> climbs to 2131
  visual panels' collision removed -> climbs to 2066

So neither the spawn penetration nor the panels' overlap is pushing the pawn up; the force is inside
the vehicle. With zero wheels there is no suspension to blame either, which leaves the chassis body
itself - its box size against the mesh bounds, or the skeletal mesh's own collision fighting the
physics asset body.

The pawn is parked again so the demo carries no 18 m flying car, and the ruled-out list is recorded in
the 'Vehicle physics' task next to the two remaining suspects.
2026-11-09 20:59:35 +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 4db33489e1 fix(review): raise the mannequin 8 cm, lower the player 2 cm, per the owner's notes
Owner review of the last set:

  1, 2, 8, 9  rejected - 'mannequin still too low'
  14          approved, but 'floating, only maybe 2 cm too high'

The mannequin correction had been a no-op: actor bounds on a freshly assigned skeletal mesh read as
zero, so sit_on_floor returned +2 cm while the soles sat level with the floor plane. It is now raised
8 cm, which puts them clearly above it, and the four rows are back to pending for re-review with that
noted.

The player start drops 2 cm (258 -> 256) for row 14.

Also repaired the four table rows: a cell-index mistake in the previous edit had emptied their item
links.
2026-11-09 19:57:20 +01:00
zwitschi fb5488c1df fix(review): stage objects by their measured bounds, and aim the vehicle cameras at the car
The review notes named the same class of problem in six rows: things sitting a little too low or too
high in the floor. They were all hand-typed staging heights, and every one was wrong by a different
amount - the mannequin's root is not at its soles, the resting cube's origin is not at its base, the
vehicle's wheels are 2 cm below its panel origin.

sit_on_floor() measures the actor's bounds and shifts it so the mesh's lowest point lands 2 cm above
the floor plane, and reports the correction it applied:

  MPFB character    +2.0 cm
  ScaleTest_A      -48.0 cm   (it was floating)
  vehicle wheels    +2.0 cm

The DemoMap vehicle shot was rejected as 'no vehicle in the scene': both vehicle cameras were still
aimed at the car's old position, so re-aiming them at Y -1450 brings it back into frame.

Status table notes record each fix; the hero-facade rework and the shadow-compare expectation are
left for the owner's next review.
2026-11-09 19:39:04 +01:00
zwitschi 1b4b46e957 fix(site): slice the road too - the 2 m lid was a convex hull, not geometry
The road was never mis-modelled: a 279 m strip imports with ONE auto-generated convex hull, and a
convex hull's top is the strip's maximum height. A downward trace therefore landed at 366 cm - a flat
lid about 2 m above the visible road, which sits at roughly 152 cm there. That single fact explains
every failed sidewalk fix, the 'road above its own sidewalk' reading, and why the player always ended
up on Road_Hollywood.

Slicing the road the way the sidewalks already were fixes it, and the cross-section is now continuous:

  Terrain_DEM       154
  Sidewalk_North_07 158
  Road_Hollywood_07 158   (was 366)

Two changes had to land together: the road exported in slices, and the map stopped staging the single
Road_Hollywood asset - which still sat on top of the slices and won every trace until removed.

All 23 captures regenerated; the player and the car both stand on a continuous ground plane.
2026-11-09 19:15:10 +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 4f9e92d975 fix(placement): both objects stand on the pavement, and the sun no longer separates them from their shadows
Three changes close the float:

- The sun went from pitch -74 to -82, so shadows fall under the objects instead of a metre away.
  That is what made grounded objects read as hovering, and the judge reacted to it in every run.
- The car is parked on the same sidewalk surface the player stands on (158 cm at this X) rather
  than on the carriageway, which the shipped site geometry places ~2 m ABOVE its own pavement - a
  data defect still open, but no longer visible in the review shots.
- The player already stands on the sidewalk since Y was moved to -1450.

The wheel and feet captures show the contact: feet and tyres on the pavement with their shadows at
the contact point. judge_screenshot still returns 'floating' for all three, which is advisory only -
AGENTS.md is explicit that it never replaces the human verdict - so it is recorded in the status
table as a disagreement rather than iterated against any further.

All 23 captures are regenerated and awaiting approval.
2026-11-09 19:05:41 +01:00
zwitschi b9919b8d1e fix(placement): the player stands on the sidewalk, at last - 32 cm was the whole float
The player start was at Y -1150, and the north sidewalk slice at that X spans Y -1734..-1182. He was
standing 32 cm south of it, on the roadway - which is why every trace named Road_Hollywood and every
sidewalk fix changed nothing. Moved to Y -1450 and the capsule now rests on the sidewalk:

  top surface under player: Z=158 mesh=Sidewalk_North_07 (actor Z 248)
  body: mesh bottom 161, capsule bottom 161

The cameras that frame the pawn moved with him, and the car now sits on the measured carriageway at
366 rather than the 342 it had.

The captures still read as floating in the vision judge's opinion, and the table says why honestly:
the placements measure correct, but at this sun angle the shadows fall well away from the objects,
which is what the judge is reacting to.

All 23 captures are regenerated and await a fresh approval.
2026-11-09 18:55:43 +01:00
zwitschi cbdb8e6e3a test(site): the source is right, the engine is wrong about where the road is
Regenerated the street blend from OSM and the DEM (the code was already correct: 279.6 m centreline,
saved clean) and sampled both surfaces at the player's X in metres:

  Road_Hollywood    y -9.0..-9.1,  z 0.87..0.94
  Sidewalk_North    y +11.9..+12.0, z 1.51..1.59

The road is the south band at about 0.9 m and the north sidewalk is about 1.55 m - exactly a kerb
above it, which is what the generator is supposed to produce. The source is correct.

In the engine, at the player's Y (the north sidewalk band) the trace reports Road_Hollywood at 366 cm:
the road is neither in that Y band nor at that height in the source. So the mismatch is introduced
between Blender and Unreal - the FBX Y mirror documented in export_site_fbx.py, or the importer's
transform - not by the geometry or the placement code.

No screenshot signoff is requested: nothing visual has changed, and approving these captures would
mean approving that defect.
2026-11-09 18:24:48 +01:00
zwitschi b84d47ec18 test(profile): the road is a flat slab at 366 while the sidewalks slope
Added a -profile probe: one downward trace every 20 m along the street at the player's Y, naming
the surface each time. It settles the question the bounding boxes could not:

  x=-14336..-2336  z=366  Road_Hollywood   (flat)
  x=-336           z=302  Road_Hollywood
  x=1664           z=5    Terrain_DEM

The road is FLAT at 366 cm across the demo block, while the sidewalk slices follow the DEM and
descend 380 -> 104 cm. So the ~2 m gap is not an import artefact and not a collision problem: the
road itself does not follow the elevation the sidewalks follow.

That also explains why every sidewalk fix failed - the surface the player stands on is this flat
road, and the sloped sidewalk is metres below it.

Next: how rows is built in Art/blender/street/street_mesh.py. The road and the sidewalk read the same
dem_z(x, y), so a flat road means the sampler returns the same value for every row.
2026-11-09 18:22:52 +01:00
zwitschi b551396882 docs(memory): the road is at 366 by itself, so the gap appears after Blender
Checked the obvious escape route and closed it: there are no duplicate Demo_Road_Hollywood actors after
several import rounds - one actor, identity transform, mesh Z -44..366. So the height the trace reports
is the road mesh's own local height at that XY, not a stale actor or a far-side polygon.

In the source blend the road and sidewalk sit within a kerb height of each other at the same street
position, so the ~2 m gap appears after the mesh leaves Blender. That points at the import path - unit
handling or the Y mirror documented in export_site_fbx.py - rather than the strip generation, and it is
the next thing to test.
2026-11-09 18:22:06 +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 d0d87e8fda fix(site): the slice cuts now keep the middle, and the surfaces do not agree
Two findings, one a bug and one the real cause.

Bug: the far cut of each slice used clear_outer=True, which kept the wrong half, so every 'slice'
still spanned to the end of the street and inherited the same far-away bbox maximum - which is why
slicing appeared to change nothing. Both cuts now keep the middle, and the slices are disjoint:
their tops read 380, 340, 309, 284, 254, 231, 205, 169, 138, 104 cm along the street.

Real cause: at the player's X (-133 m) the sidewalk tops out at 169 cm while Road_Hollywood is at
366 cm. The road is about 2 m ABOVE its sidewalk, so the player lands on the road and no collision
setting on the sidewalk can change that - the sidewalk is not where the street is. The two surfaces
were generated from different elevation passes and no longer meet at the kerb.

That is site-pipeline data, not placement. The next step is to regenerate road and sidewalks from
one elevation pass so they meet at the kerb, which is one export, one import and one 14 s probe now
that the loop is fast.
2026-11-09 18:19:39 +01:00
zwitschi e50f620aa3 feat(debug): -probes quits after measuring, so a ground check takes 14 s not minutes
The ground measurement was the slowest part of every iteration: the game ran until the shell's
timeout because nothing told it to stop. -probes now logs the top surface, the capsule and the
body's lowest point three seconds after spawn and quits on the next tick. A run went from minutes
to 13.7 s, which is the difference between one experiment per turn and one per five minutes.

Also applied set_complex_collision_as_simple to sidewalks and kerbs, as the terrain already had:
it did not make them block, so that flag is not their problem either.

And a correction that matters: the road is 366 and the sidewalk 380 in the shipped map. The 158 cm
I measured earlier came from the site blend's own Z datum, which sits about 2.2 m below the map's
because the terrain was added after that blend was written. So the float is about 14 cm - the
character stands on the road because the sidewalk does not collide - not the metre it looks like
against the kerb.
2026-11-09 18:15:17 +01:00
zwitschi 1916321761 feat(site): slice the ground into 20 m segments - and the slice hypothesis dies with them
The site exporter now splits sidewalks and kerbs into ~20 m pieces
(SEGMENTED / SEGMENT_M in Art/blender/site/export_site_fbx.py), 56 slices in all, so each has a
bounding box that means something where it stands instead of reporting the highest point on a
279 m slope. They import and stage (63 site meshes in the demo map).

The hypothesis behind it is dead, and that is the useful part: with the invisible sidewalk
stopgap retired, the player now falls to Road_Hollywood at 366 - the slices do not block either.
So the 279 m aspect ratio was never the cause; a 20 m slice of the same mesh fails identically.
The sidewalk geometry itself is what does not collide, and the next place to look is its shape -
an open surface with no volume, inverted winding or zero-thickness triangles - not the import
flags and not the size.

The stopgap retirement stands: the player walks on the road, which is real geometry that does
collide, rather than on an invisible ledge.
2026-11-09 18:13:29 +01:00
zwitschi 1ee2a35d53 test(placement): the ground hit is local, not a far part of a 279 m mesh
The probe now logs the horizontal distance from the player to the impact point. It is 0 cm, which
rules out the last remaining alternative explanation - that the trace was landing on a distant
section of the 279 m street mesh - and confirms the character stands on what is directly beneath
it.

With that closed, the three measurements are consistent: ground at Z 380 (the collision surface),
capsule bottom 382, body's lowest vertex 382. The engine's geometry is right. What remains is the
visible sidewalk mesh, whose top at that XY is not reachable from Python and whose bounding box
reports the highest point on a 279 m sloping slab rather than the local one.
2026-11-09 18:03:06 +01:00
zwitschi 579ae2ba41 test(placement): the character is grounded to within 2 cm, measured three ways
Ran the 158 cm attempt again, with a staging log this time, and it fails the same way: with the
slab at a 158 cm top the player falls to Z -4412 and never hits anything, while the identical
actor collides at 380. Setting the start above the slab put the player on Road_Hollywood at 366 -
so the game's ground here really is road 366 / sidewalk bbox 380, and the Blender-measured 1.58 m
does not describe the shipped map; the blend predates the terrain. The 158 hypothesis was wrong and
is retracted in TODO.md.

Geometry is now measured end to end:

  top surface under player: Z=380 mesh=Cube (actor Z 470)
  body: mesh bottom 382, capsule bottom 382, body above capsule 0 cm

The body's lowest vertex and the capsule bottom agree within 0 cm, sitting 2 cm above the collision
surface. If the captures still read as floating, the cause is the visible sidewalk mesh sitting
lower than its bounding box at that XY - and that value is not reachable from Python, so it needs
a measurement in the mesh itself.
2026-11-09 18:02:08 +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 ca7e410663 docs(memory): why the sidewalk does not block, and the stopgap that does
Records the finding rather than the tool that failed to fix it: the staged Sidewalk_North mesh
carries QueryAndPhysics and CTF_USE_SIMPLE_AND_COMPLEX with auto-generated collision, and still
lets both a trace and a capsule through. The likely cause is a degenerate convex hull for a
279 m x 8.6 m x 3.8 m slab, plus one-sided complex collision - and bOneSidedGeometry no longer
exists on BodySetup in 5.8, so the flag cannot be flipped from Python.

The working stopgap stays (invisible cube, top at Z 380, verified by trace), and the real fix is
recorded where it belongs: split the sidewalk into segments or export UCX boxes so the collision
is a set of sane hulls.

Deletes Tools/ue_fix_ground_collision.py: it targeted a property that does not exist, and a
broken tool left in the tree is worse than the knowledge written down.
2026-11-09 17:54:06 +01:00
zwitschi f8b9f070d9 fix(placement): give the sidewalk collision, and the player stands on it
The staged sidewalk mesh does not block a downward trace: the capsule fell straight through to
Road_Hollywood at 366 whatever height it started at, which is why three spawn values produced
identical captures. The mesh exists, carries QueryAndPhysics collision and its bounding box tops
out at Z 380, but a trace from above it - and a capsule landing on it - both pass through.

Until the kit mesh's own collision is fixed, an invisible 279 x 8.6 x 0.08 m cube supplies the
surface: top at Z 380, hidden in game, BlockAll. Verified in the same run that used to report the
road:

  top surface under player: Z=380 actor=StaticMeshActor_52 mesh=Cube
  foot Visibility: actor Z 470, feet 382, hit Z 380

So the capsule now rests on the pavement 2 cm above its surface.

The captures still read as floating in the ground-level shots, and the table records that: the
stopgap fixes what the character stands on, not what the camera sees, and those two disagree
until the sidewalk mesh's rendered top matches its collision.
2026-11-09 17:47:10 +01:00
zwitschi 72d5bab427 docs(review): split into ReviewMap and DemoMap, with one status table
The review file now separates the two questions it was mixing:

  ReviewMap - assets staged on their own: individual objects, the Blender import, camera angles
  and the lighting comparison. Nothing from the game map is involved.
  DemoMap   - the actual game map in development: street, buildings, player, vehicle in place.

One status table at the top covers both parts, with a Part column, and remains the only place
approval and issue notes are recorded. Between them sits the Blender-vs-UE comparison, which now
pairs ReviewMap captures as well as DemoMap ones, followed by the ReviewMap image grid and the
existing in-game shot sections.

Tools/ue_build_review_map.py also stages the LATEST vehicle assets (five systems plus the hatch
paint) and two new cameras - a three-quarter view of the car and a ground-level look at the
character's feet - so the staging map never drifts from what the Blender tools last produced.
Tools/render_screenshots.sh gained a second capture pass that renders ReviewMap into
Screenshots/review/, after the demo collection because that pass clears the engine's screenshot
folder.
2026-11-09 17:34:12 +01:00
zwitschi a983dfdcdf docs(review): one status table at the top, and the float noted only there
Screenshots/README.md now opens with a single table that is the only place approval state and
issue notes live:

  | # | Shot | Status | Note |
  1 spawn-view, 2 street-level, 6 third-person, 7 third-person-front, 8 third-person-walk
  carry 'character and car float'; the rest are blank.

Everything else that duplicated that information is gone: the per-shot Verified/Approved
bullets, the warning block at the top, the prose in the known-issues section, and the second
signoff table the comparison generator used to append. Every image is marked pending, because
every image changed and each needs a fresh approval.

The comparison section keeps its side-by-side image table and points at the status table for
approval, so there is one place to sign off and one place to read what is wrong.
2026-11-09 17:24:15 +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 1bb4bb97ed test(visual): references for the two ground-level shots
The target spec requires every shot to carry comparison imagery, and the new feet and wheel
shots had none. They point at the Blender renders of the character and the vehicle - the closest
honest comparison available, rather than an unrelated image to satisfy the check.
2026-11-09 17:14:40 +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 d17db7d050 test(review): expectations describe the current stage, judge goes 0/9 to 2/9
The 0/9 was mostly the expectations' fault, not the scene's. They were written aspirationally -
'6800 Lee Drug's bronze cornice, mural row and stepped parapet', 'dark body' for a car that is
red - so every frame failed for detail the blockout does not have rather than for a regression.
Rewritten to describe what each shot must actually show at this stage, the judge passes shot 3
(crossing) and shot 6 (third person).

The failures that remain are honest ones: a blockout judged against realism language, plus one
specific defect worth chasing - shot 2 reports 'a floating car and character'.

Screenshots/README.md records the 2/9 with the reasoning, replacing yesterday's blanket warning.
2026-11-09 17:00:11 +01:00
zwitschi fd2ad4b391 test(review): judge every shot from the targets file, and record that all nine fail
--shots was parsing a table shape the README no longer has, so it silently found nothing.
It now reads Tools/visual_targets.json, the same file the render gate uses, and reports one
line per shot with the model's verdict and reasoning.

The result is not good news and is committed as such: 0 of 9 shots judged 'yes'. Shot 7 shows
the scene itself wrong - a disassembled car floating over a blank surface with no street or
buildings in frame - and the others fail for missing detail rather than for a broken frame.
visual_loop --verify still reports 9/9, because it only measures exposure and size.

A warning now sits at the top of Screenshots/README.md: treat every image there as stale until
the scene is fixed. The cause is not yet identified - the map builder's recent changes are 15
lines of vehicle staging, which does not explain a missing street.
2026-11-09 16:55:59 +01:00
zwitschi 58095b6a33 test(vehicle): wheels are created with the physics state, and still come out zero
Two measurement errors found and corrected before drawing any conclusion:

  - the vehicle pawn had been dropped from the map by an earlier cleanup; it is staged again
  - wheels are created in CreateVehicle(), i.e. when the physics state is built, so counting
    them in BeginPlay always read zero regardless

Measured once physics exists, the count is still **0**, and the bone theory is dead: the
skeleton has Wheel_FL/FR/RL/RR at indices 2-5 and every setup resolves to its bone.

CreateWheels() runs only inside CreateVehicle() when PVehicleOutput is valid, so a zero count
means the physics state never produced a vehicle - the problem is upstream of the wheel config,
in the physics asset or the way the skeletal mesh binds it, not in the wheel setups.

Also logged at the settle check: the pawn's own downward trace reports ground at Z 152 while
the visible car's panels stand on a road of 366, so the pawn and the visible car may not be in
the same place at all - worth checking before anything else.
2026-11-09 16:53:25 +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 cc57ad6c02 feat(vehicle): front and rear wheel classes, so Chaos has parameters to build from
Chaos reads steering and drive flags from the wheel object a setup points at, and the first
attempt pointed every setup at the base UChaosVehicleWheel. Dedicated front and rear classes
carry the modelled dimensions (radius 33 cm, width 22 cm), steering on the front axle, drive
on the rear, and the suspension travel - which is also how Unreal's own vehicle template is
organised.

Compiles clean. Not yet verified in a run: the drive test did not log the vehicle this time,
so whether the wheel count changes from 0 is still unproven and recorded as such rather than
claimed.
2026-11-09 16:32:25 +01:00
zwitschi 98eae6f3e9 docs(architecture): make section pages canonical 2026-11-09 16:30:29 +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 046e2c8b9e docs(review): the Blender-vs-in-game comparison lives in Screenshots/README.md
Merged COMPARISON.md into the review README as a generated section between
<!-- blender-comparison:begin/end --> markers, so there is one place to look and one place
to sign off. Tools/build_comparison_md.py rewrites only that block - the hand-written
sections around it are untouched - and formats the file with prettier afterwards, because a
generated table is never byte-identical to prettier's output and the repo gates on
--check.

Run it after every capture or Blender render; it is the routine that keeps the page current
with new images rather than a one-off export.
2026-11-09 16:10:47 +01:00
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
337 changed files with 7414 additions and 1507 deletions

No files matched your search

+5 -1
View File
@@ -56,7 +56,9 @@ Thumbs.db
/Docs/data-inventory.md
/Docs/environment.md
/Docs/reference-notes-hollywood.md
/.agents/skills/blender-skills/
# leave skills out of repo
/.agents/skills/
# Blender/Blosm working caches
/Art/blender/.cache/
@@ -69,3 +71,5 @@ Thumbs.db
# Unreal per-project build artefacts (FileOpenOrder logs and the like)
Build/
dsh-session-*
tmp/
+90 -12
View File
@@ -55,7 +55,7 @@ Semantic Versioning: `MAJOR.MINOR.PATCH`.
The two files have separate roles:
- **[TODO.md](TODO.md)** is the single, central task list for all agents: checkbox items, progress marks, and active-section ownership. Put all planned actionable work and follow-ups there. Do not create parallel task lists elsewhere.
- **[TODO.md](TODO.md)** is the single, central task list for all agents: checkbox items, progress marks, and active-section ownership. Put all planned actionable work and follow-ups there. Do not create parallel task lists elsewhere. Any notes or observations should be recorded in [MEMORY.md](#memory).
- **[ROADMAP.md](ROADMAP.md)** holds the development plan: milestone goals, scope, decisions, and release gates for each version. It is a planning document and contains **no checkbox items**. Read it for future direction; record scope changes there as prose, and record the resulting tasks in `TODO.md`.
Rules for `TODO.md`:
@@ -126,7 +126,7 @@ Rules that keep tools reusable and reviewable:
- **Result files for Unreal.** Unreal commandlets swallow `print()`; write the outcome to `/tmp/gigline/<tool>.txt` and read that instead of trusting the exit code alone.
- **Absolute paths into sandboxes.** Blender Flatpak (`LD_LIBRARY_PATH="$B/blender/lib"`) and Unreal do not share the repo's working directory; pass absolute paths.
- **Clean worktree for generators.** Tools that rewrite committed assets (site/street/hero builders, importers, map builders) run from a clean worktree; inspect the LFS diff before committing. Note that `.blend` and `.umap` check out read-only — `chmod u+w` first or the save silently fails.
- **Network and paid-API tools are human-triggered.** `Tools/commons_fetch.py`, `Tools/page_image_fetch.py`, `Tools/judge_screenshot.py` (OpenRouter) and anything similar never sit in a gate or CI. Handle HTTP 429 with back-off rather than retrying immediately.
- **Network and paid-API tools are human-triggered.** `Tools/gen/commons_fetch.py`, `Tools/gen/page_image_fetch.py`, `Tools/review/judge_screenshot.py` (OpenRouter) and anything similar never sit in a gate or CI. Handle HTTP 429 with back-off rather than retrying immediately.
- **Measure, don't assert.** Verification tools print the sampled numbers — counts, holes, Z ranges, worst step and its location, frame-time percentiles — so a claim in `TODO.md`, `CHANGELOG.md` or `Docs/` can be re-checked by re-running the command.
**Document every tool.** A new or changed tool gets a row in the inventory table of the README beside it: [`Tools/README.md`](Tools/README.md), [`Geo/README.md`](Geo/README.md), or [`Art/blender/README.md`](Art/blender/README.md). What it does, its arguments, and a typical command. A tool without a README row is unfinished.
@@ -141,7 +141,7 @@ Keep the tools standard-library-only where practical; if a third-party import is
## Architecture documentation
[`Docs/Architecture/`](Docs/Architecture/) is the arc42 documentation: twelve numbered section files, `about-arc42.md`, and the combined [`architecture.md`](Docs/Architecture/architecture.md). **Markdown is canonical** — the `.adoc` sources were removed on 2026-11-08.
[`Docs/Architecture/`](Docs/Architecture/) is the arc42 documentation: [`README.md`](Docs/Architecture/README.md) provides the overview and links to twelve numbered section files; `about-arc42.md` records the template attribution. **Markdown is canonical** — the `.adoc` sources were removed on 2026-11-08.
Keep it current **in the same change as the implementation**, not in a later documentation pass. When work alters any of the following, edit the matching section:
@@ -160,7 +160,7 @@ Rules:
- **Describe what exists.** No aspirational architecture: anything planned belongs in `ROADMAP.md` and `TODO.md`. Every structural claim names the file, script or flag that implements it.
- **ADRs are append-only.** Never rewrite an accepted decision; supersede it with a new ADR that says what it replaces and why.
- **Keep the combined document in sync.** `architecture.md` mirrors the section files; when a section changes, update the combined document too.
- **Keep the index current.** `Docs/Architecture/README.md` links to the canonical section pages; update it when the structure or section list changes.
- **Images** live in `Docs/Architecture/images/` and are referenced, not duplicated.
- **Verify:** all Markdown must pass `prettier --check` (config in `.prettierrc`), and a command you quote must still run.
@@ -181,22 +181,100 @@ Always use a consistent scale test, named export presets, and repeatable import
The 93 upstream Blender skill packs (MIT, blender-skills 1.3.0) live once, in the **common** agent folder `~/.pi/agent/skills/`, together with the shared `references/` notes and the upstream `LICENSE`. They are agent instructions, not add-ons, and nothing in the pipeline depends on them.
Only the project-specific pack stays in the repository: `.agents/skills/blender-skills/blender-director/` — the pipeline orchestrator, which names this project's tools and stages. It refers to the shared notes by absolute path (`~/.pi/agent/skills/references/…`).
The project-specific pack, `.agents/skills/blender-skills/blender-director/` — the pipeline orchestrator, which names this project's tools and stages — also stays **out of the repository**: `/.agents/skills/` is gitignored. It refers to the shared notes by absolute path (`~/.pi/agent/skills/references/…`).
Do not copy the common packs into the repository: a second copy under a different root is a name collision, and Pi keeps the first one it finds and warns on every start. Add a pack to `~/.pi/agent/skills/<pack>/SKILL.md` (one level down, flat) and it is picked up everywhere; `references/` sits beside them at `~/.pi/agent/skills/references/`.
**All agents must consult the relevant skill packs before starting a task in that discipline.** Each patch section in `TODO.md` lists its suggested skills; the general mapping is:
**Entry point:** `blender-director` — the orchestrator picks the specialist packs in pipeline order and matches a reference photograph before detail work. Start any multi-step Blender task there.
- **Modeling / procedural assets** — `blender-modeler`, `procedural-modeling`, `geometry-nodes`, `hard-surface`.
- **Façades and architecture** — `archviz`, `historical-worlds` (1920s Hollywood Blvd direction), `realistic-style`.
**Pack index.** Every pack available to this project, grouped by discipline. The 93 common packs plus the project's `blender-director`; `unity-export` and `godot-export` are installed for other projects and unused here, since the project ships Unreal only.
| Group | Packs |
| --- | --- |
| Pipeline entry point | `blender-director` (project pack) |
| Core modelling | `blender-modeler`, `hard-surface`, `prop-artist`, `vehicle-artist`, `character-artist`, `creature-artist`, `sculpting`, `retopology`, `geometry-nodes`, `procedural-modeling` |
| Environment and architecture | `environment-artist`, `archviz`, `set-dressing`, `vegetation-artist`, `scene-assembly`, `collision-proxy` |
| Characters and animation | `character-archetypes`, `rigging`, `animation`, `cloth-sim`, `hair-groom` |
| Materials, light and render | `materials`, `texture-workflow`, `lookdev`, `lighting`, `rendering`, `compositing`, `camera-cinematography` |
| UVs, export and optimisation | `uv-workflow`, `export-pipeline`, `unreal-export`, `asset-optimization`, `lod-pipeline` |
| Physics and VFX | `physics-sim`, `vfx-fx` |
| Review and QA | `qa-review`, plus the `references/` notes below |
| Project direction | `realistic-style`, `historical-worlds` |
| World packs | `apocalypse-worlds`, `biome-worlds`, `fantasy-worlds`, `sci-fi-punk-worlds` |
| Genre art direction | `genre-action-combat`, `genre-card-party-idle`, `genre-metroidvania-roguelike`, `genre-narrative-vn`, `genre-open-world-sandbox`, `genre-puzzle-platformer`, `genre-racing-sports`, `genre-rpg`, `genre-shooter`, `genre-soulslike`, `genre-stealth`, `genre-strategy-sim`, `genre-survival` |
| Mood art direction | `brutalist-mood`, `cozy-wholesome-mood`, `dark-gritty-mood`, `dream-surreal-mood`, `neon-retrofuturism`, `noir-style` |
| Style art direction | `analog-found-footage-horror`, `anime-style`, `body-horror-style`, `cartoon-style`, `chibi-style`, `comic-book-style`, `cosmic-eldritch-horror`, `dream-weirdcore-style`, `folk-horror-style`, `frutiger-aero-style`, `hand-painted-style`, `hd-2d-style`, `horror-style`, `indie-horror-aesthetics`, `isometric-style`, `liminal-space-style`, `lowpoly-style`, `manga-style`, `mascot-puppet-horror`, `minimalist-style`, `painterly-style`, `pixel-art-style`, `psx-horror-style`, `retro-16bit-style`, `retro-8bit-style`, `stop-motion-craft-style`, `stylized-style`, `vector-style`, `visual-console-eras`, `voxel-style` |
| Other engines (unused) | `unity-export`, `godot-export` |
**All agents must consult the relevant skill packs before starting a task in that discipline.** Each task section in `TODO.md` names its suggested skills; the general mapping is:
- **Modeling / procedural assets** — `blender-modeler`, `procedural-modeling`, `geometry-nodes`, `hard-surface`, `prop-artist`, `sculpting`, `retopology`.
- **Vehicles** — `vehicle-artist` with `hard-surface`, `collision-proxy`, `lod-pipeline` (the `0.3.0` car).
- **Façades and architecture** — `archviz`, `historical-worlds` (1920s Hollywood Blvd direction), `realistic-style`, `environment-artist`.
- **Characters and animation** — `character-artist`, `rigging`, `animation`, `cloth-sim`, `hair-groom` (the MPFB `AGigLineNPCPerson` prefab; `character-archetypes` for crowd variety).
- **Materials and textures** — `materials`, `texture-workflow`, `lookdev`.
- **UVs and export** — `uv-workflow`, `export-pipeline`, `unreal-export` (verify against the `ue_metric_v1` / `ue_character_v1` presets in `Tools/`).
- **UVs and export** — `uv-workflow`, `export-pipeline`, `unreal-export` (verify against the `ue_metric_v1` / `ue_character_v1` presets in `Tools/`), `asset-optimization`, `lod-pipeline`.
- **Collision** — `collision-proxy` (UCX/UHX naming, hulls, simple colliders).
- **Scene assembly / optimization** — `scene-assembly`, `asset-optimization`, `lod-pipeline`.
- **Lighting and rendering** — `lighting`, `rendering`, `lookdev`, `compositing`, `camera-cinematography`.
- **Blender-side physics and VFX** — `physics-sim`, `vfx-fx`.
- **Review and QA** — `qa-review`, `references` (reference-image-match and visual-match checklists), `camera-cinematography` (matching reference camera angles).
**Reference notes.** `~/.pi/agent/skills/references/` sits beside the packs and holds the checklists they cite:
- `asset-pipeline.md` — the stage order every asset follows.
- `reference-image-match.md` and `reference-analysis-template.md` — match the camera and write the analysis before micro-detail when a photograph drives the asset.
- `visual-match-checklist.md` and `validation-checklist.md` — the two gates before an asset is called done.
- `naming-conventions.md` and `polycount-budgets.md` — names and budgets to hold to.
- `mcp-integration.md` and `mcp-tools.md` — the packs' MCP tooling, **not used here** (see the caveat below).
**Caveat:** the skill packs assume MCP-driven interactive Blender. This project runs Blender headless via scripts in `Tools/` — use the skills as method guides (checklists, naming conventions, proportions, validation steps), not for their MCP tooling. The headless scripts remain the execution path.
## Unreal Engine skills
The 31 Unreal Engine skill packs (MIT, [quodsoler/unreal-engine-skills](https://github.com/quodsoler/unreal-engine-skills) `f3742d7`) live locally at `.agents/skills/ue-*/` (gitignored, not committed) and target **UE 5.8 C++**. Each carries API examples verified against the 5.8 headers plus a deprecated-API table that maps old calls to the 5.8 forms. They are agent instructions, not add-ons.
`.agents/skills/ue-project-context/` is the entry point: it writes `.agents/ue-project-context.md` (module names, conventions, enabled plugins), and every other UE skill reads that file first. Refresh it when the project changes — new module, new plugin, engine bump — before consulting the rest.
The general mapping to this project's work:
- **Build and modules** — `ue-module-build-system` (Build.cs, unresolved externals, include order, plugin enablement).
- **Reflection and core types** — `ue-cpp-foundations` (UCLASS/UPROPERTY/UFUNCTION, TObjectPtr, delegates), `ue-blueprint-cpp-interop`.
- **Actor/component lifecycle** — `ue-actor-component-architecture` (spawning, attachment, tick groups, EndPlay).
- **Player and pawns** — `ue-character-movement` (the character's walking), `ue-gameplay-framework` (GameMode/Pawn/Controller), `ue-input-system` (Enhanced Input). The Chaos vehicle is the `ChaosVehiclesPlugin` — see the vehicle notes in `TODO.md`, not a skill pack.
- **Physics and collision** — `ue-physics-collision` (channels, traces, overlaps, profiles).
- **Materials and rendering** — `ue-materials-rendering`, `ue-niagara-effects`.
- **World and streaming** — `ue-world-level-streaming` (World Partition, level streaming, travel), `ue-procedural-generation` (PCG).
- **Data, save and serialization** — `ue-data-assets-tables`, `ue-serialization-savegames`.
- **Networking** — `ue-networking-replication` (replication, RPCs, relevance).
- **UI** — `ue-ui-umg-slate` (UMG, Slate, Common UI).
- **Debugging, profiling, tests** — `ue-testing-debugging`, `ue-async-threading`.
- **Editor tooling** — `ue-editor-tools` (detail customizations, menus, factories).
- **Animation** — `ue-animation-system` (AnimInstance, montages, notifies, root motion).
- **As-needed** — `ue-ai-navigation`, `ue-audio-system`, `ue-game-features`, `ue-gameplay-abilities` (GAS), `ue-gameplay-cameras`, `ue-gameplay-tags-messaging`, `ue-mass-entity`, `ue-mover`, `ue-sequencer-cinematics`, `ue-state-trees`.
## C++ skill
One router skill, `cpp` (MIT, [DiamonDinoia/skill-cpp](https://github.com/DiamonDinoia/skill-cpp) `f2617fb`), lives at `.agents/skills/cpp/` and covers general C++11 through C++23 against the C++ Core Guidelines. It detects the project language standard from the build system (**C++20** for this project, from the UBT compile flags), then loads the matching standard reference plus the topic references the task touches. Load it for any C++ work that is not Unreal-specific; the UE skills supersede it for engine API questions.
## Skill roots and provenance
Every skill pack is agent instruction, never game code or an add-on, and none is committed: `/.agents/skills/` is gitignored. The runtime discovers packs from these roots:
| Root | Packs | Source |
| --- | --- | --- |
| `~/.pi/agent/skills/<pack>/SKILL.md` | 93 Blender packs, flat, one level down | [blender-skills](https://github.com/arjun988/blender-skills) 1.3.0, MIT |
| `~/.pi/agent/skills/references/` | shared notes beside the packs, not a pack | same |
| `.agents/skills/blender-skills/blender-director/` | 1 project orchestrator | project-specific |
| `.agents/skills/ue-*/` | 31 Unreal Engine 5.8 C++ packs | [quodsoler/unreal-engine-skills](https://github.com/quodsoler/unreal-engine-skills) `f3742d7`, MIT |
| `.agents/skills/cpp/` | 1 C++ router | [DiamonDinoia/skill-cpp](https://github.com/DiamonDinoia/skill-cpp) `f2617fb`, MIT |
| `~/.agents/skills/` | user-level packs shared with other projects | local |
- `.agents/skills/skills.manifest.json` records every installed source, commit, target and local patch, with `LICENSE-unreal-engine-skills.txt` and `LICENSE-skill-cpp.txt` beside it. Keep the manifest current when a pack is installed, upgraded or removed.
- The upstream Blender clone sits at `~/.agents/skills/blender-skills/` (`git remote https://github.com/arjun988/blender-skills.git`, 94 packs under `.claude/skills/`). `~/.pi/agent/skills/` is the installed copy the runtime actually reads and holds 93 of them; `blender-director` is kept project-local because it names this project's tools and stages.
- Because the packs are gitignored, a fresh checkout has none of them: every task must stay doable from the scripts in `Tools/`, `Geo/tools/` and `Art/blender/`, with the packs only guiding method.
- User-level packs: `de-ai-writing` strips AI-writing tells from `Docs/`, `CHANGELOG.md` and `MEMORY.md` prose; `hf-cli` (Hugging Face Hub CLI) and `reverse-engineer-anything` (REA for shipped binaries and packaged builds) are not part of this pipeline and are loaded only when the task is actually about them.
## Licensing & Distribution
- Publish project-owned code under a chosen open-source license.
@@ -212,10 +290,10 @@ Do not copy the common packs into the repository: a second copy under a differen
Any change that affects the game's appearance — meshes, materials, lighting, levels, character, camera, post-processing — must be rendered for human review:
1. Run `Tools/render_screenshots.sh` (rebuilds `Game/GigLine/Content/Test/ReviewMap.umap` from current content, runs the game on it, captures the fixed camera list, copies the PNGs into `Screenshots/`).
2. Optional pre-check with a vision model: `Tools/judge_screenshot.py --image <shot.png> --expect "<what it should show>"` (or `--shots` to run every shot in the README) flags likely-missing content before a human looks. It reads `OPENROUTER_API_KEY` from `.env` (ignored). Small helper only — it never replaces the human verdict.
1. Run `Tools/review/render_screenshots.sh` (rebuilds `Game/GigLine/Content/Test/ReviewMap.umap` from current content, runs the game on it, captures the fixed camera list, copies the PNGs into `Screenshots/`).
2. Optional pre-check with a vision model: `Tools/review/judge_screenshot.py --image <shot.png> --expect "<what it should show>"` (or `--shots` to run every shot in the README) flags likely-missing content before a human looks. It reads `OPENROUTER_API_KEY` from `.env` (ignored). Small helper only — it never replaces the human verdict.
3. Update `Screenshots/README.md` **in the same change as any visual work** — every change that affects appearance, not only new shots: the image is **embedded inline** (`![alt](demo/<shot>.png)`) with its view, expected content, verification and approval state, plus one row in the status table at the top. The review happens in the file; a path alone is not a review, and a shot whose README section is stale is a defect.
4. The PNGs **are committed** so reviewers see exactly what the build rendered (`.gitignore` does not exclude them). Keep them in sync with this README: drop retired shots in the same change.
5. Wait for the user's approval before treating a visual change as done; record approval by ticking both the shot's section and its row in the status table in `Screenshots/README.md`.
New content that needs a different camera angle gets a new entry in the shot list in `Tools/ue_build_review_map.py`.
New content that needs a different camera angle gets a new entry in the shot list in `Tools/unreal/ue_build_review_map.py`.
+1 -1
View File
@@ -153,7 +153,7 @@ The extract is complete for the block but not for every relation inside its bbox
## Scale test and the Unreal frame (2026-11-07)
`Art/blender/scaletest/scale-test-2026-11-07.blend` — three 1 m cubes at origin, +10 m east, +10 m north, plus one red test material. Regenerate with `Tools/scale_test_export.py`; import with `Tools/ue_scale_test_import.py`. Both are the named-preset templates for production assets.
`Art/blender/scaletest/scale-test-2026-11-07.blend` — three 1 m cubes at origin, +10 m east, +10 m north, plus one red test material. Regenerate with `Tools/blender/scale_test_export.py`; import with `Tools/unreal/ue_scale_test_import.py`. Both are the named-preset templates for production assets.
Export preset **`ue_metric_v1`** (Blender FBX): `apply_unit_scale`, `apply_scale_options='FBX_SCALE_NONE'`, `axis_forward='-Y'`, `axis_up='Z'`, `use_space_transform`, no bake-space-transform. Import preset **`ue_metric_v1`** (UE `FbxImportUI`): static mesh, import materials, uniform scale 1.0, no unit conversion, no force-front-X.
Binary file not shown.
+6 -6
View File
@@ -10,12 +10,12 @@
"max": [
13.08,
0.13,
14.5
14.7
],
"size": [
26.16,
12.63,
14.5
14.7
]
},
"parts": [
@@ -122,10 +122,10 @@
"object": "Hero_Medallion_x2",
"dims_m": [
1.1,
0.04,
0.2,
1.1
],
"evidence": "photographs: two stone discs beside the motto, mounted on the parapet face"
"evidence": "photographs: two round reliefs beside the motto - dark rim, raised light field, dark boss"
},
{
"object": "Hero_Stylobate",
@@ -166,9 +166,9 @@
{
"object": "Hero_Banner_x3",
"dims_m": [
1.55,
1.35,
0.06,
5.4
6.2
],
"evidence": "photographs: JIMMY KIMMEL LIVE banners on the column line"
},
+82
View File
@@ -22,6 +22,85 @@ import fbx_export_preset as fbx_preset # noqa: E402
MESHES = ("Terrain_DEM", "Road_Hollywood", "Sidewalk_South", "Sidewalk_North",
"Kerb_South", "Kerb_North", "Crossing_Markings")
# Ground surfaces are exported in slices. One 279 m slab cannot report an honest height:
# its bounding box is the highest point anywhere along the street, which is 2.2 m above the
# surface at the player's position, and that misled every placement decision in the demo.
# A 20 m slice has a bounding box that means something where it stands.
# The road is sliced for the same reason as the sidewalks: a 279 m strip imports with a single
# convex hull whose top is the strip's MAXIMUM height, so a downward trace anywhere on it lands on
# that hull - a flat lid 2 m above the visible surface. A 20 m slice has a hull that follows the
# surface it belongs to.
SEGMENTED = ("Sidewalk_North", "Sidewalk_South", "Kerb_North", "Kerb_South",
"Road_Hollywood")
SEGMENT_M = 20.0
def _ucx_box(name, x0, x1, y0, y1, z0, z1):
"""A thin convex box for a ground slice, named for Unreal's UCX convention."""
thickness = max(0.10, min(0.30, (z1 - z0) * 0.25))
cx, cy = (x0 + x1) * 0.5, (y0 + y1) * 0.5
cz = z1 - thickness * 0.5
bpy.ops.mesh.primitive_cube_add(size=1.0, location=(cx, cy, cz))
hull = bpy.context.active_object
hull.name = f"UCX_{name}_01"
hull.scale = ((x1 - x0) * 0.99, (y1 - y0) * 0.99, thickness)
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
return hull
def _segment(obj, name, out_dir, length_m):
"""Export `obj` as slices of `length_m` along X, each with its own honest bbox."""
xs = [(obj.matrix_world @ v.co).x for v in obj.data.vertices]
x0, x1 = min(xs), max(xs)
count = max(1, int(round((x1 - x0) / length_m)))
step = (x1 - x0) / count
bpy.context.view_layer.objects.active = obj
for index in range(count):
a = x0 + index * step
b = a + step
bpy.ops.object.select_all(action="DESELECT")
obj.select_set(True)
bpy.ops.object.duplicate()
piece = bpy.context.active_object
bpy.context.view_layer.objects.active = piece
bpy.ops.object.mode_set(mode="EDIT")
# Two cuts, each with the normal pointing at the half to KEEP and clear_inner=True:
# normal +X at a -> keeps x > a
# normal -X at b -> keeps x < b
# The first version used clear_outer for the far cut and produced nested slices that
# each still spanned to the end of the street, which made every slice's bbox top out
# at the same far-away maximum.
bpy.ops.mesh.select_all(action="SELECT")
bpy.ops.mesh.bisect(plane_co=(a, 0.0, 0.0), plane_no=(1.0, 0.0, 0.0),
clear_inner=True)
bpy.ops.mesh.select_all(action="SELECT")
bpy.ops.mesh.bisect(plane_co=(b, 0.0, 0.0), plane_no=(-1.0, 0.0, 0.0),
clear_inner=True)
bpy.ops.object.mode_set(mode="OBJECT")
piece.name = f"{name}_{index:02d}"
zs = [(piece.matrix_world @ v.co).z for v in piece.data.vertices]
# Explicit UCX box per slice. Neither the triangulated complex collision nor
# complex-as-simple makes these meshes block in Unreal, so the collision is authored
# here: one convex box per 20 m slice, thin, with its top at the slice's surface.
xs_w = [(piece.matrix_world @ v.co).x for v in piece.data.vertices]
ys_w = [(piece.matrix_world @ v.co).y for v in piece.data.vertices]
hull = _ucx_box(piece.name, min(xs_w), max(xs_w), min(ys_w), max(ys_w),
min(zs), max(zs))
path = os.path.join(out_dir, f"{piece.name}.fbx")
bpy.ops.object.select_all(action="DESELECT")
piece.select_set(True)
hull.select_set(True)
bpy.context.view_layer.objects.active = piece
fbx_preset.export_static(path, selection=True)
bpy.data.objects.remove(hull, do_unlink=True)
print(f"[site] {piece.name:22} x {a:+.1f}..{b:+.1f} m "
f"z {min(zs):+.2f}..{max(zs):+.2f} m -> {path}")
bpy.data.objects.remove(piece, do_unlink=True)
# The context loses its active object when the duplicate goes; restore the source so
# the next iteration's operators have something to act on.
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
def main():
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
@@ -62,6 +141,9 @@ def main():
me.update()
_ = bpy.context.view_layer
bpy.context.view_layer.objects.active = obj
if name in SEGMENTED:
_segment(obj, name, args.out, SEGMENT_M)
continue
path = os.path.join(args.out, name + ".fbx")
fbx_preset.export_static(path, selection=True)
if mirrored:
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.
@@ -0,0 +1,106 @@
{
"prompt": "top-down orthographic game texture, flat even lighting, no shadows, no AO, no vignette, PBR albedo, material scan, neutral colours, smooth pale limestone marble cladding, large thin-jointed ashlar blocks, very low contrast, desaturated off-white concrete tone, fine even grain, flat even lighting, no shadows, no vignette, bright clean PBR albedo",
"profile": "albedobase",
"size": [
1024,
1024
],
"seed": 55,
"settings": {
"steps": 30,
"cfg": 6.0,
"sampler": "dpmpp_2m",
"scheduler": "karras"
},
"licence": "AlbedoBase XL v3.1 (SDXL) - game albedo, commercial use with credit",
"tool": "Tools/gen/comfyui_generate.py",
"generated_utc": "2026-11-10T13:15:21Z",
"workflow": {
"1": {
"class_type": "CheckpointLoaderSimple",
"inputs": {
"ckpt_name": "albedobaseXL_v31Large.safetensors"
}
},
"4": {
"class_type": "CLIPTextEncode",
"inputs": {
"text": "top-down orthographic game texture, flat even lighting, no shadows, no AO, no vignette, PBR albedo, material scan, neutral colours, smooth pale limestone marble cladding, large thin-jointed ashlar blocks, very low contrast, desaturated off-white concrete tone, fine even grain, flat even lighting, no shadows, no vignette, bright clean PBR albedo",
"clip": [
"1",
1
]
}
},
"5": {
"class_type": "CLIPTextEncode",
"inputs": {
"text": "shadows, perspective, vignette, dramatic lighting, blurry, low quality, text, watermark",
"clip": [
"1",
1
]
}
},
"6": {
"class_type": "EmptyLatentImage",
"inputs": {
"width": 1024,
"height": 1024,
"batch_size": 1
}
},
"7": {
"class_type": "KSampler",
"inputs": {
"seed": 55,
"steps": 30,
"cfg": 6.0,
"sampler_name": "dpmpp_2m",
"scheduler": "karras",
"denoise": 1.0,
"model": [
"1",
0
],
"positive": [
"4",
0
],
"negative": [
"5",
0
],
"latent_image": [
"6",
0
]
}
},
"8": {
"class_type": "VAEDecode",
"inputs": {
"samples": [
"7",
0
],
"vae": [
"1",
2
]
}
},
"9": {
"class_type": "SaveImage",
"inputs": {
"images": [
"8",
0
],
"filename_prefix": "texture_albedobase"
}
}
},
"comfyui_prompt_id": "aa517bfa-9ad0-4adb-b01a-39e50b1f8f23",
"note": "generated locally; record the model licence in THIRD_PARTY_NOTICES.md before shipping"
}
@@ -0,0 +1,120 @@
{
"prompt": "top-down orthographic game texture, flat even lighting, no shadows, no AO, no vignette, PBR albedo, material scan, neutral colours, smooth pale limestone marble cladding, large thin-jointed ashlar blocks, very low contrast, desaturated off-white concrete tone, fine even grain, flat even lighting, no shadows, no vignette, bright clean PBR albedo",
"profile": "krea2",
"size": [
1024,
1024
],
"seed": 55,
"settings": {
"steps": 8,
"cfg": 1.0,
"sampler": "euler",
"scheduler": "simple"
},
"licence": "Krea-2 Turbo NVFP4 - fast stochastic detail (stone, bark, foliage)",
"tool": "Tools/gen/comfyui_generate.py",
"generated_utc": "2026-11-10T13:15:33Z",
"workflow": {
"1": {
"class_type": "UNETLoader",
"inputs": {
"unet_name": "krea2_turbo_nvfp4.safetensors",
"weight_dtype": "default"
}
},
"2": {
"class_type": "CLIPLoader",
"inputs": {
"clip_name": "qwen3vl_4b_int8_convrot.safetensors",
"type": "krea2",
"device": "default"
}
},
"3": {
"class_type": "VAELoader",
"inputs": {
"vae_name": "texture_fix_vae_for_qwen_image_1_and_krea_2_bf16.safetensors"
}
},
"4": {
"class_type": "CLIPTextEncode",
"inputs": {
"text": "top-down orthographic game texture, flat even lighting, no shadows, no AO, no vignette, PBR albedo, material scan, neutral colours, smooth pale limestone marble cladding, large thin-jointed ashlar blocks, very low contrast, desaturated off-white concrete tone, fine even grain, flat even lighting, no shadows, no vignette, bright clean PBR albedo",
"clip": [
"2",
0
]
}
},
"5": {
"class_type": "ConditioningZeroOut",
"inputs": {
"conditioning": [
"4",
0
]
}
},
"6": {
"class_type": "EmptyLatentImage",
"inputs": {
"width": 1024,
"height": 1024,
"batch_size": 1
}
},
"7": {
"class_type": "KSampler",
"inputs": {
"seed": 55,
"steps": 8,
"cfg": 1.0,
"sampler_name": "euler",
"scheduler": "simple",
"denoise": 1.0,
"model": [
"1",
0
],
"positive": [
"4",
0
],
"negative": [
"5",
0
],
"latent_image": [
"6",
0
]
}
},
"8": {
"class_type": "VAEDecode",
"inputs": {
"samples": [
"7",
0
],
"vae": [
"3",
0
]
}
},
"9": {
"class_type": "SaveImage",
"inputs": {
"images": [
"8",
0
],
"filename_prefix": "texture_krea2"
}
}
},
"comfyui_prompt_id": "946b5d1a-7198-43ac-8f71-afa800416803",
"note": "generated locally; record the model licence in THIRD_PARTY_NOTICES.md before shipping"
}
Binary file not shown.
+21 -21
View File
@@ -38,12 +38,12 @@ The character becomes a character: clothed, haired, and measured against a gener
### Added
- **Clothing** — navy tee, dark jeans and off-white shoes as three separate skinned meshes (`Tools/character_clothing_build.py`), built from the body's own faces per the `character-artist` workflow: duplicate faces, solidify for cloth thickness, one mesh per piece. Each imports with its own single material slot and is attached as a leader-pose component.
- **Hair cards** (`Tools/character_hair_build.py`) — a scalp cap plus 26 cards in three overlapping rows, quads only, every vertex weighted to `head`. Cards are oriented from the scalp normal at their anchor, so the cut follows the skull instead of standing out as spikes.
- **Character blueprints** (`Tools/comfyui_character_blueprint.py`) — front, side, back and head views generated by the local ComfyUI instance from one description, with fixed seeds and a JSON record of prompts and prompt ids.
- **Blueprint comparison** (`Tools/character_match.py`) — figure aspect, fill and coverage deltas, mean CIE76 and dominant-palette distance, plus a side-by-side montage. The deltas stay separate on purpose: one similarity number between a photoreal sheet and a low-detail body would mean nothing.
- **ComfyUI driver** (`Tools/comfyui_generate.py`) and **procedural textures** (`Tools/generate_character_textures.py`), each writing a provenance sidecar.
- **Material builder** (`Tools/ue_character_materials.py`) — `M_Character_Skin` and `M_Character_Hair_Mat` built from scratch, because `unreal.MaterialEditingLibrary` segfaults in a headless commandlet when it rebuilds an existing graph.
- **Clothing** — navy tee, dark jeans and off-white shoes as three separate skinned meshes (`Tools/blender/character_clothing_build.py`), built from the body's own faces per the `character-artist` workflow: duplicate faces, solidify for cloth thickness, one mesh per piece. Each imports with its own single material slot and is attached as a leader-pose component.
- **Hair cards** (`Tools/blender/character_hair_build.py`) — a scalp cap plus 26 cards in three overlapping rows, quads only, every vertex weighted to `head`. Cards are oriented from the scalp normal at their anchor, so the cut follows the skull instead of standing out as spikes.
- **Character blueprints** (`Tools/blender/comfyui_character_blueprint.py`) — front, side, back and head views generated by the local ComfyUI instance from one description, with fixed seeds and a JSON record of prompts and prompt ids.
- **Blueprint comparison** (`Tools/blender/character_match.py`) — figure aspect, fill and coverage deltas, mean CIE76 and dominant-palette distance, plus a side-by-side montage. The deltas stay separate on purpose: one similarity number between a photoreal sheet and a low-detail body would mean nothing.
- **ComfyUI driver** (`Tools/gen/comfyui_generate.py`) and **procedural textures** (`Tools/gen/generate_character_textures.py`), each writing a provenance sidecar.
- **Material builder** (`Tools/unreal/ue_character_materials.py`) — `M_Character_Skin` and `M_Character_Hair_Mat` built from scratch, because `unreal.MaterialEditingLibrary` segfaults in a headless commandlet when it rebuilds an existing graph.
### Changed
@@ -92,7 +92,7 @@ Third-person body. The pawn gains a visible, animated body and a third-person ca
### Added
- **`Tools/character_anim_build.py`**: `GigLine_Idle` (60 frames) and `GigLine_Walk` (30 frames) at 30 fps, authored as parametric cycles so the loop closes by construction. Limb axes were **measured** on the rig — a probe rotated `upperleg01.L` by 0.5 rad on each axis and read `foot.L`, which showed X swings the limb fore/aft while Y twists and Z abducts.
- **`Tools/blender/character_anim_build.py`**: `GigLine_Idle` (60 frames) and `GigLine_Walk` (30 frames) at 30 fps, authored as parametric cycles so the loop closes by construction. Limb axes were **measured** on the rig — a probe rotated `upperleg01.L` by 0.5 rad on each axis and read `foot.L`, which showed X swings the limb fore/aft while Y twists and Z abducts.
- **Third-person rig** (`ADR-9`): a visible skeletal body, a `USpringArmComponent` with collision testing, and a **V** toggle. One pawn carries both rigs; first person stays the shipped default.
- **Walk capture harness**: a per-shot `ShotFollow` mode that re-places the review camera every frame in the pawn's frame, plus `SetReviewWalk` / `ReturnToStartForReview`, so a stride is captured deterministically and the fixed-camera shots still find the pawn at spawn.
- **Review shots 6, 7 and 8** — third-person back, front and mid-stride — embedded in `Screenshots/README.md`.
@@ -130,8 +130,8 @@ Retail frontage. The two retail buildings beside the hero are modelled from thei
### Added
- **`Tools/retail_frontage_build.py`**: 6800 Lee Drug and 6806–6820 built from the one usable reference (`Art/reference/retail-neighbours/`, Gb321, CC BY-SA 4.0). Lee Drug gets a bronze cornice, storefront glazing with mullions, framed portrait panels, a painted lettering band, two-tier parapet with reveals and a fluted corner tower; 6806–6820 gets a burgundy fascia, brown pilasters, white-framed windows with security bars and its own banded parapet. Built in world coordinates from the blockout bounds (Lee Drug X −33.20…−14.37, face Y −14.63, H 12.10 m; 6806–6820 X −66.08…−33.07), joined to one mesh per building with a JSON part report.
- **`Tools/ue_import_retail_detail.py`**: imports the two meshes with complex-as-simple collision — trim decorates a solid the player already collides with and must not add snag points.
- **`Tools/blender/retail_frontage_build.py`**: 6800 Lee Drug and 6806–6820 built from the one usable reference (`Art/reference/retail-neighbours/`, Gb321, CC BY-SA 4.0). Lee Drug gets a bronze cornice, storefront glazing with mullions, framed portrait panels, a painted lettering band, two-tier parapet with reveals and a fluted corner tower; 6806–6820 gets a burgundy fascia, brown pilasters, white-framed windows with security bars and its own banded parapet. Built in world coordinates from the blockout bounds (Lee Drug X −33.20…−14.37, face Y −14.63, H 12.10 m; 6806–6820 X −66.08…−33.07), joined to one mesh per building with a JSON part report.
- **`Tools/unreal/ue_import_retail_detail.py`**: imports the two meshes with complex-as-simple collision — trim decorates a solid the player already collides with and must not add snag points.
- **`Art/blender/retail/ref-analysis.md`**: the reference analysis the `references` skill pack requires, written before modelling.
- **`Tools/_common.py`** and `Tools/tests/test_retail_frontage.py`: the bay-layout arithmetic, extracted so it is testable without Blender (4 tests).
- **Review shot 5**, `demo/review-demo-retail-frontage`, embedded in `Screenshots/README.md`.
@@ -144,7 +144,7 @@ Retail frontage. The two retail buildings beside the hero are modelled from thei
### Fixed
- **`Tools/render_screenshots.sh` never captured anything.** The capture step called `./Engine/...` while every commandlet above it went through `run_cmdlet`'s `cd "$ENGINE"`. Under `set -e` the script died at exit 127 _after_ rebuilding the maps, so the PNGs silently stayed a day old and `--verify` never ran.
- **`Tools/review/render_screenshots.sh` never captured anything.** The capture step called `./Engine/...` while every commandlet above it went through `run_cmdlet`'s `cd "$ENGINE"`. Under `set -e` the script died at exit 127 _after_ rebuilding the maps, so the PNGs silently stayed a day old and `--verify` never ran.
- **Duplicate `RetailTrim_*` actors on every rebuild**: the demo builder's clear list did not include the new label, so each run stacked another copy. Caught by the scene audit, not by eye.
- **Mural plates were placed 0.20 m inside the solid blockout volume** and never rendered; the inset is now faked with a proud frame and a near-flush plate.
- **Trim detail sat 0.05 m off the wall**, under a pixel of depth at 40 m, so the wall won the depth test. Trim planes now stand 0.10–0.20 m proud.
@@ -170,8 +170,8 @@ Façade finishing. The kit gets the hero detail it was missing, and a class of s
### Added
- **Scene audit** (`Tools/ue_audit_scene.py`): fails when any staged mesh has no material, when a frontage building is more than ±1 m off its verified footprint, or when two actors share a label. Wired into `Tools/render_screenshots.sh`, which now runs the whole staging chain in the correct order and stops on an audit failure.
- **`Tools/ue_placement.py`**: one source of truth for staged positions and the verified frontage spans, with `span_error` / `describe_span_failure` validators.
- **Scene audit** (`Tools/unreal/ue_audit_scene.py`): fails when any staged mesh has no material, when a frontage building is more than ±1 m off its verified footprint, or when two actors share a label. Wired into `Tools/review/render_screenshots.sh`, which now runs the whole staging chain in the correct order and stops on an audit failure.
- **`Tools/unreal/ue_placement.py`**: one source of truth for staged positions and the verified frontage spans, with `span_error` / `describe_span_failure` validators.
- **Hero fluting**: 20 grooves per column as shallow shadow-tone cylinders (120 pieces) rather than boolean cuts.
- **Presence of references for the retail run**: `Art/reference/retail-neighbours/` (6800 Lee Drug and its neighbour). Local only, not committed.
@@ -210,9 +210,9 @@ Building façades. The block goes from flat extruded boxes with flat colours to
- **Generated surface textures** — deterministic stdlib PNGs for ashlar stone, stucco, asphalt and concrete (base colour + roughness), sampled world-aligned with per-material tiling; byte-identical on re-run.
- **Materials**: masonry coursing and roughness variation in the material graphs; `M_FacadeStoneShadow`, `M_FacadeGlass` (specular 0.62 ≈ IOR 1.5) and `M_FacadeBanner`. Material assignment is now **per slot**, keyed on the imported slot material, instead of slot 0 only.
- **El Capitan Theatre trim**: projecting marquee (12.0 × 2.60 m canopy at a 4.60 m soffit), illuminated fascia, recessed soffit, and a vertical blade sign (1.30 × 5.20 m) with frame and wall bracket.
- **Visual review loop** (`Tools/visual_loop.py` + `visual_targets.json`): verifies every capture against an expectation (missing, blank, tone-less or wrong-size shots fail) and measures each render against its reference photograph, emitting feedback that names the discipline to consult. Iterations are written under `Art/reference/iterations/`, which is gitignored until a shot is confirmed.
- **`Tools/fbx_export_preset.py`** — one export preset replacing six copies, and it exports smoothing groups (`FACE`), so round geometry is no longer faceted.
- **`Tools/ue_common.py`** — clears the read-only bit an LFS `.umap` checks out with and makes a refused save loud.
- **Visual review loop** (`Tools/review/visual_loop.py` + `visual_targets.json`): verifies every capture against an expectation (missing, blank, tone-less or wrong-size shots fail) and measures each render against its reference photograph, emitting feedback that names the discipline to consult. Iterations are written under `Art/reference/iterations/`, which is gitignored until a shot is confirmed.
- **`Tools/blender/fbx_export_preset.py`** — one export preset replacing six copies, and it exports smoothing groups (`FACE`), so round geometry is no longer faceted.
- **`Tools/unreal/ue_common.py`** — clears the read-only bit an LFS `.umap` checks out with and makes a refused save loud.
### Changed
@@ -249,7 +249,7 @@ Packaging and setup fixes. No gameplay or visual scope; this patch makes the pip
### Fixed
- **Map imports could report success while saving nothing.** `.umap` files are LFS-tracked and check out read-only, so `save_map()` returned `False` with no error and the staged actor vanished on the next load. New `Tools/ue_common.py` (`make_writable`, `require_saved`) clears the read-only bit and makes the failure loud; `ue_hero_facade_import.py` and `ue_import_north_buildings.py` now use it. Verified from a forced `0o444` map: chmod logged, `save_map: True`, exit 0.
- **Map imports could report success while saving nothing.** `.umap` files are LFS-tracked and check out read-only, so `save_map()` returned `False` with no error and the staged actor vanished on the next load. New `Tools/unreal/ue_common.py` (`make_writable`, `require_saved`) clears the read-only bit and makes the failure loud; `ue_hero_facade_import.py` and `ue_import_north_buildings.py` now use it. Verified from a forced `0o444` map: chmod logged, `save_map: True`, exit 0.
### Changed
@@ -268,8 +268,8 @@ Scale and façade correction patch. Buildings and the hero elevation now match t
### Added
- **`Tools/facade_detail_concepts.py`** — generates frontal building plates (whole elevation, entrance in its façade, building with neighbours) from a local reference photograph through OpenRouter image models. Spec-driven, never overwrites, writes a provenance sidecar per image, output stays in the gitignored `Art/reference/` tree. Local working material only.
- **`Tools/fbx_export_preset.py`** — the single FBX export preset (`ue_metric_v1` / `ue_character_v1`). Replaces the argument block that had been copied into six exporters.
- **`Tools/gen/facade_detail_concepts.py`** — generates frontal building plates (whole elevation, entrance in its façade, building with neighbours) from a local reference photograph through OpenRouter image models. Spec-driven, never overwrites, writes a provenance sidecar per image, output stays in the gitignored `Art/reference/` tree. Local working material only.
- **`Tools/blender/fbx_export_preset.py`** — the single FBX export preset (`ue_metric_v1` / `ue_character_v1`). Replaces the argument block that had been copied into six exporters.
- **Façade occlusion probe** in `-qaselftest`: 40 m traces sideways from the road centre at walking height, required wherever a building is staged.
- **Ionic detail on the hero**: base moulding at every shaft foot, a square abacus over every capital, and a 52-dentil cornice course.
- **Tool tests**: `Tools/tests/` now covers the input driver, height classification, the Mapbox cross-check, the concept generator and the FBX preset (49 tests).
@@ -335,16 +335,16 @@ The first playable release: a walkable exterior demo of the **6840 Hollywood Blv
- **Unreal project** (`Game/GigLine`) — C++ game module, Enhanced Input, packaged Linux build, and a `DemoMap` that a plain launch opens directly.
- **Walkable demo level** — terrain from the 3DEP DEM, Hollywood Blvd carriageway, both sidewalks and kerbs with collision, the designated mid-block crossing, and fixed daytime lighting (sun, sky light, sky atmosphere, height fog, pinned exposure).
- **Hero façade** for 6840 Hollywood Blvd — six-column neoclassical elevation with three genuine portal recesses, entablature, parapet and motto band (`Tools/hero_facade_build.py`, `Tools/ue_hero_facade_import.py`).
- **Hero façade** for 6840 Hollywood Blvd — six-column neoclassical elevation with three genuine portal recesses, entablature, parapet and motto band (`Tools/blender/hero_facade_build.py`, `Tools/unreal/ue_hero_facade_import.py`).
- **Block frontage** — the five verified south-side neighbours (6922, 6902, 6838 El Capitan Theatre, 6822 Budget, 6806–6822, 6800 Lee Drug) and the two north-side buildings (TCL Chinese Theatre, Madame Tussaud's) placed at their OSM positions.
- **First-person player** — `AGigLineCharacter`: 1.75 m capsule, 1.60 m eye camera, 4 m/s walk, mouse look, jump, reset (`R`) and pause (`Escape`); controls documented in `Docs/controls.md` and shown in-game.
- **In-game ODbL attribution** — `AGigLineHUD` draws the OpenStreetMap credit and licence line every frame.
- **QA harness** — `-qaselftest` (walk, reset, pause, seam transect, invisible-wall probe, frame-time capture) and `Tools/input_test.py` / `Tools/input_emulation.py` for OS-level virtual keyboard/mouse input.
- **QA harness** — `-qaselftest` (walk, reset, pause, seam transect, invisible-wall probe, frame-time capture) and `Tools/input/input_test.py` / `Tools/input/input_emulation.py` for OS-level virtual keyboard/mouse input.
- **Documentation** — setup, build, toolchain, licensing, ODbL review, release checks, and arc42 architecture docs.
### Fixed
- **All controls were dead in packaged and PIE builds.** The mapping-context generator wrote key bindings to the deprecated `mappings` array; UE 5.8 reads `DefaultKeyMappings.Mappings`, so the cooked game had zero runtime mappings and every key resolved to nothing. `Tools/ue_create_input_assets.py` now populates the current field.
- **All controls were dead in packaged and PIE builds.** The mapping-context generator wrote key bindings to the deprecated `mappings` array; UE 5.8 reads `DefaultKeyMappings.Mappings`, so the cooked game had zero runtime mappings and every key resolved to nothing. `Tools/unreal/ue_create_input_assets.py` now populates the current field.
- **"Fall-through" was an 8 m invisible box.** `Terrain_DEM` shipped only a bounding-box collision proxy; forcing complex-as-simple collision (`UGigLineAssetTools`) put the player on the actual street.
- **Reset returned the player to mid-air** — the start position is now captured on the first grounded frame.
- **Plain launch opened the engine template** — the default map now lives in `DefaultEngine.ini` and points at `DemoMap`.
+1 -1
View File
@@ -30,7 +30,7 @@ All modelling and geographic processing live outside the game project in `Art/`,
## Quality achievement
- **Realism first**: reference photography and measured data drive every façade decision; renders are compared against the photographs with `Tools/visual_loop.py`, and a façade is built from the shared 4.96 m module kit so the street wall reads consistently. Materials are authored in Unreal with world-aligned generated textures, and lighting is fixed daytime so a shot is comparable between runs.
- **Realism first**: reference photography and measured data drive every façade decision; renders are compared against the photographs with `Tools/review/visual_loop.py`, and a façade is built from the shared 4.96 m module kit so the street wall reads consistently. Materials are authored in Unreal with world-aligned generated textures, and lighting is fixed daytime so a shot is comparable between runs.
- **Performance as a budget, not a driver**: dynamic-only lighting (no build step, no static shadow maps), world-space-noise materials (cheap procedural variation), minimal overdraw for street-level geometry, and a 60 FPS at 1440p floor that every release must meet. It closes the quality list because it constrains how much realism fits, not what the block should look like.
- **Walkability**: complex-as-simple collision on terrain (C++ helper forces the flag on the asset), block-all collision on every site mesh, swept transect QA verifies zero raycast holes.
+2 -2
View File
@@ -20,7 +20,7 @@ C++ classes in `Game/GigLine/Source/GigLine/`:
| --- | --- | --- |
| `AGigLineCharacter` | First-person player pawn | 1.75 m capsule, 4.5 m/s walk, Enhanced Input bindings, reset, pause, `-qaselftest` harness |
| `AGigLineGameModeBase` | Sets game mode and HUD class | `HUDClass = AGigLineHUD` |
| `AGigLineHUD` | HUD text overlay | `DrawText` every frame: ODbL attribution bottom-left, controls bottom-right, and an F3 debug readout top-left (`gigline.Debug`, on for `-perf`/`-qaselftest`) |
| `AGigLineHUD` | HUD text overlay | `DrawText` every frame: controls bottom-right, and an F3 debug readout top-left (`gigline.Debug`, on for `-perf`/`-qaselftest`) |
| `AGigLineNPCPerson` | NPC prefab (future) | Capsule + visible MPFB body, feet on Z 0 |
| `AGigLineShotActor` | Screenshot camera walker | Drives cameras in `-game` mode, `FScreenshotRequest`, quits |
| `UGigLineAssetTools` | C++ collision helpers | `SetComplexCollisionAsSimple` on `StaticMesh` assets |
@@ -49,7 +49,7 @@ Static meshes and materials in `/Game/Hollywood/Site/`:
| `SM_HeroFacade` | El Capitan Entertainment Centre detailed façade |
| `SM_ElCapitanTheatre`, `SM_Budget_6822`, `SM_Hollywood_6806`, `SM_Hollywood_6902`, `SM_Hollywood_6922`, `SM_LeeDrug_6800` | South block frontage (6 buildings) |
| `SM_Madame_Tussauds`, `SM_TCL_Chinese_Theatre` | North-side buildings (within 50 m) |
| `SM_Retail_LeeDrug`, `SM_Retail_6806` | Retail trim over 6800 and 6806–6820, modelled from their own photograph (`Tools/retail_frontage_build.py`, staged as `RetailTrim_*`). `6822 Budget` stays a bare volume: no photograph exists. |
| `SM_Retail_LeeDrug`, `SM_Retail_6806` | Retail trim over 6800 and 6806–6820, modelled from their own photograph (`Tools/blender/retail_frontage_build.py`, staged as `RetailTrim_*`). `6822 Budget` stays a bare volume: no photograph exists. |
### Art Pipeline
+3 -3
View File
@@ -18,7 +18,7 @@
## Screenshot capture
1. `Tools/render_screenshots.sh` compiles editor, builds `ReviewMap` from `Tools/ue_build_review_map.py`, launches game in `-game` mode.
1. `Tools/review/render_screenshots.sh` compiles editor, builds `ReviewMap` from `Tools/unreal/ue_build_review_map.py`, launches game in `-game` mode.
2. `AGigLineShotActor` iterates the camera list, sets view, calls `HighResShot 1280x720`, waits for capture.
@@ -48,6 +48,6 @@
1. Blender headless (`Tools/export_*.py`) reads `.blend` files, applies metre→centimetre conversion, exports FBX with `ue_metric_v1` preset.
2. Unreal headless (`Tools/ue_import_*.py`) imports FBX via Interchange, places meshes at geographic coordinates, sets collision profiles.
2. Unreal headless (`Tools/unreal/ue_import_*.py`) imports FBX via Interchange, places meshes at geographic coordinates, sets collision profiles.
3. `Tools/ue_build_demo_map.py` assembles `DemoMap`: creates/repositions actors, sets lighting, saves map and assets.
3. `Tools/unreal/ue_build_demo_map.py` assembles `DemoMap`: creates/repositions actors, sets lighting, saves map and assets.
+2 -2
View File
@@ -22,7 +22,7 @@ Ordered by priority: realism first, performance last. The order decides design t
| ID | Source/Stimulus | Response | Metric |
| --- | --- | --- | --- |
| QS-1 | Screenshot is compared against its reference photograph | The render reads as the same building | Similarity score and visual match confirmed by the owner; `Tools/visual_loop.py` reports the deltas |
| QS-1 | Screenshot is compared against its reference photograph | The render reads as the same building | Similarity score and visual match confirmed by the owner; `Tools/review/visual_loop.py` reports the deltas |
| QS-2 | A façade is placed from measured data | It sits on its verified footprint | World span probed against `Geo/verification/` within ~1 m |
| QS-3 | Player walks both sides of the demo | No fall-through, character stays at ground Z | 0 raycast holes in 84-sample transect |
| QS-4 | Player holds W for 2 s on flat road | Character moves forward | ≥ 200 cm displacement, Z constant within ±5 cm |
@@ -41,5 +41,5 @@ Ordered by priority: realism first, performance last. The order decides design t
| BR-1 | A building with a reference photograph has a review shot compared against it | Silhouette and proportion deltas recorded per building in `Screenshots/README.md`; owner approval per shot |
| BR-2 | Every building is either photo-matched or explicitly recorded as an unfounded blockout | List in `TODO.md`; no building in an in-between state |
| BR-3 | Every building has a collision proxy and at least one LOD | Count of buildings missing either = 0 |
| BR-4 | No surface carries a placeholder material | Material audit in `Tools/ue_audit_scene.py` reports 0 placeholder slots |
| BR-4 | No surface carries a placeholder material | Material audit in `Tools/unreal/ue_audit_scene.py` reports 0 placeholder slots |
| BR-5 | The realism pass does not break the floor | QS-9 still holds at 1440p, with the measured cost of the pass recorded |
+27
View File
@@ -0,0 +1,27 @@
---
title: Gig Line Architecture Documentation
description: Architecture documentation for the Gig Line Linux-first Unreal Engine 5 demo around 6840 Hollywood Blvd.
status: v0.1.0
date: 2026-11-08
---
Gig Line is a Linux-first Unreal Engine 5 walking and driving demo around 6840 Hollywood Boulevard and the El Capitan Entertainment Centre. The architecture prioritises visual realism and geographically faithful placement while maintaining a fully walkable scene, reproducible asset pipeline, documented provenance, and a 60 FPS floor at 1440p.
This directory contains the canonical arc42 documentation. Each section is maintained independently; update the relevant page in the same change as its implementation. Project work and future scope remain in [TODO.md](../../TODO.md) and [ROADMAP.md](../../ROADMAP.md).
## Sections
1. [Introduction and Goals](01_introduction_and_goals.md)
2. [Architecture Constraints](02_architecture_constraints.md)
3. [Context and Scope](03_context_and_scope.md)
4. [Solution Strategy](04_solution_strategy.md)
5. [Building Block View](05_building_block_view.md)
6. [Runtime View](06_runtime_view.md)
7. [Deployment View](07_deployment_view.md)
8. [Cross-cutting Concepts](08_concepts.md)
9. [Architecture Decisions](09_architecture_decisions.md)
10. [Quality Requirements](10_quality_requirements.md)
11. [Risks and Technical Debt](11_technical_risks.md)
12. [Glossary](12_glossary.md)
See also [About arc42](about-arc42.md).
-570
View File
@@ -1,570 +0,0 @@
---
title: Gig Line Architecture Documentation
description: Architecture documentation for the Gig Line Linux-first Unreal Engine 5 demo around 6840 Hollywood Blvd.
status: v0.1.0
date: 2026-11-08
---
# Introduction and Goals
Gig Line is a Linux-first Unreal Engine 5 game that reconstructs a walkable (and eventually drivable) stretch of Hollywood Boulevard around 6840 Hollywood Blvd / El Capitan Entertainment Centre. The initial release (`0.1.0`) delivers a first-person walking demo of the south-side block between Highland Avenue and Orange Drive, plus north-side buildings within 50 m of the frontage.
## Requirements Overview
| ID | Description |
| --- | --- |
| F-1 | Walk both sides of Hollywood Blvd in the demo area without falling through geometry |
| F-2 | First-person movement (WASD + mouse look), jump, reset (R), pause (Escape) |
| F-3 | Fixed daytime lighting and atmosphere (sun, sky, fog, exposure) |
| F-5 | Designated street crossing with zebra markings at signalised crossings |
| F-6 | Building façades recognisable against reference photography |
| F-7 | Terrain, road, and sidewalk follow real-world DEM elevation |
| F-8 | 60 FPS at 1440p on the documented workstation and quality preset |
Full requirements and progress are in [TODO.md](../../TODO.md); version direction is in [ROADMAP.md](../../ROADMAP.md).
## Quality Goals
| Priority | Quality Goal | Scenario |
| --- | --- | --- |
| 1 | **Visual realism** | Renders read as the reference photographs: façade identity, proportion, material and light — judged side by side, shot against photo |
| 2 | **Geographic fidelity** | Building placement and street scale match the measured data and the photographs |
| 3 | **Terrain walkability** | Zero raycast holes across both sidewalks and the carriageway (84-sample transect passes) |
| 4 | **Build reproducibility** | Fresh checkout + documented engine path produces a runnable Linux build |
| 5 | **Provenance** | Every committed asset has recorded provenance |
| 6 | **Performance efficiency** | 60 FPS at 2560×1440 on RTX 5070 Ti, documented preset, no frame generation |
**How the order is used.** It decides trade-offs. When realism and performance disagree, realism wins and the cost is paid — but never by silently shipping a frame rate below the floor. The 60 FPS figure is a **budget that must still be met**, so the response to "realism costs too much frame time" is to cut the realism feature deliberately, record the cut, and keep the floor — not to let frames slip. Walkability outranks realism in one direction only: a beautiful block the player falls through is not a demo.
## Stakeholders
| Role | Contact | Expectations |
| --- | --- | --- |
| Owner / Director | zwitschi | Sets scope, approves screenshots, cuts release tags |
| AI coding agents | multiple sessions | Read `AGENTS.md`, `TODO.md`, `ROADMAP.md`, and this document for architecture constraints; never commit directly to `main` |
| Players | end users | Walk the demo at 60 FPS without falling through the world; see a recognisable Hollywood block |
# Architecture Constraints
## Technical Constraints
| Constraint | Explanation |
| --- | --- |
| Engine: Unreal 5.8.4 | Pinned; no bundled Linux clang — uses system clang 23.1.4. Game targets use `BuildSettingsVersion.V7`. |
| Platform: Linux first | Developed and tested on Bazzite Linux (Fedora 45 base). Windows builds ship but are secondary. |
| GPU: RTX 5070 Ti 16 GB | Dev workstation target. Vulkan RHI; open kernel module driver 615.78.08. |
| No static lighting | `r.AllowStaticLighting=False`. The demo is a fixed daytime scene lit entirely dynamically to avoid "LIGHTING NEEDS TO BE REBUILT" warnings. |
| Blender→UE via FBX | Flat diffuse colours do not survive FBX→Unreal import. All materials are authored in UE. |
| Terrain collision | UE 5.8 exposes `CollisionTraceFlag` (not `bUseComplexAsSimpleCollision`); Python cannot set it. Must use C++ helper `UGigLineAssetTools::SetComplexCollisionAsSimple`. |
| Coordinate convention | South = +Y, north = −Y (ground truth = street mesh orientation). 1 m in Blender = 100 cm in Unreal. |
## Organisational Constraints
| Constraint | Explanation |
| --- | --- |
| Squash-merge to `main` | Every PR is squash-merged for a linear history. No direct commits to `main` except integration merges. |
| Visual review required | Any change affecting appearance must produce screenshots for human approval before being marked done. |
| Asset provenance | Every committed asset must have verified licence and redistribution permission recorded in `THIRD_PARTY_NOTICES.md`. |
| Reference photography | Local only, gitignored, never committed. Used for façade work under fair-use / internal-development rationale. |
| LFS for binaries | `.uasset`, `.umap`, `.blend` tracked with Git LFS; `Binaries/`, `Intermediate/`, `Saved/` are gitignored. |
## Conventions
| Convention | Explanation |
| --- | --- |
| Semantic versioning | `MAJOR.MINOR.PATCH`. `0.x` documents breaking changes. `1.0.0` = agreed game scope. |
| Task tracking in `TODO.md` | Single central task list. `- [ ]` open, `- [-]` or `- [~]` in progress, `- [x]` done, `- [A]` active, `- [D]` deferred. |
| Headless Blender | All Blender work driven by `bpy` scripts in `Tools/`, not MCP. Blender skills are method guides only. |
| Named export presets | `ue_metric_v1` for static meshes, `ue_character_v1` for skeletal. Every exporter calls the metre→centimetre conversion. |
# Context and Scope
## Business Context
The system is a self-contained single-player game. It has no server backend, no multiplayer, and no live service dependencies. Its communication partners are:
| Partner | Inbound | Outbound |
| --- | --- | --- |
| Player (keyboard/mouse) | WASD, mouse, R (reset), Escape (pause), Space (jump) | Rendered 3D scene + HUD |
| OSM data | Extracted building footprints, street geometry, elevation | — |
| Filesystem | Engine content (`.uasset`, `.umap`), Blender sources (`.blend`), DEM GeoTIFF | Saved maps, screenshots, packaged build |
## Technical Context
| Channel | Protocol / Format | Purpose |
| --- | --- | --- |
| Keyboard + mouse | Enhanced Input (IA\_\*, IMC_Default) | First-person movement, look, jump, reset, pause |
| Vulkan RHI | GPU rendering | Scene display at 60 FPS / 1440p |
| FBX | Autodesk FBX (binary) | Blender→Unreal mesh and skeleton transfer |
| GeoTIFF | 3DEP 1 m DEM | Terrain heightfield source |
| OSM XML | Overpass API extract | Building footprint and street layout source |
| Git / LFS | HTTPS to Gitea remote | Source control, binary asset storage |
# Solution Strategy
## Technology decisions
- **Unreal Engine 5.8.4** for rendering, World Partition, physics (Chaos), and the Enhanced Input system. Chosen for its Linux support, open C++ source, and real-time rendering quality.
- **Blender 5.2 LTS** for all 3D modelling, UV work, and collision mesh authoring. Driven headlessly via `bpy` Python scripts, not MCP.
- **QGIS 4.2.3** for geographic data cleanup, coordinate conversion (EPSG:26911), and DEM preparation.
- **Blosm 2.7.28** for OSM building and street blockouts inside Blender (layout data only — not authoritative façade, height, or pavement detail).
- **MPFB 2.0.18** for human base meshes and baked materials (NPCs in `0.2.0+`).
## Top-level decomposition
The system is a single Unreal game module (`GigLine`) with no plugins beyond `EnhancedInput`. Content is organised by domain under `/Game/Hollywood/`:
- `Site/` — terrain, road, sidewalks, kerbs, crossing markings, site materials
- `ElCapitan/` — hero façade mesh and materials
- `Frontage/` — south-side block building meshes
- `North/` — north-side building meshes
- `Input/` — Enhanced Input actions and mapping context
All modelling and geographic processing live outside the game project in `Art/`, `Geo/`, and `Tools/`. The `Game/` directory contains only engine-ready content and C++ source.
## Quality achievement
- **Realism**: reference photography drives every façade decision, measured data fixes placement and scale, and renders are compared against the photographs (`Tools/visual_loop.py`, `Screenshots/README.md`). One module kit at the 4.96 m bay keeps the street wall coherent; materials are world-aligned and lit by a fixed daytime rig so a shot is comparable between runs.
- **Performance holds the floor, it does not set the agenda**: dynamic-only lighting (no build step, no static shadow maps), cheap procedural materials, minimal overdraw for street-level geometry, and 60 FPS at 1440p as a budget every release must meet.
- **Walkability**: complex-as-simple collision on terrain (C++ helper forces the flag on the asset), block-all collision on every site mesh, swept transect QA verifies zero raycast holes.
- **Fidelity**: geographic data verified before modelling (footprints, bounds, DEM); building placement checked against street mesh, never in isolation; reference photography drives façade work.
- **Reproducibility**: pinned toolchain in `Docs/toolchain.md`, named export presets, `Tools/check-lfs.sh` gate, fresh-checkout build procedure in `Docs/build.md`.
# Building Block View
## Whitebox Overall System
The system decomposes into five top-level building blocks:
| **Name** | **Responsibility** |
| --- | --- |
| Gameplay | Player pawn, movement, camera, input bindings, HUD, game mode |
| Site | Terrain, road, sidewalks, kerbs, crossing markings, site-level collision |
| Façades | Hero façade (El Capitan), south block frontage, north-side buildings |
| Art Pipeline | Blender models, geographic data, export scripts, import scripts |
| QA & Review | Screenshot pipeline, self-test harness, visual review workflow |
### Gameplay
C++ classes in `Game/GigLine/Source/GigLine/`:
| Class | Responsibility | Key detail |
| --- | --- | --- |
| `AGigLineCharacter` | First-person player pawn | 1.75 m capsule, 4.5 m/s walk, Enhanced Input bindings, reset, pause, `-qaselftest` harness |
| `AGigLineGameModeBase` | Sets game mode and HUD class | `HUDClass = AGigLineHUD` |
| `AGigLineHUD` | HUD text overlay | `DrawText` every frame: ODbL attribution bottom-left, controls bottom-right, and an F3 debug readout top-left (`gigline.Debug`, on for `-perf`/`-qaselftest`) |
| `AGigLineNPCPerson` | NPC prefab (future) | Capsule + visible MPFB body, feet on Z 0 |
| `AGigLineShotActor` | Screenshot camera walker | Drives cameras in `-game` mode, `FScreenshotRequest`, quits |
| `UGigLineAssetTools` | C++ collision helpers | `SetComplexCollisionAsSimple` on `StaticMesh` assets |
Input assets in `/Game/Input/`: `IA_Move`, `IA_MoveBack`, `IA_Strafe`, `IA_StrafeLeft`, `IA_Look`, `IA_Jump`, `IA_Reset`, `IA_Pause`, `IA_ToggleCamera`, `IA_ToggleDebug`, bound in `IMC_Default`.
Player-facing bindings: **WASD** move, **mouse** look, **Space** jump, **R** reset, **Escape** pause, **V** swap first/third person. The camera and body work landed with `0.2.0` (see ADR-9): the shipped default view stays first person, and `-thirdperson` starts the pawn in the third-person rig — used by the screenshot run so the review is reproducible.
### Site
Static meshes and materials in `/Game/Hollywood/Site/`:
| Asset | Role |
| --- | --- |
| `Terrain_DEM` | Ground heightfield (complex-as-simple collision) |
| `Road_Hollywood` | Carriageway surface (block-all collision) |
| `Sidewalk_North`, `Sidewalk_South` | Pedestrian surfaces (block-all collision) |
| `Kerb_North`, `Kerb_South` | Kerb edges |
| `Crossing_Markings` | Zebra markings at signalised crossing |
| `M_Asphalt`, `M_Concrete`, `M_KerbConcrete`, `M_Ground`, `M_RoadPaint`, `M_Stucco`, `M_FacadeStone` | Procedural materials (world-space-noise blend + roughness) |
### Façades
| Asset | Role |
| --- | --- |
| `SM_HeroFacade` | El Capitan Entertainment Centre detailed façade |
| `SM_ElCapitanTheatre`, `SM_Budget_6822`, `SM_Hollywood_6806`, `SM_Hollywood_6902`, `SM_Hollywood_6922`, `SM_LeeDrug_6800` | South block frontage (6 buildings) |
| `SM_Madame_Tussauds`, `SM_TCL_Chinese_Theatre` | North-side buildings (within 50 m) |
| `SM_Retail_LeeDrug`, `SM_Retail_6806` | Retail trim over 6800 and 6806–6820, modelled from their own photograph (`Tools/retail_frontage_build.py`, staged as `RetailTrim_*`). `6822 Budget` stays a bare volume: no photograph exists. |
### Art Pipeline
Python scripts in `Tools/` drive the content pipeline:
| Script group | Purpose |
| --- | --- |
| `export_*`, `hero_facade_build` | Blender headless export (FBX) |
| `ue_import_*`, `ue_hero_facade_import` | Unreal headless import + placement |
| `character_anim_build`, `ue_character_anim_import` | Character animation clips (`GigLine_Idle`, `GigLine_Walk`) and their import |
| `export_*`, `hero_facade_build`, `retail_frontage_build`, `theatre_trim_build`, `facade_kit_build` | Blender headless export (FBX); trim builders work in world coordinates |
| `ue_import_*`, `ue_hero_facade_import`, `ue_import_retail_detail` | Unreal headless import + placement |
| `ue_build_demo_map`, `ue_build_review_map` | Map assembly (lighting, meshes, player start) |
| `ue_create_input_assets` | Enhanced Input actions + mapping context |
| `render_screenshots.sh` | Screenshot capture pipeline |
| `judge_screenshot.py` | Vision-model pre-check (optional) |
| `input_emulation.py`, `input_test.py` | OS-level virtual keyboard/mouse injection for input verification |
| `Tools/_common.py` | Pure helper shared by Blender builders, importable without Blender (bay layout) |
| `Tools/tests/` | Stdlib `unittest` tests for the pure parts of the tools (`requirements-tools.txt`) |
| `Geo/tools/mapbox_buildings.py` | Second height source: Mapbox `mapbox-streets-v8` building data vs our OSM heights |
Source data: `Art/blender/` (`.blend` files), `Geo/` (OSM extracts, DEM, verification docs).
### QA & Review
- `AGigLineShotActor` walks fixed cameras in `-game` mode, captures PNGs, and quits.
- `-qaselftest` switch on `AGigLineCharacter` runs a core-ticker harness: walk 2 s, reset, pause/unpause, ground transect — all PASS/FAIL logged.
- `Screenshots/` holds committed PNGs and `README.md` approval table.
## Level 2
### White Box Gameplay — Input Flow
1. `AGigLineCharacter::SetupPlayerInputComponent` binds `IA_*` actions to `MoveForward`, `MoveBack`, `MoveStrafe`, `MoveLeft`, `Look`, `StartJump`/`StopJump`, `ResetToStart`, `TogglePause`.
2. Movement delegates to `ApplyMove(Forward, Strafe)` → `AddMovementInput`.
3. Look applies yaw/pitch scaled by `LookSensitivity` (Y invertable via `bInvertLookY`).
4. `ResetToStart` teleports to `StartLocation` (captured on first grounded frame), clears velocity, resets control rotation.
5. `TogglePause` toggles `APlayerController::SetPause`, swaps input mode between `FInputModeGameOnly` and `FInputModeUIOnly`, shows/hides mouse cursor.
### White Box Site — Collision Model
- Every site mesh (road, sidewalks, kerbs, crossing) is `STATIC` with `BlockAll` collision.
- `Terrain_DEM` uses `CTF_UseComplexAsSimple` (forced by `UGigLineAssetTools` at build time, saved on the asset).
- The terrain corridor is carved to half-width 10.7 m around the road centreline so the player can walk the carriageway and both sidewalks.
- Ground Z: north sidewalk 395, road 366, south sidewalk 317 (3.94 m fall along the block, matching the DEM).
# Runtime View
## Walk the block
1. Player launches game → `AGigLineGameModeBase` spawns `AGigLineCharacter` at `PlayerStart` `(-13336, -1150, 600)`.
2. Character falls 144 cm to road surface (Z 456) and begins walking. `StartLocation` captured on first grounded frame.
3. Player holds W → `IA_Move` fires → `ApplyMove(1, 0)` → `AddMovementInput(ForwardVector, 1)` → capsule moves at 450 cm/s.
4. Mouse move → `IA_Look` fires → yaw/pitch applied to controller rotation → camera follows (attached to capsule at 160 cm eye height).
5. Player presses R → `ResetToStart` → teleport to `StartLocation`/`StartRotation`, `StopMovementImmediately`, reset control rotation.
6. Player presses Escape → `TogglePause` → `SetPause(true)`, switch to `FInputModeUIOnly`, show cursor. World timers stop; core tickers continue.
7. Player presses Escape again → `TogglePause` → `SetPause(false)`, switch to `FInputModeGameOnly`, hide cursor.
## Screenshot capture
1. `Tools/render_screenshots.sh` compiles editor, builds `ReviewMap` from `Tools/ue_build_review_map.py`, launches game in `-game` mode.
2. `AGigLineShotActor` iterates the camera list, sets view, calls `HighResShot 1280x720`, waits for capture.
3. On last camera, requests engine quit. PNGs are copied to `Screenshots/` and committed.
## QA self-test (`-qaselftest`)
1. Core ticker starts at `BeginPlay` (not a world timer — world timers freeze on pause).
2. 1.0 s: begin driving forward input.
3. 3.0 s: stop, measure displacement (> 200 cm = PASS).
4. 3.5 s: call `ResetToStart`.
5. 4.0 s: measure distance from spawn (< 100 cm = PASS).
6. 4.5 s: call `TogglePause`, check `IsGamePaused()` (= true = PASS).
7. 5.5 s: call `TogglePause`, check `IsGamePaused()` (= false = PASS).
8. 6.0 s: swept ground transect across three lanes (Y −1400, −167, +1000), X −14900 to −9420 at 200 cm spacing. 84 samples, 0 holes = PASS.
9. 7.0 s: final verdict logged.
## Build and import pipeline
1. Blender headless (`Tools/export_*.py`) reads `.blend` files, applies metre→centimetre conversion, exports FBX with `ue_metric_v1` preset.
2. Unreal headless (`Tools/ue_import_*.py`) imports FBX via Interchange, places meshes at geographic coordinates, sets collision profiles.
3. `Tools/ue_build_demo_map.py` assembles `DemoMap`: creates/repositions actors, sets lighting, saves map and assets.
# Deployment View
## Infrastructure Level 1 — Development Workstation
| Node | Specification |
| --- | --- |
| Dev workstation | Bazzite Linux 45 (Fedora 45 base, immutable ostree), Ryzen 7 7700X, 64 GB RAM, RTX 5070 Ti 16 GB, Vulkan RHI |
| Engine install | `/run/media/system/DATA/Engines/UE_5.8.4` (73 GiB, external to repo) |
| Project repo | `/run/media/system/DATA/Documents/06-games/gig-line` (Git + LFS, remote at `git.allucanget.biz`) |
| Build output | `/run/media/system/DATA/Builds/GigLineQA/Linux` (packaged Development build, ~1.1 GiB; outside the repository) |
Motivation
Single-machine development. No build farm, no CI pipeline yet. The engine, toolchain, and GPU are all on the same host.
Quality and Performance Features
- Vulkan RHI on RTX 5070 Ti, open kernel module driver 615.78.08.
- System clang 23.1.4 (no bundled engine clang) compiles game code.
- Build time: ~90 s incremental, ~5 min clean for the editor target.
Mapping of Building Blocks to Infrastructure
| Building Block | Infrastructure Element |
| --- | --- |
| Gameplay (C++) | Dev workstation — compiled to `libUnrealEditor-GigLine.so`, loaded by engine |
| Site + Façades (UE content) | Dev workstation — `.uasset`/`.umap` in `Game/GigLine/Content/` |
| Art Pipeline (Python/Blender) | Dev workstation — headless Blender 5.2 Flatpak, Python 3.14.8 |
| QA & Review | Dev workstation — game run in `-game` mode, PNGs to `Screenshots/` |
| Git remote | `git.allucanget.biz` — Gitea instance, LFS-enabled |
## Infrastructure Level 2 — Packaged Build
| Element | Detail |
| --- | --- |
| Linux packaged build | Development config, ~1.1 GiB. Contains engine binaries, game module, cooked content, no editor. |
| Run command | `./GigLine.sh -windowed -ResX=2560 -ResY=1440` |
No server infrastructure. The game is a standalone executable.
# Cross-cutting Concepts
## Coordinate system and scale
- **Source CRS**: EPSG:26911 (UTM zone 11N). Local origin recorded in `Geo/`.
- **Blender**: metres. East → +X, north → +Y (Blender convention).
- **Unreal**: centimetres. East → +X, north → **−Y** (confirmed from street mesh: `Sidewalk_North` Y −1912..−1053, `Sidewalk_South` Y +718..+1447).
- **Conversion**: every exporter calls the metre→centimetre function. Import presets (`ue_metric_v1`, `ue_character_v1`) are named and repeatable.
Rule: place a building only against a neighbour or street mesh, never in isolation.
## Collision model
- Site meshes (road, sidewalks, kerbs, crossing): `STATIC` mobility, `BlockAll` collision profile.
- Terrain: `STATIC`, `CTF_UseComplexAsSimple` (forced by `UGigLineAssetTools` at build time, saved on the asset — the flag lives on the `UStaticMesh`/`UBodySetup`, not the map).
- Buildings: `STATIC`, simple collision (convex hull from mesh).
- Rule: the collision flag must be saved on the asset. Saving only the `.umap` silently reverts it.
## Lighting
- Dynamic-only: `r.AllowStaticLighting=False`. All lights are `MOVABLE`.
- Fixed daytime rig: directional light (sun, −62° pitch), sky light, exponential height fog, post-process volume with fixed exposure.
- Rationale: avoids "LIGHTING NEEDS TO BE REBUILT" warnings; no bake step; the scene never changes time of day.
## Materials
- All materials authored in Unreal, not carried from Blender (FBX import loses diffuse colours).
- World-space-noise blend nodes for cheap procedural variation (asphalt grain, concrete speckle, stucco roughness).
- Instance parameters where tunable (e.g. wetness, ageing).
## Geographic data provenance
- OSM extract is layout data only — not authoritative for façade, height, or pavement detail.
- DEM from 3DEP 1 m; elevation verified against cross-sections in `Geo/verification/`.
- Reference photography (local, gitignored) drives manual façade work.
## QA harness
- `-qaselftest`: core ticker (not world timer) so pause cannot freeze the test that proves pause works.
- `GigLine SIM` / `GigLine GROUND`: TEMP diagnostic logging, to be removed before release tag.
- Screenshot approval: human must tick `Screenshots/README.md` before visual work is marked done.
# Architecture Decisions
## ADR-1: Dynamic-only lighting for the demo
- **Context**: The demo is a fixed daytime scene. Stationary lights demand a lighting build and produce "LIGHTING NEEDS TO BE REBUILT" warnings.
- **Decision**: `r.AllowStaticLighting=False`, all lights `MOVABLE`.
- **Consequences**: No bake step, no warning, simpler iteration. Slightly higher GPU cost per frame (no baked shadows) — acceptable for the demo scope.
## ADR-2: Materials authored in Unreal, not imported from Blender
- **Context**: FBX→Unreal import maps all Blender diffuse colours to white. Flat colour materials do not survive the pipeline.
- **Decision**: Build all materials (asphalt, concrete, stucco, façade stone, road paint) directly in Unreal with procedural nodes.
- **Consequences**: Materials cannot be previewed in Blender look-dev. Must round-trip to Unreal for visual check. But: full PBR control, world-space noise, no import surprise.
## ADR-3: Complex-as-simple collision on terrain via C++ helper
- **Context**: UE 5.8 removed `bUseComplexAsSimpleCollision`. Python cannot set `CollisionTraceFlag` on `StaticMesh`/`BodySetup` at all. The Interchange default is bbox-only simple collision, which placed an invisible lid 8 m above the street.
- **Decision**: `UGigLineAssetTools::SetComplexCollisionAsSimple` (C++) forces `CTF_UseComplexAsSimple` and saves the asset.
- **Consequences**: One C++ helper class. The flag must be saved on the asset (not just the map). No Python workaround exists.
## ADR-4: Reset captures StartLocation on first grounded frame
- **Context**: `BeginPlay` captures `StartLocation` at the PlayerStart, which is above grade (Z 600) so the capsule does not spawn embedded. Every reset dropped the player 1.4 m back into the air.
- **Decision**: Repeating 0.25 s timer captures `StartLocation` once the character reports `IsMovingOnGround()`, then clears itself.
- **Consequences**: Reset is correct. A one-shot timer at 2 s is wrong (fires after walking begins, records mid-stride position).
## ADR-5: Core ticker for QA self-test, not world timer
- **Context**: Pausing the game stops world timers. A timer-driven test freezes exactly when it proves pause works.
- **Decision**: `FTSTicker::GetCoreTicker()` drives the `-qaselftest` harness.
- **Consequences**: Test runs even while paused. Must be enabled explicitly (`-qaselftest`), never in normal play.
## ADR-6: South = +Y, north = −Y in Unreal
- **Context**: The street mesh established this convention: `Sidewalk_North` has Y range −1912..−1053, `Sidewalk_South` has +718..+1447.
- **Decision**: Adopt it project-wide. Never place a building by assumption; always verify against a street mesh or neighbour.
- **Consequences**: Matches Blender’s north→+Y after the Y-flip in the `ue_metric_v1` export preset.
## ADR-7: Headless Blender via bpy scripts, not MCP
- **Context**: Blender skill packs assume an MCP server; the project does not run one.
- **Decision**: All Blender work driven by Python scripts in `Tools/`. Skill packs used as method guides (checklists, naming, proportions) only.
- **Consequences**: Scripts are version-controlled and repeatable. No live Blender session needed for CI.
## ADR-8: Realistic building façades are a minor milestone, not a patch
- **Context**: `0.1.4` carried the first façade pass as a _patch_, while the release policy says patches contain fixes and refinements and never new scope. The `0.1.6` retail pass then showed what that costs: a façade built in a patch has no release gate of its own, so a first pass that does not yet read as the building (`Screenshots/README.md`, 0.1.6 gaps) can sit inside a "done" patch series. Realism is also the project's leading quality goal, which a patch cannot carry.
- **Decision**: Building realism gets its own minor milestone, `0.9.0 — Realistic buildings`, placed before the feature freeze. Its scope is the façade system (orders, profile library, storefront types, signage as decals), per-building photo-matched models, a measured material library with a texel-density rule, and LODs plus collision proxies. Patches keep fixes and refinements only. Renumbered by this decision: the former `0.9.0` freeze/beta/audit milestone becomes `0.10.0`, its patches `0.10.1`–`0.10.3`, and `1.0.0` moves to weeks 39–44.
- **Consequences**: Façade work now has a release gate — every building measured against its own photograph, every building with a collision proxy and a LOD, no placeholder materials — so "modelled" means verified, not merely present. The price is that realism work stops being sliced into patches, and the schedule shifts by one minor cycle. Blockouts without a photograph stay blockouts and are listed as such, rather than being dressed up to fill the milestone.
## ADR-9: Third-person body is an attachment of the first-person pawn, not a second pawn
- **Context**: `0.2.0` needs a visible body and a third-person camera, while `0.1.0` shipped a first-person pawn with no body and the owner's decision was first person for that release. Two separate pawn classes would duplicate movement, input and reset logic, which is the code most likely to drift.
- **Decision**: one pawn, both rigs. `AGigLineCharacter` carries a skeletal mesh (hidden in first person), a collision-tested spring-arm camera, and a **V** toggle that swaps the active camera and the body's visibility. The shipped default view stays first person; `-thirdperson` starts the pawn in the third-person rig and is what the screenshot run passes, so the review shot is reproducible without a human pressing a key.
- **Consequences**: movement, reset, pause and the QA self-test stay in one place. The body's animation state is driven from `Tick` by speed rather than from input, so being pushed or falling animates correctly. The cost is that the camera swap is not a separate actor, so a future cinematic camera needs its own path rather than reusing the toggle.
# Quality Requirements
## Quality Requirements Overview
Ordered by priority: realism first, performance last. The order decides design trade-offs; it does not license dropping a lower item silently (see the trade-off rule below the table).
| Priority | Category | Requirement |
| --- | --- | --- |
| 1 | Realism / Fidelity | Façade identity, proportion, material and lighting read like the reference photographs when compared side by side |
| 2 | Realism / Fidelity | Building placement matches the measured footprints and the photographs (within ~1 m) |
| 2 | Realism / Fidelity | Street scale (lane width, sidewalk width) within ~10 cm of the measured profile |
| 3 | Reliability / Terrain | Zero raycast holes across sidewalks and carriageway (84-sample transect) |
| 3 | Usability | WASD + mouse look, jump, reset (R), pause (Escape) — no tutorial needed |
| 4 | Maintainability | Fresh checkout builds with the documented engine installation |
| 4 | Portability | Linux build packages and runs; Windows build ships |
| 5 | Provenance | Every committed asset has a recorded source and licence |
| 6 | Performance efficiency | 60 FPS at 2560×1440, documented preset, no frame generation |
**Trade-off rule.** Realism leads: when a realism improvement costs frame time, the improvement wins. Performance is still a **floor, not a preference** — 60 FPS at 1440p without frame generation must be met at every release. If realism cannot fit inside that budget, reduce the realism scope deliberately and record the reduction in `CHANGELOG.md`; never ship below the floor without the owner's explicit approval. Walkability outranks realism in one direction: a block the player falls through is not a demo, whatever it looks like.
## Quality Scenarios
| ID | Source/Stimulus | Response | Metric |
| --- | --- | --- | --- |
| QS-1 | Screenshot is compared against its reference photograph | The render reads as the same building | Similarity score and visual match confirmed by the owner; `Tools/visual_loop.py` reports the deltas |
| QS-2 | A façade is placed from measured data | It sits on its verified footprint | World span probed against `Geo/verification/` within ~1 m |
| QS-3 | Player walks both sides of the demo | No fall-through, character stays at ground Z | 0 raycast holes in 84-sample transect |
| QS-4 | Player holds W for 2 s on flat road | Character moves forward | ≥ 200 cm displacement, Z constant within ±5 cm |
| QS-5 | Player presses R | Character teleports to spawn | < 100 cm from `StartLocation` |
| QS-6 | Player presses Escape | Game pauses, cursor appears | `IsGamePaused()` = true |
| QS-7 | Player presses Escape while paused | Game unpauses, cursor hides | `IsGamePaused()` = false |
| QS-8 | Fresh checkout + engine install | Build succeeds | Exit code 0, runnable binary produced |
| QS-9 | Game runs on dev workstation at 1440p | Frame rate holds the floor | ≥ 60 FPS, 99th percentile ≥ 55 FPS |
> QS-9 is last on purpose. Performance is a floor that every release must meet, not the goal that shapes the content: realism decides what gets built, the frame budget decides how much of it.
### Building realism gate (0.9.0)
| ID | Check | Metric |
| --- | --- | --- |
| BR-1 | A building with a reference photograph has a review shot compared against it | Silhouette and proportion deltas recorded per building in `Screenshots/README.md`; owner approval per shot |
| BR-2 | Every building is either photo-matched or explicitly recorded as an unfounded blockout | List in `TODO.md`; no building in an in-between state |
| BR-3 | Every building has a collision proxy and at least one LOD | Count of buildings missing either = 0 |
| BR-4 | No surface carries a placeholder material | Material audit in `Tools/ue_audit_scene.py` reports 0 placeholder slots |
| BR-5 | The realism pass does not break the floor | QS-9 still holds at 1440p, with the measured cost of the pass recorded |
# Risks and Technical Debts
| ID | Risk / Debt | Priority | Mitigation |
| --- | --- | --- | --- |
| R-1 | Terrain bbox proxy misdiagnosed as fall-through — already fixed, but the root-cause path (Interchange defaults to simple collision) could recur on reimport | High | `UGigLineAssetTools` runs in the build script; always reassert complex-as-simple after any reimport. Documented in `MEMORY.md` and ADR-3. |
| R-2 | Blosm 3Dsimple on Blender 5.2 — worked around, not resolved; `3Drealistic` mode untested | Medium | Pin Blosm 2.7.28 and Blender 5.2 LTS; test realistic mode before depending on it. |
| R-3 | Kerb height is an assumption (DEM does not resolve it) | Medium | Record the assumption in `Geo/verification/`; measure from reference photography if possible; mark as unresolved until verified. |
| R-5 | `GigLine SIM` / `GigLine GROUND` TEMP diagnostic logging still in `GigLineCharacter.cpp` | Medium | Strip behind a cvar or remove entirely before the `v0.1.0` release tag. |
| TD-1 | Materials are Unreal-only — no Blender preview round-trip | Low | Accepted (ADR-2). Look-dev happens in UE; screenshot review catches issues. |
| TD-2 | No CI pipeline — all builds and QA are manual on the dev workstation | Low | Acceptable for `0.x`. CI is a `0.3.0+` concern. |
# Glossary
| Term | Definition |
| --- | --- |
| Blosm | Blender add-on (2.7.28) that imports OSM data as 3D building and street blockouts. Used for layout only — not authoritative for façade or height. |
| PIE | Play In Editor — an Unreal Editor feature that runs the current map as a live game session inside the editor window, with full game code, input, and physics, without packaging a build. Used for interactive verification (walk, reset, pause) before the scripted QA self-test. |
| Complex-as-simple collision | `CTF_UseComplexAsSimple`: Unreal collision mode where the render mesh triangles are used as the collision surface, instead of a simplified proxy. Required for terrain so the player walks on the actual ground shape. |
| DEM | Digital Elevation Model. 3DEP 1 m resolution GeoTIFF covering the demo bounds. Source of terrain heightfield. |
| El Capitan | El Capitan Entertainment Centre, 6838 Hollywood Blvd. The hero building of the demo. |
| Enhanced Input | Unreal’s modern input system (UE 5+). Uses Input Actions (`IA_*`) and Input Mapping Contexts (`IMC_*`) instead of legacy axis/action bindings. |
| EPSG:26911 | UTM zone 11N, the projected CRS used for the demo area. Local origin recorded in `Geo/`. |
| Hero façade | The detailed, reference-calibrated model of the El Capitan building frontage. |
| MPFB | MakeHuman Plugin for Blender (2.0.18). Generates parametric human base meshes with rigs and baked materials. |
| PlayerStart | Unreal actor that defines where the player pawn spawns. Positioned above grade so the capsule does not embed in the floor on spawn. |
| QA self-test | The `-qaselftest` command-line switch that runs an automated behaviour check (walk, reset, pause, ground transect) via a core ticker. |
| Site | The terrain, road, sidewalks, kerbs, and crossing markings — the ground-level infrastructure of the demo area. |
| Squash merge | Git merge strategy that compresses all branch commits into one commit on `main`, keeping a linear history. |
| Swept transect | A QA technique that raycasts vertically at regular intervals along a path (e.g. every 200 cm along both sidewalks and the carriageway) and counts holes (misses). Zero holes = the block is fully walkable. |
| UE | Unreal Engine — Epic Games' real-time 3D creation platform (version 5.8.4 in this project). Comprises the Unreal Editor (including PIE, content browser, viewport, blueprint editor), a C++ game framework, and runtime systems for rendering (Vulkan RHI on Linux), physics (Chaos), and input (Enhanced Input). The game project is `GigLine.uproject`; game code is a single C++ module in `Game/GigLine/Source/GigLine/`. |
| World Partition | Unreal's world composition system for large open-world levels. Not yet used in `0.1.0` but planned for `0.3.0+` driving routes. |
| Y convention | In Unreal, south = +Y, north = −Y (confirmed from street mesh orientation). In Blender, north = +Y; the `ue_metric_v1` export preset flips Y. |
---
**About arc42**
<div data-wrapper="1" role="lead">
arc42, the template for documentation of software and system architecture.
</div>
Template Version 0.1.0. initial architecture documentation, 2026-11-08
Created, maintained and © by Dr. Peter Hruschka, Dr. Gernot Starke and contributors. See <https://arc42.org>.
Adapted for the Gig Line project (Linux-first walking/driving demo around 6840 Hollywood Blvd).
+19 -19
View File
@@ -56,9 +56,9 @@ Both content-acceptance tests run headless and are the template for all asset im
```bash
# Static-mesh scale test (Blender export -> Unreal import -> measure)
# Blender: Tools/scale_test_export.py, Unreal: Tools/ue_scale_test_import.py
# Blender: Tools/blender/scale_test_export.py, Unreal: Tools/unreal/ue_scale_test_import.py
# MPFB character (generate -> rig -> bake -> export -> skeletal import)
# Blender: Tools/mpfb_character_export.py, Unreal: Tools/ue_character_import.py
# Blender: Tools/blender/mpfb_character_export.py, Unreal: Tools/unreal/ue_character_import.py
# Unreal side of any test:
./Engine/Binaries/Linux/UnrealEditor-Cmd \
@@ -78,22 +78,22 @@ The hero block and the demo level are built by a fixed sequence. Each step write
| Street | `Art/blender/street/street_mesh.py` | `Art/blender/street/street-*.blend` |
| Site assembly | `Art/blender/site/site_assembly.py` | `Art/blender/site/site-*.blend` |
| Site export | `Art/blender/site/export_site_fbx.py` | seven FBXs, one per mesh (terrain, road, both sidewalks, both kerbs, crossing) |
| Hero façade | `Tools/hero_facade_build.py` | `Art/blender/hero/hero-facade-*.blend` + JSON part report |
| Block frontage | `Tools/export_block_frontage.py` | six frontage FBXs from the blockout |
| North buildings | `Tools/export_north_buildings.py` | TCL and Tussaud's FBXs from the blockout |
| Site import | `Tools/ue_import_site.py` | `/Game/Hollywood/Site/*` |
| Frontage import | `Tools/ue_import_block_frontage.py` | `/Game/Hollywood/Frontage/*` |
| North import | `Tools/ue_import_north_buildings.py` | `/Game/Hollywood/North/*` |
| Hero import | `Tools/ue_hero_facade_import.py` | `/Game/Hollywood/ElCapitan/SM_HeroFacade` |
| Demo map | `Tools/ue_build_demo_map.py` | `/Game/Hollywood/DemoMap` |
| Façade kit assembly | `Tools/ue_apply_facade_kit.py` | kit actors in `DemoMap` |
| Theatre trim | `Tools/ue_stage_theatre_trim.py` | marquee + blade sign actors |
| Scene audit | `Tools/ue_audit_scene.py` | materials, placement spans, labels (gate) |
| Review shots | `Tools/ue_build_review_map.py` + `Tools/render_screenshots.sh` | `Screenshots/` |
| Hero façade | `Tools/blender/hero_facade_build.py` | `Art/blender/hero/hero-facade-*.blend` + JSON part report |
| Block frontage | `Tools/blender/export_block_frontage.py` | six frontage FBXs from the blockout |
| North buildings | `Tools/blender/export_north_buildings.py` | TCL and Tussaud's FBXs from the blockout |
| Site import | `Tools/unreal/ue_import_site.py` | `/Game/Hollywood/Site/*` |
| Frontage import | `Tools/unreal/ue_import_block_frontage.py` | `/Game/Hollywood/Frontage/*` |
| North import | `Tools/unreal/ue_import_north_buildings.py` | `/Game/Hollywood/North/*` |
| Hero import | `Tools/unreal/ue_hero_facade_import.py` | `/Game/Hollywood/ElCapitan/SM_HeroFacade` |
| Demo map | `Tools/unreal/ue_build_demo_map.py` | `/Game/Hollywood/DemoMap` |
| Façade kit assembly | `Tools/unreal/ue_apply_facade_kit.py` | kit actors in `DemoMap` |
| Theatre trim | `Tools/unreal/ue_stage_theatre_trim.py` | marquee + blade sign actors |
| Scene audit | `Tools/unreal/ue_audit_scene.py` | materials, placement spans, labels (gate) |
| Review shots | `Tools/unreal/ue_build_review_map.py` + `Tools/review/render_screenshots.sh` | `Screenshots/` |
**Order matters.** `ue_build_demo_map.py` rebuilds the site materials, so any actor staged by another tool must be (re)staged _after_ it, or it keeps a reference to a material that no longer exists — that is how 604 actors once ended up untethered from their materials while the map only looked slightly flat. `Tools/render_screenshots.sh` runs the full chain in the right order and gates on `ue_audit_scene.py`.
**Order matters.** `ue_build_demo_map.py` rebuilds the site materials, so any actor staged by another tool must be (re)staged _after_ it, or it keeps a reference to a material that no longer exists — that is how 604 actors once ended up untethered from their materials while the map only looked slightly flat. `Tools/review/render_screenshots.sh` runs the full chain in the right order and gates on `ue_audit_scene.py`.
Every Blender exporter calls [`Tools/fbx_export_preset.py`](../Tools/fbx_export_preset.py) (`ue_metric_v1` / `ue_character_v1`) instead of repeating the FBX argument block — one place for the scale conversion, the axis mapping and `mesh_smooth_type="FACE"` (without which every Unreal import warns about missing smoothing groups and round geometry arrives faceted).
Every Blender exporter calls [`Tools/blender/fbx_export_preset.py`](../Tools/blender/fbx_export_preset.py) (`ue_metric_v1` / `ue_character_v1`) instead of repeating the FBX argument block — one place for the scale conversion, the axis mapping and `mesh_smooth_type="FACE"` (without which every Unreal import warns about missing smoothing groups and round geometry arrives faceted).
Two rules that the pipeline enforces rather than documents:
@@ -103,7 +103,7 @@ Two rules that the pipeline enforces rather than documents:
## Known engine quirks (2026-11-07)
- One handled `PSOPrecache` ensure fires at packaged-build exit only; not blocking.
- Interchange 5.8 does not create material/texture assets for skeletal FBX imports — `Tools/ue_character_import.py` finishes that chain explicitly.
- Interchange 5.8 does not create material/texture assets for skeletal FBX imports — `Tools/unreal/ue_character_import.py` finishes that chain explicitly.
- Android Turnkey status errors and datarouter DNS warnings in logs are benign.
## Packaged Linux build
@@ -129,13 +129,13 @@ cd /run/media/system/DATA/Builds/GigLineQA/Linux
| `Add-on not loaded: "bl_ext.blender_org.mpfb"` | `mpfb_character_export.py` needs the MPFB add-on visible to Blender | export `BLENDER_USER_SCRIPTS` (see the invocation in [../Art/blender/README.md](../Art/blender/README.md)) |
| The packaged run exits after a couple of seconds during QA | `GigLineShotActor` runs its screenshot list and quits (`bQuitWhenDone`) | it skips its rounds when `-qaselftest` is on the command line — keep that guard |
| Headless `-nullrhi` traces report no hits for everything | the null RHI has no physics scene | verify traces in the **packaged** run (`-qaselftest`), never in a commandlet |
| Import logs "No smoothing group information" for every mesh | `mesh_smooth_type="OFF"` in the FBX export | use `Tools/fbx_export_preset.py` (`FACE`) |
| Import logs "No smoothing group information" for every mesh | `mesh_smooth_type="OFF"` in the FBX export | use `Tools/blender/fbx_export_preset.py` (`FACE`) |
## PIE input test (compare editor vs packaged input intake)
Use to isolate whether the "no controls respond" bug is the packaged standalone's input intake or the Enhanced Input wiring generally.
- `Tools/run_pie.sh` opens the editor on `/Game/Hollywood/DemoMap`. Press the green **Play** (PIE) button and walk / mouse-look.
- `Tools/input/run_pie.sh` opens the editor on `/Game/Hollywood/DemoMap`. Press the green **Play** (PIE) button and walk / mouse-look.
- Interpretation:
- PIE **works** -> the Enhanced Input wiring is fine and the standalone packaged build's SDL input intake is the environment-dependent bug.
- PIE **fails** too -> the Enhanced Input setup itself is broken; rebuild the input config / character from scratch.
+3 -3
View File
@@ -19,7 +19,7 @@ Keyboard and mouse. The same list is shown in-game, bottom-right of the frame (`
- Movement is relative to where you are looking (controller yaw), so the capsule stays upright on the sloped street.
- The mouse is captured on launch, so looking works without clicking first. `Escape` toggles the capture as part of pause.
- Bindings are Enhanced Input actions in `/Game/Input` built by `Tools/ue_create_input_assets.py`; the mapping context is `IMC_Default`.
- Bindings are Enhanced Input actions in `/Game/Input` built by `Tools/unreal/ue_create_input_assets.py`; the mapping context is `IMC_Default`.
## Changes since `0.1.0`
@@ -48,10 +48,10 @@ The walking shot is driven per shot rather than by the switch: the shot actor en
## Testing input without a physical keyboard
`Tools/input_test.py` injects OS-level keyboard/mouse events through a virtual `/dev/uinput` device (see `Tools/input_emulation.py`), e.g.:
`Tools/input/input_test.py` injects OS-level keyboard/mouse events through a virtual `/dev/uinput` device (see `Tools/input/input_emulation.py`), e.g.:
```bash
python3 Tools/input_test.py --sequence move:2,look:3
python3 Tools/input/input_test.py --sequence move:2,look:3
```
It drives the focused game window exactly like real hardware, which is how the controls were regression-tested on the packaged build.
+3 -3
View File
@@ -50,7 +50,7 @@ Measured at 1440p on the development workstation: Bazzite, Ryzen 7 7700X, 64 GB
## `0.1.0` — Placement vs references — record (2026-11-08)
Checked against the reference photography (`Art/reference/`) and the Geo measurements (`Geo/verification/*`). Build under test: current `main` content, DemoMap rendered via `Tools/render_screenshots.sh` (2026-11-08, Vulkan SM5, 1920×1080); engine positions probed from the DemoMap (commandlet, 2026-11-08).
Checked against the reference photography (`Art/reference/`) and the Geo measurements (`Geo/verification/*`). Build under test: current `main` content, DemoMap rendered via `Tools/review/render_screenshots.sh` (2026-11-08, Vulkan SM5, 1920×1080); engine positions probed from the DemoMap (commandlet, 2026-11-08).
### Placement and scale (in-engine ↔ reference)
@@ -82,7 +82,7 @@ Checked against the reference photography (`Art/reference/`) and the Geo measure
- **Crossing ramps are kerb flushes, not graded ramps** — the `0.1.1` Crossing ramp gradient item.
- Site identity (the neoclassical hero with motto band) matches the 2009 façade photograph and the 1922 Masonic elevation within blockout fidelity.
Pre-check method: `Tools/judge_screenshot.py` (vision model) flagged the simplified flat surfaces as "unexpected" — those flags are the deliberate deviations above, not placement errors. Final verdict is by human review of the shots in `Screenshots/demo/`.
Pre-check method: `Tools/review/judge_screenshot.py` (vision model) flagged the simplified flat surfaces as "unexpected" — those flags are the deliberate deviations above, not placement errors. Final verdict is by human review of the shots in `Screenshots/demo/`.
## `0.1.0` — Collision and packaging — audit note (2026-11-08)
@@ -90,7 +90,7 @@ Run on the packaged build (`GigLineQA`, Development, Vulkan SM5) with `-qaselfte
- **Transect (holes/gaps):** 1456 samples over 16 rows — lane centres **plus the kerb/road/sidewalk seam Ys** (−1382, −1053, +718, +1047 and neighbours) and X −20000..−2000. Result **0 holes**. (The old 84-sample check covered only 3 lane centres; the seam rows are what this check adds.)
- **Invisible-wall probe:** horizontal traces 30 m long, grounded at 160 cm above the local ground, along the road and both seam rows. Result **0/35 blocking hits inside 21 m** — no invisible walls or misplaced blockers in the street envelope.
- **Facade occlusion probe:** from the road centre, 40 m traces sideways at walking height toward each frontage, required only where the map stages a building (the spans are listed in `AGigLineCharacter` and mirror `Tools/ue_build_demo_map.py`). Result **0/9 directions without a blocking wall** — the player cannot walk through the hero or the frontage. This probe found the hero façade had **no simple collision** at all (complex-only, so traces and capsule sweeps passed through it); it is now `UseComplexAsSimple`. Auto-generated convex hulls were tried first and rejected: on a 95-part, 26 m façade they bulged into the street (the wall probe went 0/35 → 2/35). (First pass used a fixed Z 160 start; the street is at Z ~366, so the probe started inside the earth and reported every row as a dist-0 blocker. Grounding the probe fixed that measurement bug.)
- **Facade occlusion probe:** from the road centre, 40 m traces sideways at walking height toward each frontage, required only where the map stages a building (the spans are listed in `AGigLineCharacter` and mirror `Tools/unreal/ue_build_demo_map.py`). Result **0/9 directions without a blocking wall** — the player cannot walk through the hero or the frontage. This probe found the hero façade had **no simple collision** at all (complex-only, so traces and capsule sweeps passed through it); it is now `UseComplexAsSimple`. Auto-generated convex hulls were tried first and rejected: on a 95-part, 26 m façade they bulged into the street (the wall probe went 0/35 → 2/35). (First pass used a fixed Z 160 start; the street is at Z ~366, so the probe started inside the earth and reported every row as a dist-0 blocker. Grounding the probe fixed that measurement bug.)
- **Walk / reset / pause:** 761 cm in 2 s (Z steady 456), reset 0 cm, pause `IsGamePaused()=1`, unpause 0 — all PASS.
- Packaging: the packaged build launches, reaches gameplay, and the QA run quits cleanly. The `GigLineShotActor` screenshot rounds are suppressed under `-qaselftest` so they cannot quit the run before the audit phases.
- Caveat recorded: ground Z extremes (−12 m / 3000 cm) are trace-start/terrain-edge artifacts of the widened extent, not walkable holes — the walkable corridor is continuous (walk + transect both PASS).
+1 -1
View File
@@ -91,7 +91,7 @@ Two tools talk to paid services and read their keys from the gitignored `.env`:
| Variable | Used by | Needed for |
| --- | --- | --- |
| `OPENROUTER_API_KEY` | `Tools/judge_screenshot.py`, `Tools/facade_detail_concepts.py` | vision pre-check and AI concept plates |
| `OPENROUTER_API_KEY` | `Tools/review/judge_screenshot.py`, `Tools/gen/facade_detail_concepts.py` | vision pre-check and AI concept plates |
| `MAPBOX_ACCESS_TOKEN` | `Geo/tools/mapbox_buildings.py` | the second building-height source |
Nothing in the build, the tests or the release path needs either key. Never commit `.env`; `git check-ignore .env` should succeed.
+11 -3
View File
@@ -34,12 +34,20 @@ Unreal Engine 5.9 is in preview and Unreal Engine 6 has been announced as of Nov
| Package | blender-skills `1.3.0`, upstream commit `8f778d2`, MIT |
| Source | <https://github.com/arjun988/blender-skills> |
| Location | Local only, common root: `~/.pi/agent/skills/` (93 packs, `references/`, `LICENSE`) — not distributed with the repository |
| Project-specific | `.agents/skills/blender-skills/blender-director/` in this repository |
| Project-specific | `.agents/skills/blender-skills/blender-director/` (gitignored, not committed) |
| Installed | 2026-11-06, copied from a local checkout of `.claude/skills/` without edits; flattened to the common root on 2026-11-10 |
| UE skills | `quodsoler/unreal-engine-skills` `f3742d7`, MIT — 31 packs at `.agents/skills/ue-*/` (gitignored), target UE 5.8 C++ |
| C++ skill | `DiamonDinoia/skill-cpp` `f2617fb`, MIT — `cpp` router at `.agents/skills/cpp/` (gitignored) |
These are instructions and reference notes for agents, not code that runs in a build. Four of them assume a **Blender MCP server**; this project drives Blender headlessly through `bpy` (`Tools/*.py`) instead, so MCP is optional and nothing depends on it.
All skill packs — Blender, Unreal and C++ — are agent instructions, not code that runs in a build, and **none are committed**: `/.agents/skills/` is gitignored and the Blender packs live in the common `~/.pi/agent/skills/` root. Four of the Blender packs assume a **Blender MCP server**; this project drives Blender headlessly through `bpy` (`Tools/*.py`) instead, so MCP is optional and nothing depends on it.
To update: copy `.claude/skills/` from a newer upstream checkout over the local directory, then record the new version and commit hash in this table. Skills are read on demand, so the harness catalog may need a reload before they appear as first-class entries.
To update a pack:
- **Blender** — copy `.claude/skills/` from a newer `arjun988/blender-skills` checkout over `~/.pi/agent/skills/`, then record the version and commit in the table above.
- **Unreal Engine** — re-install the `quodsoler/unreal-engine-skills` git source at the new commit (`pi install <source-url>@<commit>`); the 31 packs land under `.agents/skills/ue-*/`. Record the commit in `skills.manifest.json` and the table.
- **C++** — re-install the `DiamonDinoia/skill-cpp` git source at the new commit; the router lands at `.agents/skills/cpp/`. Record the commit in `skills.manifest.json` and the table.
`skills.manifest.json` (in `.agents/skills/`) is the install record — the git sources, commits and layout — and is the thing to keep in step with the table. Skills are read on demand, so the harness catalog may need a reload before they appear as first-class entries.
## Pin rules
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.
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 337 files, more files were not shown because too many files have changed in this diff. Show more