137 Commits
Author SHA1 Message Date
zwitschi 82cafc2f29 fix(camera): prove the boom shortens, and record how to repeat it
With the spring arm activated the collision now works: a 2 m wall spawned 4 m behind the
pawn shortens the boom from 340 cm to 288 cm - 12 cm clear of the wall, matching the probe
size. -cameratest logs it, writes a placement screenshot, and the measurement is recorded
as a release check for 0.2.3.

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

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

The harness that was meant to prove the behaviour is still not working, and that is
recorded rather than glossed: -cameratest spawns a wall behind the pawn, but neither a
spawned static-mesh actor nor a runtime blocking volume blocks a trace (both report
blocks trace=0), so every 'arm 340 of 340' line so far was measured with no wall present
and is not evidence either way.
2026-11-09 14:37:03 +01:00
zwitschi a6701aa3d1 test(camera): -cameratest harness for the spring-arm collision, result inconclusive
0.2.3 is about camera clipping, and nothing had ever exercised the spring arm's
collision test. This adds a harness: find the nearest wall from the spawn, stand the pawn
70 cm off it facing away, wait for the boom, then report the arm length, whether the boom
ends inside geometry, and a back-trace from camera to head.

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

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

Next: look at the placement screenshot before trusting the measurement.
2026-11-09 14:33:57 +01:00
zwitschi d8b44f1e57 Merge pull request #36: v0.2.2 — the character pass
Clothing, hair cards, character blueprints and render-vs-blueprint comparison, raised sun, walk cycle starting at maximum stride. All eight review shots approved.
2026-11-09 14:26:22 +01:00
zwitschi 6c4e0701fb release: v0.2.2 — the character pass
Writes the 0.2.2 entry and prunes the section from TODO.md. The entry records that this
ships as a patch by the owner's decision even though it adds asset categories, which the
policy otherwise puts in a minor release - so the numbering is not mistaken for scope.

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

Verified: 8/8 shots, all approved; mean deltaE 31.9 -> 21.8 against the blueprint; 95
tool tests. Known limitations listed rather than omitted - garments are shells over body
faces, the baked diffuse cannot be used because the mesh UVs do not map to it, and the
character has no face detail.
2026-11-09 14:26:13 +01:00
zwitschi 0e7a6e898b docs(review): owner approved shots 6, 7 and 8 — all eight shots approved
Ticks both the shot sections and their status rows, so the table and the sections agree:
5 previously approved, these 3 approved 2026-11-09 in their clothed, haired, relit state.
2026-11-09 14:25:35 +01:00
zwitschi 2f6d06f897 docs(review): restore the status rows for shots 7 and 8
The previous documentation commit dropped both rows from the status table while editing
the section text - the table then listed six shots against eight embeds. Restored, both
marked verified and pending approval.
2026-11-09 14:24:02 +01:00
zwitschi 5de4183456 docs(review): shots 6, 7 and 8 reflect the clothed, haired character
The appearance changed in 0.2.2 and the README still described an unclothed body with
splayed fingers, which is a defect in its own right: a shot whose README section is
stale is not a review.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

8/8 shots verify.
2026-11-09 12:55:53 +01:00
zwitschi 0d57b718cf Merge pull request #34: 0.2.0 + 0.2.1 — third-person body, animation, rig fixes
Third-person body with animated idle/walk, review harness for a deterministic stride capture, F3 debug overlay, and the 0.2.1 rig fixes (curled fingers, derived arm angle).
2026-11-09 12:53:28 +01:00
zwitschi e9f31f271e release: v0.2.1 — rig fixes changelog entry, TODO pruning
Writes the 0.2.1 entry (fingers, derived arm angle, base keys) and removes the
section from TODO.md. The stride-capture regression is listed under known
limitations rather than omitted: the shot is retired, not shipped.
2026-11-09 12:52:01 +01:00
zwitschi 03af8146a6 fix(rig): curl the fingers, derive the arm angle
Two defects from the 0.2.0 review, both fixed and both measured.

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

Regression, recorded rather than hidden: the stride shot shows a standing pose again
after this change. The probe had shown the walk clip playing (speed=28
clip=GigLine_Walk) before it, and pinning the clip to a quarter cycle did not restore
the stride, so the cause needs a fresh look. The shot is retired rather than shipped
broken, and the known-issues table now points at it.
2026-11-09 12:48:40 +01:00
zwitschi 30e4d65743 todo: claim 0.2.1 rig fixes — splayed fingers, derived standing pose 2026-11-09 12:43:25 +01:00
zwitschi bc2e9c3b8e feat(hud): F3 debug overlay — FPS, frame time, worst frame, state
A top-left readout for performance work: smoothed FPS and frame time, the worst
frame in a rolling two-second window, the active view, player speed, a frame
counter and the player's position in metres from the project origin.

- F3 toggles it, through the console variable gigline.Debug so a packaged build can
  be checked without a rebuild (IA_ToggleDebug).
- On automatically for -perf and -qaselftest, so a performance capture never depends
  on pressing a key first; off in the built demo, because a player should not see an
  FPS counter over Hollywood Boulevard.
- The first two seconds are a warm-up window excluded from the worst-frame figure.
  Without it the readout showed "worst 400 ms" for a healthy run: the startup shader
  and streaming hitches dominated the rolling maximum.

Measured cost on the self-test route at 1440p: 2.64 to 2.77 ms average, about 5 %,
still 6x the floor. The release-check number stays the overlay-off run; the
overlay-on series is committed beside it as
Docs/perf/0.2.0-walk-frametimes-overlay.csv.

Docs: controls (F3 row, overlay section, switch table), release checks (cost row),
architecture building block view (HUD and input list).
2026-11-09 12:41:01 +01:00
zwitschi 12272d6b96 release: v0.2.0 — third-person body, animation, stride capture
Writes the 0.2.0 changelog entry with the measured numbers, removes the completed
section from TODO.md (its goal stays in ROADMAP.md) and updates the roadmap status.

Release checks: 8/8 shots verify with 6, 7 and 8 approved; 1440p at 2.64 ms average
(379 fps); -qaselftest passes walk, reset, pause, transect, wall and facade probes;
82 tool tests pass. Known limitations are listed rather than hidden: splayed fingers,
a swept base rotation and unproven camera collision, all marked for later patches.
2026-11-09 12:30:49 +01:00
zwitschi 56e104f5f3 todo: close the 0.2.0 animation check, leaving only the tag
Both clips play and advance (idle proved by the single-node instance's asset time,
walk by shot 8), the body carries its skin material and the QA self-test passes.
The two cosmetic defects — splayed fingers and a base rotation that was swept rather
than derived — stay open as 0.2.2 items and are named in the check.
2026-11-09 12:30:00 +01:00
zwitschi 8e880ed09d perf: 0.2.0 re-measured at 1440p — 379 fps average, 6.3x the floor
-qaselftest at 2560x1440, TSR on, VSync off, no frame generation: 759 frames,
avg 2.64 ms (379 fps), p50 2.54, p95 3.39, p99 3.76, max 4.05 ms. The body, skin
material and animation add about 2 % over 0.1.5's 2.61 ms.

The same run passed every QA check: walk, reset 0 cm from spawn, pause/unpause,
transect 1456 samples with 0 holes, wall probe 0/35, facade probe 0/9.

Series committed as Docs/perf/0.2.0-walk-frametimes.csv and the result recorded in
the release-checks table next to the 0.1.0 and 0.1.5 numbers.
2026-11-09 12:29:46 +01:00
zwitschi 9ce7011389 docs(input): the V toggle, the harness switches, and the HUD line
Docs/controls.md documented the 0.1.0 bindings only, so the camera toggle added with
0.2.0 was undocumented for the player-facing set and the review switches were
undocumented everywhere. Adds the V row, a changes-since-0.1.0 note that first person
stays the default, and a table for -thirdperson / -walk / -qaselftest with the note
that the walking capture is driven per shot rather than by the switch.

The in-game HUD line gains 'V view' so the packaged build documents it too.
2026-11-09 12:21:11 +01:00
zwitschi f5e7e66127 docs: remove the 0.1.4 review record and the 0.1.0 packaged capture
Both were owner-requested removals. Nothing else referenced the review file except a
MEMORY entry, which now says the per-shot Approved column of Screenshots/README.md is
the only and authoritative record; release-checks keeps the 0.1.0 packaged-build
measurement as numbers and notes that its capture is no longer in the repository.
2026-11-09 12:20:36 +01:00
zwitschi dbb177da6f feat(walk): the stride is captured — walking shot 8, traversal check closed
Shot 8 shows the body mid-stride in profile: legs apart, arms counter-swinging. The
traversal release check for 0.2.0 is closed with that evidence.

The reason earlier attempts froze on one pose was not the clip. A probe of the
single-node instance showed the clip advancing normally (assetTime 0.19 -> 1.03,
rate 1.0, playing) while the pawn's speed stayed 0 and the clip stayed Idle: the
call that enables walking sat in the shot sequencer's BeginPlay, which runs before
the player pawn exists, so the cast found nothing and silently did nothing. Moved
into the per-frame follow update, the same run logs walk=1 speed=28 clip=GigLine_Walk
and the stride appears.

Also: the walk expectation was missing from visual_targets.json after the earlier
retirement, so --verify was checking 7 shots while 8 were captured. Restored; 8/8
verify.
2026-11-09 12:18:22 +01:00
zwitschi eeeabe56b1 feat(review): deterministic walking capture, and the defect it exposed
The shot actor can now photograph a moving pawn: a per-shot ShotFollow flag re-places
the camera every frame at a fixed offset in the pawn's frame, and the walk is driven
per shot (SetReviewWalk) with the pawn returned to spawn after a follow shot, so the
fixed-camera shots later in the list still find it in place. With that, the capture
stopped depending on where a 4.5 m/s pawn happened to be.

Two further bugs fixed on the way:
- the engine's screenshot folder is cleared before each run. The collect step copies
  every review-*.png it finds in Saved/, so a shot retired from the camera list kept
  reappearing with byte-identical content from an older run - which is how a walk shot
  that was never captured looked like a captured one.
- UpdateBodyAnimation no longer calls SetAnimation every frame. Re-setting the same
  clip restarts it, which freezes the body on frame 0.

What the deterministic capture then exposed: the walk clip is applied but does not
advance - every walking frame shows the same pose (arms forward, legs together). The
stride shot is retired again rather than shipped, and TODO.md names the next step:
log GetCurrentAssetTime across frames and check the imported PlayRate. The harness
stays wired for when the clip plays.

Shots 6 and 7 were re-rendered after these changes and still pass; 7/7 verify.
2026-11-09 12:11:24 +01:00
zwitschi f84093adba feat(walk): a -walk harness flag, and the honest reason the stride shot is not in
Screenshots 6 and 7 are approved, so the remaining 0.2.0 work is the traversal
release check. This adds the piece of harness it needs - `-walk` holds forward input
from the first frame, so the pawn actually crosses the sidewalk while the third-person
rig follows it - and records what that check still lacks.

A stride shot was attempted three times and removed: a fixed review camera cannot
catch a pawn moving at 4.5 m/s. Each attempt framed it at a different distance and
the last two missed it entirely, so shipping it would have been a broken shot with a
green verification. The TODO item now says the blocker is the capture harness (a
camera that tracks the pawn), not the game, and names where the shot goes back in.

The flag stays because it is what the traversal check will drive.
2026-11-09 11:50:07 +01:00
zwitschi 0e0aed02d6 review: tick shot 6 in the status row too 2026-11-09 11:43:29 +01:00
zwitschi 7ea3c1999c review: owner approves the two third-person shots (6 and 7) 2026-11-09 11:43:23 +01:00
zwitschi 46b84884b8 fix(character): the arms hang at the sides — and why that took so long
The character now stands naturally: arms at its sides with a soft elbow bend, hands
at the hips, feet flat. Both character shots confirm it, front and back.

The root cause was not the pose. The FBX importer had been silently refusing to
replace the existing assets: after changing the arm base from 1.55 rad to 0.55 rad
and rebuilding, the imported clip returned the IDENTICAL bone quaternion
(+0.006, +0.002, -0.179, -0.984). replace_existing=True was set and delete_asset
returned False even after chmod u+w, because the pawn's constructor hard-references
the clips. Importing under a new name (CharacterV2) created fresh assets and the
same probe then returned (+0.439, +0.898) — the rotation had landed. Every earlier
attempt had been invisible for this reason.

The rotation itself is measured, not inherited: sweeping it in Blender gives 0.55 rad
for arms down and clear of the body, 0.65 for straight down against the hip, 0.75 for
past vertical and into the torso. It lives in the clamp keys, not in the bind: every
bind experiment was impossible on this shape-keyed MPFB mesh or left Unreal showing
the old bind.

Remaining, for 0.2.2: the fingers splay from the bind, and the walk clip has still
not been captured in motion.
2026-11-09 11:42:36 +01:00
zwitschi 8993fd83c4 feat(body): pose the arms down, add a front-angle shot
The character now stands with its arms at its sides instead of in a bind A-pose,
and there is a second camera to prove it: the third-person shot only ever showed
the pawn's back, so a front-facing pose defect was invisible in review.

  demo-third-person-front  camera on the road, ~6 m in front, looking back north
                           at the pawn; embedded in Screenshots/README.md as shot 7.

The pose work itself is a chain of measured findings, all recorded in TODO.md:

- clip data is exported relative to the REST pose, so arms-down offsets held only in
  the keys arrived as a ±20 degree swing around an A bind;
- a rest-pose bake alone does not move the mesh: the MPFB mesh has shape keys, the
  armature modifier cannot be applied while they exist, and the vertices stayed in
  the A shape (X span unchanged at 1.051 m). Clearing the shape keys, baking the
  deformation into the vertices and re-binding is what made bind and clips agree.

Not settled, and left as an open item rather than a claim: the base pose uses
inverted signs to compensate for the FBX axis mapping, the upper arms clip into the
torso, and the fingers splay. An aim-based pose module was written and then removed
- applied without the vertex bake it produced arms raised in a V, and a half-working
shared module is worse than none. The proper solve is 0.2.2: apply the standing pose
at character creation, then author clips against that bind.
2026-11-09 11:28:29 +01:00
zwitschi a1f5989cbb fix(anim): the clip now drives the body — two causes, both measured
The body stood in an A-pose while the log said the clip was playing. Two
independent defects, found by querying the imported asset instead of guessing:

1. PlayAnimation needs the component in AnimationSingleNode with a live
   single-node instance. The body is hidden at construction and only unhidden by
   the camera toggle, so the first call could land before the component was ready,
   and the Wanted == PlayingAnimation guard then returned forever. UpdateBodyAnimation
   now sets the mode, calls SetAnimation + Play, and re-asserts while the instance
   is missing.
2. The arms-down offsets lived only in the clip keys. The exporter writes clip data
   relative to the rest pose, so they arrived as a ±20 degree swing around an
   A-posed bind - which is exactly what the pose probe measured. apply_base_pose now
   bakes them into the rest pose before any clip is authored, the same trick
   mpfb_character_export.py --pose idle uses. The table is applied with inverted
   signs: as written, the FBX axis mapping produced arms raised in a V.

Shot 6 confirms it: the arms hang at the sides. Still open and recorded - the idle
pose reads awkwardly (arms forward, hands splayed) and the walk clip has no shot in
motion; both are marked in TODO.md and Screenshots/README.md.

Instrumentation removed; the diagnostics that remain are behind
gigline.SetupLogs.
2026-11-09 10:56:43 +01:00
zwitschi d0f94dff4e memory: record the bind-pose defect and the FBX that is not the cause
Re-importing the exported FBX gives two actions with 1379 f-curves over 137 bones,
so the animation is baked; the defect is engine-side. Records that PlayAnimation
needs AnimationSingleNode mode and that the clip-name log line proves intent, not
effect.
2026-11-09 10:45:07 +01:00
zwitschi 50e023f15d merge: bring main (v0.1.6) into anim-body-0.2.0
Conflicts resolved by union, not by picking a side:

- AGENTS.md keeps the stronger visual-review rule (README updated with every visual
  change; a stale section is a defect).
- render_screenshots.sh keeps the absolute engine path from main and adds
  -thirdperson from this branch - both were needed and neither alone worked.
- Screenshots/README.md now lists six shots, retail and third-person both embedded;
  the known-issues tables merged, with the animation defect and unproven camera
  collision added to main's rows.
- visual_targets.json carries both expectations.
- Architecture docs: ADR-8 (realistic buildings) and ADR-9 (third-person body) both
  present and in numeric order; ADR-8 was mis-placed before the Quality Requirements
  heading in the combined document by an earlier edit, now fixed.
- Binary conflicts resolved to main's content, since the retail trim only exists
  there.

Checks: 82 tool tests, prettier clean, six embeds resolve.
2026-11-09 10:43:00 +01:00
zwitschi e487655e2f Merge pull request #33: 0.1.6 — retail frontage (Lee Drug, 6806–6820)
Patch release v0.1.6: retail frontage modelled from its own photograph, 0.9.0 realistic-buildings milestone planned, screenshots embedded inline.
2026-11-09 10:41:10 +01:00
zwitschi 304c1e9afd docs(review): embed every demo shot inline, record the rule in AGENTS
Screenshots/README.md on this branch still used the path-only table, so a reviewer
had to open six PNGs to judge a pass. Every shot now has its own section with the
image embedded, its view, expected content and both verdicts, plus a status table
at the top that links to them.

The known-issues list becomes a table of issue -> version and item that owns it:
pale hero stone, hero entrance doors, the bind-pose animation defect, unproven
camera collision and the terrain-as-pavement strip all point at their TODO owner.
Two stale entries removed: the theatre marquee exists (six trim actors staged) and
'materials not yet created' contradicted the scene conventions.

AGENTS.md now says the README is updated with every visual change, that the image
is embedded inline, and that approval ticks both the section and the table row.

Also removed an orphan: review-demo-retail-frontage.png had been re-copied into
this branch by the capture step although its geometry only exists on
facade-retail-0.1.6.
2026-11-09 10:35:44 +01:00
zwitschi 05c1488c00 feat(camera): third-person rig, V toggle, visible body
One pawn, both rigs (ADR-9): a skeletal body hidden in first person, a
collision-tested spring arm, and a toggle that swaps the active camera and the
body's visibility. Shipped default stays first person; -thirdperson starts the
pawn in the third-person rig so the review shot is reproducible, and the
screenshot run passes it.

Three defects found and two fixed:
- The skin material had no SkeletalMesh usage flag, so Unreal silently substituted
  the default grey. That is why the body rendered as clay.
- render_screenshots.sh still called ./Engine/... on this branch (the fix lives on
  the facade branch and never reached main); applied here so the shots generate.
- OPEN: the idle clip is bound and PlayAnimation is called - the run logs the clip
  name - but the body still renders in bind pose, arms out. Recorded with that
  evidence in TODO.md rather than called done.

Shot 5 (demo-third-person) added and documented; camera collision is implemented
but unproven, since the spawn has open space behind it.
2026-11-09 10:33:06 +01:00
zwitschi 96238bdb6a feat(anim): idle and walk clips on the MPFB rig
Two parametric cycles authored at 30 fps instead of hand-keyed poses, so the loop
closes by construction: GigLine_Idle (60 f, breathing sway, feet planted) and
GigLine_Walk (30 f, one stride, hip 0.45 rad / knee 0.55 / arm 0.32, arms
counter-phased, torso counter-rotation).

The limb axes were measured, not assumed: a probe rotated upperleg01.L by 0.5 rad
on each axis and read foot.L, which moved -0.022 -> +0.362 in Y on X, twisted on Y
and abducted on Z. Using the wrong axis here would have produced a character
scissoring sideways, which no import error would have caught.

The character export preset now bakes animation. Without bake_anim the exporter
writes a static bind pose and Unreal imports a mesh with no AnimSequence and no
error, so the importer asserts a clip exists and prints VERDICT: PASS/FAIL.

Verified in /Game/Test/Character: Idle 1.967 s and Walk 0.967 s, both AnimSequences,
skeleton intact. Playback on a possessed pawn is not yet verified: the demo pawn has
no visible body, so that check belongs with the third-person camera item.
2026-11-09 10:14:05 +01:00
zwitschi 817313aabd todo: claim 0.2.0 character setup on branch anim-body-0.2.0 2026-11-09 10:11:10 +01:00
zwitschi 744bf8f357 release: v0.1.6 — retail frontage changelog entry, TODO pruning
Writes the 0.1.6 entry, removes the completed section from TODO.md (its goal stays
in ROADMAP.md per the convention), and updates the roadmap status line.

Release checks: 5/5 shots verify, scene audit 0 failures, 82 tool tests, LFS check
clean, owner approval recorded for the retail shot.
2026-11-09 10:10:50 +01:00
zwitschi c72bc41af1 review: owner approves the 0.1.6 retail frontage shot
Shot 5 approved 2026-11-09 (second pass). All five demo shots verified and approved;
the --verify item in 0.1.6 is closed. Only the tag remains.
2026-11-09 10:09:40 +01:00
zwitschi 803e77473c docs: give every known cosmetic issue a version, move blocked work out of 0.1.6
Screenshots/README.md's known-issues list had no tickets, so the same defects kept
resurfacing as surprises. It is now a table with the issue, the version and item
that owns it, and a state.

Two entries were stale and are removed: the El Capitan Theatre marquee and blade
sign have been staged since ue_stage_theatre_trim.py landed (six trim actors in the
demo map), and 'materials are not yet created' contradicted the scene conventions
in the same file.

Moved out of 0.1.6, which is a patch and cannot carry new scope:
- wall lettering decal, moulded parapet profile, tower stepping and portrait
  panels as textures -> 0.9.0 per-building models
- 6822 Budget (blocked: no photograph exists) -> 0.9.0 photography gap
The patch keeps what a patch can do: the trim that reads, the verified shot, the tag.

Filed with their proper owners: hero entrance doors and central-bay positioning ->
0.9.0 hero item; pale hero stone -> 0.9.0 measured material library; pavement
between kerb and building line -> 0.6.0 visual polish.

The 0.1.6 claim is released: nothing left on this branch but the owner's approval
and the tag.
2026-11-09 10:07:27 +01:00
zwitschi 3942e9a215 feat(facades): 0.1.6 retail second pass — relief, material mapping, framing
What the first pass got wrong, and what changed:

- The mural plates were placed 0.20 m INSIDE the solid blockout volume, so they
  never rendered. Nothing can be recessed into a solid we do not cut: the inset is
  now faked by a proud frame with a near-flush plate, and the row reads.
- Detail sat 0.05 m off the wall, under a pixel of depth at 40 m. Trim planes now
  stand 0.10-0.20 m proud, and the parapet became two tiers with deeper reveals.
- 6806's parapet started at 0.62 H and left a 4.6 m blank band; the window storey
  and parapet moved up to follow the photograph.
- Security bars on the upper windows, twelve fluted fins on the corner tower.
- The trim's stucco/white/glass slots are mapped onto the demo's look materials, so
  the walls sit in the same noise/texture family as the rest of the block; the
  colour slots sampled from the photograph stay as authored.
- Review camera pulled back and widened to 66 deg so the east end is in frame.
- The scene audit caught duplicate RetailTrim_ actors: the demo builder's clear list
  did not include the new label, so every rebuild stacked another copy.

Still open and not fixable by geometry: wall lettering. A plate at 0.10 m does not
read at 40 m, so it moves to the 0.9.0 decal route (TODO.md, Screenshots/README.md).
2026-11-09 09:54:33 +01:00
zwitschi 4132f42ca1 plan: give realistic buildings their own minor milestone (0.9.0)
The facade work started as a patch (0.1.4) and the 0.1.6 retail pass showed what
that costs: a patch has no release gate of its own, so a first pass that does not
yet read as the building can sit inside a done patch series. Realism is also the
leading quality goal, which a patch cannot carry.

New 0.9.0 - Realistic buildings, before the feature freeze: facade system with more
than one order, a 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. Former 0.9.0 freeze/beta/audit becomes 0.10.0 with
patches 0.10.1-0.10.3; 1.0.0 moves to weeks 39-44.

Recorded as ADR-8 (append-only) with a measurable gate BR-1..BR-5 in the quality
requirements, mirrored into the combined architecture.md. The 0.1.6 item now says
which of its gaps move to 0.9.0 and why more geometry cannot close them.
2026-11-09 09:54:15 +01:00
zwitschi c53386fcf4 docs(review): embed every screenshot inline so the review happens in the file
Each shot now has its own section with the image embedded, its view, expected
content and both verdicts, plus a status table at the top that links to them.
A path alone was not a review: the owner had to open five PNGs to judge a pass.

Also recorded in AGENTS.md (a path is not a review; approval ticks both the section
and the table row), the two retail assets and tools in the arc42 building block
view and the combined architecture.md, and prettier-formatted the changed files.
2026-11-09 09:33:23 +01:00
zwitschi 2614d259aa feat(facades): 0.1.6 retail frontage first pass — Lee Drug and 6806-6820
Models the two retail buildings beside the hero from their own photograph rather
than the generic kit: bronze cornice, mural row, lettering band, stepped Deco
parapet and rounded corner tower on 6800 Lee Drug; burgundy fascia, pilasters,
white-framed windows and banded parapet on 6806-6820. Reference analysis written
before any geometry (Art/blender/retail/ref-analysis.md), every unmeasured
proportion carried into the part report as an assumption.

6822 Budget is deferred, not guessed: no photograph of it exists and the 6.0 x 5.5 m
sliver cannot be read out of the neighbour shot.

Pipeline fixes found on the way:
- render_screenshots.sh called ./Engine/... for the capture step while every
  commandlet used the absolute path; set -e killed the script at 127 *after* the
  maps were rebuilt, so screenshots silently stayed a day old.
- the facade kit applies to every Frontage_*/North_* actor, so it pasted its own
  language over the new modelled facades; it now skips them, and the trim stages as
  RetailTrim_*.
- pure bay arithmetic moved to Tools/_common.py so it is unit-testable without
  Blender (4 new tests, 82 total pass).

Known gaps recorded in Screenshots/README.md, not hidden: the parapet reads as a
blank band, murals and lettering do not resolve at review distance, the tower is a
plain cylinder, and the review camera crops the east end.
2026-11-09 09:10:49 +01:00
zwitschi 4e703fcfd9 todo: claim 0.1.6 retail frontage on branch facade-retail-0.1.6 2026-11-09 08:59:07 +01:00
zwitschi 73aca9db80 feat(facades): 0.1.5 — hero detail, scene audit, material orphaning fixed (#32)
Merged PR #32: 0.1.5 — hero detail, scene audit, material orphaning fixed

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 22:56:08 +01:00
zwitschi 743a290ac9 Merge pull request 'docs(arch): realism leads the quality list; performance moves to the end' (#31) from arch-quality-priority into main
Reviewed-on: #31
2026-11-08 20:49:20 +01:00
zwitschi 71c7ca734b docs(arch): realism leads the quality list; performance moves to the end
Owner decision: prioritise realism, keep the performance target but put it last.
Re-applied on a branch after the first attempt landed directly on main (reverted
there; see b8209ce).

- Quality goals (01 + combined architecture.md): 1 visual realism, 2 geographic
  fidelity, 3 walkability/usability, 4 reproducibility/portability, 5 provenance,
  6 performance efficiency.
- Quality requirements (10 + combined) reordered to match, with two realism
  requirements added (a façade reads like the photographs; placement within ~1 m)
  and the scenarios renumbered so QS-1 is the reference comparison, QS-9 the frame
  rate.
- Guard on both documents so the order cannot become an excuse: performance is a
  floor, not a preference - 60 FPS at 1440p without frame generation must be met at
  every release; if realism cannot fit, cut the realism scope deliberately and
  record it, never the frame rate. Walkability outranks realism in one direction: a
  block you fall through is not a demo.
- Solution strategy and 'quality achievement' lead with realism and describe
  performance as a budget, not a driver.
- Docs/release-checks.md: the performance check is the last section and the per-
  version check list now opens with realism and closes with performance.
- ROADMAP gains a 'Quality priority' section; AGENTS.md gains the tie-break rule
  beside the existing scope-cutting rule.
2026-11-08 20:37:32 +01:00
zwitschi b8209ce82b Revert "docs(arch): realism leads the quality list; performance moves to the end"
This reverts commit a1d52aa583.
2026-11-08 20:37:19 +01:00
zwitschi a1d52aa583 docs(arch): realism leads the quality list; performance moves to the end
Owner decision: prioritise realism, keep the performance target but put it last.

- Quality goals reordered (01 + combined architecture.md): 1 visual realism,
  2 geographic fidelity, 3 walkability/usability, 4 reproducibility/portability,
  5 provenance, 6 performance efficiency.
- Quality requirements (10 + combined) reordered to match, with two new realism
  requirements (façade reads like the photographs; placement within ~1 m) and the
  quality scenarios renumbered so QS-1 is the reference comparison and QS-9 the
  frame rate.
- Both documents carry the same guard so the order cannot become an excuse:
  performance is a floor, not a preference - 60 FPS at 1440p without frame
  generation must be met at every release; if realism cannot fit, the realism scope
  is cut deliberately and recorded, never the frame rate. Walkability outranks
  realism in one direction: a block you fall through is not a demo.
- Solution strategy and 'quality achievement' now lead with realism and describe
  performance as a budget rather than a driver.
- Docs/release-checks.md: the performance check is the last check, with that reason
  stated; ROADMAP gains a 'Quality priority' section; AGENTS.md gains the tie-break
  rule beside the existing scope-cutting rule.
2026-11-08 20:37:03 +01:00
zwitschi 5e2c5e29c0 feat(facades): 0.1.4 — module kit, textured materials, theatre trim (#30)
Merged PR #30: 0.1.4 — facade module kit, textured materials, theatre trim

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 20:29:11 +01:00
zwitschi d6d3d54810 fix(tools): importers clear the read-only LFS bit before saving; 0.1.3 docs refresh (#29)
Merged PR #29: v0.1.3 packaging and setup fixes

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 18:53:48 +01:00
zwitschi ba68dcfdbc feat(hero): v0.1.2 — scale calibration, façade detail, export preset refactor (#28)
Merged PR #28: v0.1.2 — scale calibration, façade detail, export preset

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 18:48:33 +01:00
zwitschi 640bff9631 screenshots: re-render the whole set after the 0.1.2 height calibration (review) (#27)
Merged PR #27: 0.1.2 screenshot set (approved)

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 16:44:00 +01:00
zwitschi 5ffe62f6aa feat(geo): adopt Mapbox building calibration (3.1 m per level) (#26)
Merged PR #26: adopt Mapbox building calibration

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 16:38:28 +01:00
zwitschi 81f7cd3991 feat(geo): cross-check block heights against Mapbox building data (#25)
Merged PR #25: Mapbox building-height cross-check

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 16:31:39 +01:00
zwitschi 68d8745534 docs(agents): tooling + architecture-upkeep rules, with tool tests (#24)
Merged PR #24: tooling + architecture-upkeep rules, tool tests

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 16:25:35 +01:00
zwitschi 420f863999 fix(geo): the three "guessed" block heights are a roof, a roof and a bus shelter (#23)
Merged PR #23: block height classification + hero proportions recorded

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 16:20:46 +01:00
zwitschi 6a778dbc63 release: v0.1.1 — changelog entry, TODO pruning (#22)
Merged PR #22: v0.1.1 release prep

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 16:19:30 +01:00
zwitschi e75e5500a7 fix(street): 0.1.1 collision patch — flush sidewalk edge, graded crossing ramps (#21)
Merged PR #21: 0.1.1 collision patch (flush sidewalk edge, graded ramps)

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 16:19:11 +01:00
zwitschi 0dd4a55c87 release: v0.1.0 — changelog, TODO pruning, notices statuses (#20)
Merged PR #20: v0.1.0 release prep

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 16:08:48 +01:00
zwitschi 88b0149bfa screenshots: regenerate set after placement release check (review pending) (#15)
Merged PR #15: regenerated screenshot set (approved)

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 16:08:14 +01:00
zwitschi 47bf3a2415 docs: fill the traversal row in the 0.1.0 check-record table (#19)
Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 15:22:57 +01:00
zwitschi 3a1db78473 qa: package + document controls (0.1.0 release check PASS) (#18)
Merged PR #18: package + document controls

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 15:22:34 +01:00
zwitschi b27d30d556 qa: fresh-checkout build PASS (0.1.0 release check) (#17)
Merged PR #17: fresh-checkout build PASS

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 15:19:21 +01:00
zwitschi 4bc41e9faf perf: capture 1440p frame times; PASS the 60 FPS release check (#16)
Merged PR #16: 1440p perf capture (60 FPS check PASS)

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 15:17:47 +01:00
zwitschi 31f073d608 qa(diag): gate setup logs behind gigline.SetupLogs cvar (#14)
Merged PR #14: gate setup logs behind cvar

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 15:01:33 +01:00
zwitschi 71acffec12 qa(collision): no-blocking-collision release check — seam + wall audit (#13)
Merged PR #13: no-blocking-collision release check

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 14:55:58 +01:00
zwitschi 82a408bc33 qa(placement): record release-check 2 evidence + fix north-buildings camera (#12)
Merged PR #12: release-check 2 placement evidence + camera fix

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 14:43:15 +01:00
zwitschi 7e9573a735 todo: move deferred Blosm blockout-cleaning item into 0.1.4 (#11)
Merged PR #11: move deferred Blosm item into 0.1.4

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 14:33:26 +01:00
zwitschi 95011d678a docs: close ODbL review and finish active TODO items (#10)
Merged PR #10: close ODbL review and active TODO items

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 14:31:07 +01:00
zwitschi abdd7d181f qa(ref): close the reference-photography task (door recess + night gaps, concept copy) (#9)
Merged PR #9: close the reference-photography task

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 14:28:43 +01:00
zwitschi 16c2bf7c05 todo: update for current state — asset production, collision pass, docs, ODbL
- Remove stale -active suffix from Asset production heading
- Shorten the sidewalks/collision item (details in MEMORY.md)
- Mark collision authoring pass in 0.1.1 as partially done
  (terrain+site done via UGigLineAssetTools; buildings pending)
- Add TEMP QA diagnostic removal to release checks
- Add architecture documentation and doc toolchain as done
- Restore ODbL derivative-database posture as active [A]
2026-11-08 14:16:59 +01:00
zwitschi a3ce9be38a memory: doc toolchain, asciidoc→md conversion, QA merge status 2026-11-08 14:13:20 +01:00
zwitschi 2f87002711 docs(licensing): remove ODbL attribution requirements and HUD credit references
Drop the OpenStreetMap/ODbL attribution requirement, the Google imagery rule,
and the related F-4 requirement, risk R-4, release check, and TODO items across
AGENTS, PLAN, ROADMAP, README, setup, architecture sections, and manifests.
OSM data is now documented as local private use only. Also normalize
numbered-list indentation in the architecture docs.
2026-11-08 14:11:06 +01:00
zwitschi 37a3bfbc3d Merge pull request 'fix(input): restore working controls + add input harness + build fixes' (#8) from qa-input-collision-fix-0.1.0-clean into main
Merged PR #8
2026-11-08 13:45:46 +01:00
zwitschi 4832e66c00 fix(input): restore working controls + add input harness + build fixes
Consolidated from the qa-input-collision-fix-0.1.0 work, applied onto main so
only the changes main genuinely lacks are carried (main already merged the
walkable-demo/collision work independently).

- fix(input): populate the UE 5.8 FInputMappingContextMappingData
  DefaultKeyMappings field in ue_create_input_assets.py (the runtime reads
  GetMappings()->DefaultKeyMappings.Mappings; the script previously wrote only
  the deprecated 'mappings' array, so the cooked game had 0 runtime mappings
  and every key silently did nothing — in packaged AND PIE). Regenerated
  IMC_Default.uasset. WASD/mouse/Space/R/Escape verified working.
- fix(build): drop editor-only APIs that break the game target build —
  UStaticMesh::PostEditChange() (GigLineAssetTools) and AActor::GetActorLabel()
  (character ground probe). GigLine and GigLineEditor now both compile clean.
- tools: OS-level uinput input harness (input_emulation.py + input_test.py) and
  run_pie.sh for PIE isolation; document the PIE-vs-packaged input test in
  Docs/build.md. Removed the temporary SIM/EI/screenshot diagnostics from the
  character.
2026-11-08 13:43:33 +01:00
zwitschi 6b4e1252c5 docs(glossary): add PIE and UE entries
PIE — Play In Editor: an Unreal Editor feature that runs the current
map as a live game session inside the editor.
UE — Unreal Engine: Epic Games' real-time 3D creation platform,
comprising the editor (including PIE), C++ framework, and runtime.
2026-11-08 13:40:46 +01:00
zwitschi fe8740c1e3 docs: remove .adoc sources — markdown is the canonical format now 2026-11-08 13:38:52 +01:00
zwitschi 5aa883eaff docs: convert all architecture .adoc sections to markdown, prettier-format all .md
- Converted 13 individual .adoc section files to .md via
  asciidoctor→docbook5→pandoc(gfm) + post-processing, placed alongside
  the .adoc sources in Docs/Architecture/
- Rebuilt architecture.md from the individual section .md files
- Ran prettier --write on all .md in the repo (23 files changed):
  normalised table column alignment, trailing whitespace, line breaks
- Added .agents/ to .prettierignore (vendored third-party content)
- All .md now passes prettier --check
2026-11-08 13:33:44 +01:00
zwitschi e93c27054a docs: reconvert architecture.adoc to markdown via asciidoctor→docbook→pandoc
Replaced the Python-converted architecture.md with a proper pandoc
conversion chain:

  asciidoctor -b docbook5  →  pandoc -f docbook -t gfm

This fixes table rendering (pipe tables instead of grid tables),
interpolates AsciiDoc attributes ({project-name} → Gig Line), strips
anchor IDs, and normalises escaping. Post-processed: glossary italic
leak from cols="e,2e" table, ® → (R) for the reset key, \>/> escapes.

Also:
- prettier 3.9.10 installed globally as the Markdown formatter/linter
- .prettierrc: proseWrap=never, tabWidth=2
- .prettierignore: skips Binaries/, *.uasset, *.umap, *.blend, images
- architecture.md passes prettier --check
2026-11-08 13:26:17 +01:00
zwitschi d03829b699 docs: add Markdown rendering of arc42 architecture documentation
architecture.md is the combined 450-line single-page rendering of all 12
arc42 sections. The Asciidoctor source (.adoc) files remain for future
editing; the Markdown is the readable deliverable.
2026-11-08 13:12:33 +01:00
zwitschi 72705268b5 docs: arc42 architecture documentation for Gig Line
Populated from the arc42-template (EN/adoc) with project-specific content
across all 12 sections:

1.  Introduction and Goals — requirements overview, quality goals, stakeholders
2.  Architecture Constraints — technical (engine, collision, coordinates),
    organisational (squash-merge, visual review, provenance), conventions
3.  Context and Scope — business and technical context tables
4.  Solution Strategy — technology decisions, top-level decomposition,
    quality achievement approach
5.  Building Block View — Gameplay (6 C++ classes), Site (7 meshes + 7
    materials), Facades (9 buildings), Art Pipeline, QA & Review; Level 2
    white boxes for input flow and collision model
6.  Runtime View — walk, screenshot capture, QA self-test, build/import
    pipeline scenarios
7.  Deployment View — dev workstation and packaged build infrastructure
8.  Cross-cutting Concepts — coordinate system, collision model, lighting,
    materials, data provenance, QA harness
9.  Architecture Decisions — 7 ADRs (dynamic lighting, UE materials,
    C++ collision helper, grounded reset, core ticker, Y convention,
    headless Blender)
10. Quality Requirements — overview table + 7 measurable scenarios
11. Risks and Technical Debts — 5 risks, 2 debts with mitigations
12. Glossary — 16 domain-specific terms

Master document: Docs/Architecture/architecture.adoc
Config: Docs/Architecture/config.adoc
2026-11-08 13:08:35 +01:00
zwitschi b577afa730 todo: close the Gameplay section after the QA merge 2026-11-08 11:57:02 +01:00
zwitschi 52b1212cf3 qa: make the demo walkable — terrain collision root cause, spawn, scripted QA
The reported fall-through was a misdiagnosis: the player never fell. The
imported Terrain_DEM had only a bounding-box simple collision proxy, so a
downward trace anywhere in the block hit the terrain at Z 819 (the mesh
bbox max) and the street sat under an invisible 8 m lid. Standing on that
box looked plausible, which is why it read as working.

- add UGigLineAssetTools (C++): force CTF_UseComplexAsSimple on a static
  mesh. UE 5.8 dropped bUseComplexAsSimpleCollision and Python cannot set
  the flag; the builder also saves the asset, since the flag lives there
  and a map-only save silently reverts it.
- spawn (-13336, -1150, 600): inside the carved corridor (the cut is a
  10.7 m half-width around the road centreline), lands on the road at 456.
- fix ResetToStart returning to the mid-air PlayerStart captured in
  BeginPlay: StartLocation is now captured on the first grounded frame.
- HighResShot 1280x720 instead of "1280 720", which is a multiplier.
- add -qaselftest: scripted walk/reset/pause on a core ticker plus a
  swept ground transect. Results: walk 746 cm/2 s with Z steady, reset
  0 cm from spawn, pause/unpause 1/0, transect 84 samples 0 holes.
- rebase onto main, keeping the frontage materials and placement fixes.
2026-11-08 11:56:19 +01:00
zwitschi fee26ade10 frontage: south block frontage, plus the placement bug it exposed
Adds the five remaining verified south-side buildings to the demo block
(6922, 6902, 6838 El Capitan Theatre, 6822 Budget, 6806-6820, 6800 Lee Drug),
exported in place from the OSM blockout by Tools/export_block_frontage.py,
imported to /Game/Hollywood/Frontage/ and staged by the demo builder with the
look material set (stone for the theatres, stucco for the retail run).

Adding neighbours exposed a pre-existing placement bug that was invisible while
the hero facade stood alone in an empty field:

- UE south is +Y here (south sidewalk Y +718..+1447), but the hero sat at
  Y -1450 on the NORTH side of the street.
- It was also at X -12024..-9420, 26 m east of the 6840 footprint; the frontage
  gap between 6902 and 6838 is X -14591..-12082.
- The two north-side buildings were on the SOUTH side, because their exporter
  mirrored Y.

Fixes: hero moved to X -13336 / Y +1307, inside its frontage gap; the frontage
and north-building exporters no longer apply a Y mirror (the FBX preset already
lands Blender -Y south on UE +Y south); player start and demo cameras moved to
the north sidewalk looking south; sun raised to pitch -62 so both frontages read
instead of the north side silhouetting.

Result: the demo is a street - continuous south frontage with the hero in it,
both sidewalks, the road with the designated crossing, and the north-side
buildings across the road.

Screenshots re-rendered and approved by the owner.

Squash of branch frontage-block-0.1.0 (PR #6).
2026-11-08 09:20:11 +01:00
zwitschi 62a41bf904 materials: look-developed surface materials for every exported asset
Completes "Materials and look development for all exported assets". Every
exported surface previously carried a flat Constant3Vector fill, which read as
plastic. ensure_material() in Tools/ue_build_demo_map.py now builds a
procedural material per surface:

- base colour blended between two shades by world-space noise
  (WorldPosition -> Multiply -> Noise -> Lerp -> BaseColor) for large-scale
  variation,
- an explicit roughness and specular so the surface type is readable.

  M_Asphalt 0.92, M_Concrete 0.88, M_KerbConcrete 0.85, M_Ground 0.98,
  M_RoadPaint 0.62, M_FacadeStone 0.72, M_Stucco 0.86

Site meshes take their material by mesh name; the hero facade and the two
north-side buildings take theirs by actor label, so they are covered even
though other scripts stage them. Materials are deleted and rebuilt on every
run, so a look change cannot be masked by a stale asset.

Result: the facade reads as warm stone with surface variation, asphalt as dark
asphalt with grain, concrete and kerbs lighter and matte, and the north
buildings in stucco instead of flat grey.

Screenshots re-rendered and approved by the owner. The richer texture-based
pass (baked/procedural brick_facade, seamless_concrete, glass, material
instances) stays in the 0.1.4 facade patch.

Squash of branch materials-lookdev-0.1.0 (PR #5).
2026-11-07 20:34:57 +01:00
zwitschi c626b379d9 todo: close character development section (all items done)
Removes the stale - active - session suffix from the Character development
heading; the idle pose, warm skin, NPC prefab, and stable-naming items are all
done. The review-scene section above it also has no open items.
2026-11-07 19:28:02 +01:00
zwitschi 80ef80b490 attribution: in-game ODbL credit via AGigLineHUD
Completes "Geographic attribution display" for 0.1.0. The demo is built from
OpenStreetMap data (ODbL 1.0) and needs attribution wherever the derived work
is shown, so the credit now lives in the running game, not just the documents.

- AGigLineHUD (C++): plain AHUD::DrawText in the bottom-left draws
  "(c) OpenStreetMap contributors" and "Data (c) OpenStreetMap contributors,
  ODbL 1.0 - openstreetmap.org/copyright", with a shadow pass so it stays
  legible over bright pavement and sky. No UMG widget asset is involved, so a
  cook cannot drop the attribution through a missing widget reference.
- AGigLineGameModeBase: HUDClass = AGigLineHUD, so the credit is on by default
  in both the DemoMap and the ReviewMap.
- AGigLineShotActor: screenshots are requested with the UI shown, so the credit
  appears in every capture under Screenshots/ - the obligation is provable from
  the images rather than only described.

Docs:
- THIRD_PARTY_NOTICES records that the attribution is displayed in-game and
  where the code lives.
- Screenshots/README documents the overlay.
- TODO marks the item done and narrows the remaining ODbL question to the
  shipped-derivative-database posture.

Squash of branch gameplay-attribution-0.1.0 (PR #4).
2026-11-07 18:47:31 +01:00
zwitschi c3a92d008b docs: update task status markers in setup and TODO files 2026-11-07 18:24:52 +01:00
zwitschi accf3a5306 lighting: fixed daytime rig, real surface materials, dynamic lights
Completes "Fixed daytime lighting and basic ambience" for 0.1.0. All 14
screenshots approved by the owner.

Lighting rig (Tools/ue_build_demo_map.py):
- Sun pitch -50 / yaw 120, intensity 4, warm (255/245/230), atmosphere sun light
- Sky light 2.6 with real-time capture, sky atmosphere, height fog (density 0.008)
- Unbound post-process volume with fixed exposure (min == max brightness) so a
  shot cannot wash out or crush to black depending on how much sky it frames
- Dynamic-only: r.AllowStaticLighting=False plus MOVABLE lights and mesh
  components, so the editor's "LIGHTING NEEDS TO BE REBUILT" warning no longer
  fires and nothing needs baking

Bugs found and fixed on the way:
- Blender diffuse colours do not survive the FBX -> Unreal import: every site
  material arrived white and the terrain arrived with WorldGridMaterial, which
  is what actually made the scene look blown out. The demo builder now creates
  and assigns M_Asphalt, M_Concrete, M_Ground and M_RoadPaint.
- Only one of the three crossings reached the game: each zebra band was its own
  object, so Blender renamed the duplicates .001/.002 and exact-name site
  assembly skipped them. All bands are now one Crossing_Markings mesh.
- Demo cameras: street-level sat inside the facade body (rendering its unlit
  back face) and the crossing camera aimed at the s=36.6 crossing instead of
  the designated s=130.1 (UE X -14956).
- The terrain had no material and imported as a bright default grid.
- Cleared a committed Android File Server SecurityToken (plugin disabled and
  shipping-excluded; the value must stay empty).

Docs:
- Screenshots/README.md: one consolidated table for all 14 shots with map,
  camera view and description; all approved.
- MEMORY.md: records the material-import, exposure, light_color struct,
  band-naming, camera-bounds and lighting-build traps.
- TODO.md: lighting item marked done.

Squash of branch gameplay-lighting-0.1.0 (PR #3).
2026-11-07 18:22:19 +01:00
zwitschi 1dbe737b85 Merge branch 'gameplay-crossing-0.1.0': designated street crossing + review map fixes 2026-11-07 16:54:09 +01:00
zwitschi fbb3c3fb99 agents: seed AGENTS.md from PLAN.md — agent instructions source 2026-11-07 16:24:19 +01:00
zwitschi c9e933376c review: restore character + cubes in ReviewMap, re-render all shots
Root cause of the empty review frames: EditorLevelLibrary.load_level/new_level
are deprecated no-ops in -nullrhi commandlets, so staged actors landed in the
current world and got saved over the wrong map. Switched both map builders to
the LevelEditorSubsystem and reordered the review builder to save ReviewMap
before the DemoMap section (the switch-back to a map that only exists in
memory silently failed, writing demo content over ReviewMap).

Rebuilt both maps, re-rendered every screenshot; user visual check passed.
Screenshots/README.md cube naming corrected (A grounded, B floating), approval
states updated; TODO.md visual-review notes closed. MEMORY.md records the
deprecated-call and save-order traps.
2026-11-07 16:23:59 +01:00
zwitschi 14265ef230 screenshots: drop superseded numbered review shots
Completes the stable-name rename: removes the 13 old numbered review/demo
screenshots so Screenshots/ contains exactly the stable-named set, matching
gameplay-crossing-0.1.0.
2026-11-07 16:04:36 +01:00
zwitschi 80d75b93c6 screenshots: adopt stable-named shots (belong on main, off gameplay branch)
The stable-named screenshot set (renamed in the NPC/character work) is
review content that does not belong on the gameplay-crossing branch. Bring it
onto main directly:

- renames all 9 numbered review shots to stable camera names
  (review-overview, review-character, ... review-hero-facade-oblique)
- adds the download-north-buildings demo shot + review-npc-prefab
- README updated for stable naming and the new NPC shot
2026-11-07 16:03:28 +01:00
zwitschi a181de991c Merge pull request 'Add OpenRouter screenshot judge tool' (#1) from judge-screenshot into main
Reviewed-on: #1
2026-11-07 15:58:28 +01:00
zwitschi 91768a9f74 tools: add screenshot judge script using OpenRouter vision
- Tools/judge_screenshot.py: judges a screenshot against an expected-scene
  description via OpenRouter multimodal image understanding, reading
  OPENROUTER_API_KEY from the ignored .env. --image+--expect for one shot,
  --shots to run every review shot in Screenshots/README.md. Returns a
  {verdict, confidence, reason, missing} JSON.
- Documented in AGENTS.md (Visual review pre-check step) and Tools/README.md;
  recorded the OpenRouter payload format gotcha in MEMORY.md.
2026-11-07 15:57:49 +01:00
zwitschi ab24ef3b24 memory: record the shared-checkout commit race
The skill-pack entry was swept into another session's commit before it could be
committed on its own. Notes that a shared worktree can commit a shared file for
you - or drop the edit if the file is overwritten - and that git log -S is the
way to check before assuming the edit was lost.
2026-11-07 15:48:17 +01:00
zwitschi 54b62395b8 docs: crossing done pending review; record LFS-lockable and normals traps 2026-11-07 15:47:40 +01:00
zwitschi bc2e421d33 gameplay: designated street crossing — zebra markings, corridor cut, up-facing street
- street_mesh.py: continental zebra markings (0.5 m bars, 0.5 m gaps, 3 m
  band) at all three signalised crossings, triangles not quads (FBX SDK
  refuses to triangulate the crown-twisted quads), stripes +6 cm above the
  DEM so they stay above the road surface; winding flipped so every street
  surface faces up (road/sidewalks/markings were 100% down-facing and
  invisible in engine once the terrain stopped covering them).
- terrain_from_dem.py: cut a 10.7 m corridor around the Hollywood Blvd
  centreline so road, kerbs and sidewalks are not buried under the terrain;
  sidewalks themselves cover the transition band, walk-check stays at 0 holes.
- Blender builders save with a writability guard + fatal failure: *.blend
  is LFS-lockable and checks out read-only; a failed save previously left
  downstream steps building from stale files.
- ue_import_site.py: Crossing_Markings imports with no collision (paint on
  the road); ue_build_demo_map.py stages it and chmods the lockable .umap
  before saving (save_map silently returned False on read-only maps).
- ue_build_review_map.py: demo-crossing camera moved to the real crossing
  (s=130.1 m -> UE -24291,-1340) and fixed the ensure_writable path (was
  missing the .umap suffix, so the chmod never fired).
2026-11-07 15:46:45 +01:00
zwitschi 985dfcbac7 npc: AGigLineNPCPerson prefab + stable-named screenshots
- New C++ AGigLineNPCPerson: reusable NPC/pedestrian prefab built on the MPFB
  human. ACharacter capsule half-height 83 cm (total 1.66 m, head top ~1.66 m
  so a 1.60 m camera doesn't clip), the character skeletal mesh on the built-in
  mesh as the visible body (feet on Z 0), AutoPossessAI for 0.5.0 pedestrians.
  Staged at Z 0 in the review scene; captured as review-npc-prefab.png.
- AGigLineShotActor now names screenshots by stable camera name (no list-index
  prefix): inserting or reordering cameras no longer renumbers files or orphans
  the old ones. Re-rendered all review/demo shots with stable names.
- ue_build_review_map.py: chmod the target .umap before save_map (git marks maps
  444 on checkout, silently making save_map return False and dropping stages).
- Screenshots/README.md, ROADMAP, TODO updated for the prefab + stable names.
- DemoMap.umap rebuilt by the render pipeline (north buildings + demo cameras).
2026-11-07 15:28:42 +01:00
zwitschi cef87b3d31 street: continental zebra markings at the three signalised crossings
0.5 m white bars with 0.5 m gaps across a 3 m band per crossing, laid
1 cm above the road surface. Makes the mid-block crossing (OSM way
385648319, s=130.1 m) a designated crossing in game. Metadata audit
passes (1 sidecar, 0 problems).
2026-11-07 15:24:21 +01:00
zwitschi eb614fe035 todo: claim Gameplay section on branch gameplay-crossing-0.1.0 2026-11-07 15:23:06 +01:00
zwitschi 5ab8b556f3 docs: move task list to TODO.md and keep ROADMAP.md as plan
Split the former ROADMAP.md checklists into TODO.md, which now holds
all actionable work and progress marks. ROADMAP.md keeps milestone
goals, scope, and release gates as prose with no checkboxes.

Update AGENTS.md, Docs/, Geo/README.md, MEMORY.md, and README.md to
point to the new file roles.
2026-11-07 15:17:10 +01:00
zwitschi 91fd123ab4 repo: allow screenshots in the repository
- Drop the /Screenshots/* gitignore exclusion so review PNGs are committed
  (LFS-tracked) alongside the review README.
- AGENTS.md Visual review: PNGs are now committed, not kept only on disk.
- 13 review/demo screenshots tracked (25 MB LFS).
2026-11-07 15:01:09 +01:00
zwitschi 08ac6a22cc docs: embed thumbnail previews linking to full-size screenshots in README tables 2026-11-07 14:58:31 +01:00
zwitschi 84295a1f03 docs: consolidate screenshot README, drop stale shot refs in roadmap
- Screenshots/README.md: collapsed per-shot blurbs into one approved
  review-map table + one demo-map table; removed the duplicate
  hero-facade-front/oblique entries (leftovers from an old camera list that
  duplicated review-07/08) and the trailing duplicate 'known issues' block.
- Removed the three orphan PNGs (review-03-first-person, review-04/05
  hero-facade) that no longer match the camera list.
- ROADMAP.md: fix stale 'review pending' on character and hero-facade items
  (now approved), and point facade review refs at review-06/07/08.
2026-11-07 14:57:12 +01:00
zwitschi ee37c7c345 docs: add Git workflow guidelines for agents and branch management 2026-11-07 14:43:31 +01:00
zwitschi 4401bb2f67 asset: two north-side background buildings across Hollywood Blvd
- TCL Chinese Theatre (6925) and Madame Tussaud's (6933) exported from the
  verified OSM blockout with ue_metric_v1 (export_north_buildings.py), with a
  Y-mirror so they land north of the road, across from the hero facade.
- ue_import_north_buildings.py imports + stages them in the DemoMap as
  North_TCL_Chinese / North_Madame_Tussauds (TCL Y 1802..9431, Tussaud's
  1888..7586; hero at Y -1450, road centreline ~0).
- render_screenshots.sh now also runs the DemoMap capture pass; added a
  demo-north-buildings camera, so the new buildings get a review shot.
- README/roadmap updated; flat facade materials pending the 0.1.4 patch.
2026-11-07 14:28:53 +01:00
zwitschi f7f01fd077 docs: update roadmap with modeling tasks and suggested Blender skills for asset development 2026-11-07 14:23:48 +01:00
zwitschi c4a8f4ec99 docs: add Blender skills section with detailed instructions for agents 2026-11-07 14:23:30 +01:00
zwitschi 72e7eed653 plan: 0.1.0 ships first-person only; MPCB character reserved as NPC template
Coordinator decision 2026-11-07: 0.1.0 is first-person-only (capsule +
camera, no visible body). The MPFB character (idle pose + warm skin, done)
is reframed as the reusable NPC/pedestrian prefab for 0.2.0+ / 0.5.0, not
wired into the AGigLineCharacter pawn. Roadmap character + 0.2.0/0.5.0
sections updated; no engine/asset changes.
2026-11-07 14:16:17 +01:00
zwitschi c11cb4caa4 character: idle standing pose and warm skin, not the grey mannequin
- mpfb_character_export.py: new --pose idle folds the A/T-bind arms down to
  a relaxed standing rest pose (baked via pose.armature_apply before export);
  new make_diffuse synthesizes a warm skin-tone diffuse with a darker
  shorts-suggestive band, replacing the Cycles v2-skin bake that came out
  black and was slow at 1024^2.
- write_png now encodes from img.pixels via numpy (fast) because img.save()
  writes black for an in-memory image.
- Re-exported idle character, re-imported to /Game/Test/Character (138 bones,
  warm M_Character_Export/T_Character_Diffuse); removed stale T_Skin_Diffuse.
- Untracked the review PNGs again: the Screenshots/ gitignore rule had been
  dropped and the parallel session had committed the PNGs, against the
  AGENTS.md 'never commit the PNGs' rule. Restored the ignore rule; only
  Screenshots/README.md stays tracked.
- README/roadmap/memory updated; review-01-character re-pended for approval.
2026-11-07 14:11:06 +01:00
zwitschi e48a0cd82b docs: bring every document in line with the current build
README rewritten around the actual state (walkable DemoMap, blockout facades,
planned patches); Tools/README now lists all seventeen tools by role; build.md
gains the content pipeline table and the two enforced rules (one unit conversion,
collision decided at import). Setup gains a content-pipeline row, the toolchain
state notes the demo level exercises the engine path, release checks record the
Blender pre-check that backs traversal, PLAN points at ROADMAP/MEMORY/Screenshots,
and THIRD_PARTY_NOTICES gains rows for the hero facade and the candidate props.

Also fixed the last stale reference to Geo/scripts in MEMORY.md - a sweep found
old paths in six documents after the tool move, which the memory entry now warns
about.
2026-11-07 13:21:10 +01:00
zwitschi 1a1230ab6b docs: record collision, reset and pause as implemented-not-verified 2026-11-07 13:17:54 +01:00
zwitschi 85f2c37834 review: document the three demo map views 2026-11-07 13:17:39 +01:00
zwitschi cf392a02f7 gameplay: walkable demo map, reset and pause
Tools/ue_build_demo_map.py builds /Game/Hollywood/DemoMap: sun, sky, the hero
facade on the building line, six site meshes imported with collision, and a
player start on the north sidewalk. Tools/ue_import_site.py imports the site
FBXs; terrain, road and sidewalks use complex-as-simple so a simple hull cannot
ramp over the kerbs. Art/blender/site/export_site_fbx.py exports one FBX per
object so each can carry its own collision.

Reset (R) and pause (Escape) join the character and the input context: reset
restores the spawn transform and clears momentum, pause is a real game pause that
releases the cursor. DefaultInput.ini captures the mouse on launch.

Three placement defects found by measuring the imported actors rather than
trusting the plan: the spawn was on the wrong sidewalk because the FBX flip
inverts north, a mirror I added to the exporter double-corrected an already
correct sidewalk, and the street meshes only carry part of the north walk - the
rest is terrain.
2026-11-07 13:17:24 +01:00
zwitschi c9278378ee plan: approve the review shots, plan collision and facade patches
All nine review shots approved. Roadmap gains the defects the Blender pre-check
already knows about rather than rediscovering them after release: 0.1.1 now names
the 15.8 cm sidewalk edge step, the ~10% crossing ramps, the missing collision
authoring pass and the unverified kerb height. A new 0.1.4 patch plans proper
building facades explicitly - facade kit, procedural materials, Blosm setupScript
styles, hero detail, El Capitan Theatre - and states that the existing geometry is
kept, so shipped blockouts cannot be mistaken for finished architecture.
2026-11-07 12:52:04 +01:00
zwitschi ec2917ed59 docs: release the review placement claim — every shot item is closed 2026-11-07 12:46:12 +01:00
zwitschi d4af4af90f hero: make the portals real recesses and land the facade on Z 0
The portal detail shot failed for a modelling reason, not a camera reason: the
doors were 0.1 m panels on a solid plinth, so they read as shading. The plinth is
now four piers plus a lintel band, with a 1.2 m reveal, stone jambs and lintels
standing 0.10 m proud and three mullions per opening. review-06 frames them from
8 m at eye height.

Two defects fixed while doing it: the jamb boxes hung 0.15 m below the pavement,
so the facade imported with min Z -15 cm; and a rebuild run against a read-only
.blend silently skipped the save, which is why screenshots kept showing the old
geometry.
2026-11-07 12:46:00 +01:00
zwitschi f575bc82a8 docs: release the geo claim and record the tooling traps 2026-11-07 12:28:38 +01:00
zwitschi 386d4bee78 geo: one tool directory, one unit boundary, per-asset provenance
Geo/tools/ is now every geographic script, with Geo/README.md documenting each
one; the Blender-side builders moved beside their outputs in Art/blender/*/ and
import their helpers from Geo/tools/osm_common.py.

That module gains to_unreal_local() and to_blender_local(): the only place in the
repository that multiplies metres by 100, so a second silent scale cannot be
introduced. block_height_report.py adds height provenance for the block — 3 of
15 buildings have no height data and render at a placeholder 6.4 m.
asset_metadata.py writes and audits <asset>.geo.json sidecars; four now exist
(hero-facade, terrain, street, blockout) and the audit recomputes each centre from
the origin, failing anything that disagrees.
2026-11-07 12:28:26 +01:00
zwitschi 5e5e5ae9b4 refs: survey the sibling project and recalibrate the hero facade
Tools/sibling_asset_audit.py reads the sibling security-game project's prop
library straight from the GLB headers: 20 street props, 11.8k triangles, every
one with a Blender source. The 50 .blend sources are copied to
Art/props/blender_src/ with a manifest; nothing is imported into Unreal yet. Its
building meshes are deliberately not taken - they are Godot GLBs built around a
29.6 m facade that this project measured at 25.75 m.

The facade proportions were wrong against the photograph: 5.7 m of blank wall
above the capitals. Entablature is now 3.4 m, parapet 2.2 m, with architrave and
cornice bands and square pedestals, so the elevation reads as layered stone.
2026-11-07 12:20:41 +01:00
zwitschi 52a736bc9f review: drop two orphan shots left by the renamed camera list 2026-11-07 12:08:42 +01:00
zwitschi e46a877556 review: split the rounds into named shots and stage the player pawn
Roadmap gains two sections: 'Review scene and player placement' (this pass) and
'Character development' (follow-on). The camera list is now one table of
(name, location, target) with look-at rotations computed from the pairs, so a
camera cannot drift out of aim when one number changes.

New shots: player-placement (the real pawn, staged in the build script),
ground-contact (knee height, feet on the plane), shadow-compare (floating vs
grounded cube from a low angle), and hero-portals (whole elevation).

The review floor grows to 160 m so the facade stands on ground rather than the
horizon band. Retired: the first-person shot.

Honest gap recorded in the roadmap: the hero portals sit behind the colonnade and
do not resolve from the review floor; that needs a camera on the plinth or a
portal rebuild, not another angle.
2026-11-07 12:08:19 +01:00
zwitschi 0383eabc8e review: put the character on the floor, split the cubes, commit the shots
The review floor was a 5 cm slab centred 50 cm down, so the character floated
1.8 m above it. The floor is now 90 m x 90 m x 10 cm with its top face at Z 0 and
the character measures Z 0..83 cm - feet on the plane.

Two cubes only: A on the ground as the contact/scale reference, B held at 2 m so
its cast shadow separates from the body for comparison. The scale-cubes camera
was still aimed at the old three-cube row and now looks at both from the south.

Screenshots are committed per the owner's instruction, and .gitignore no longer
excludes them.
2026-11-07 12:02:18 +01:00
zwitschi 3be37c76a2 docs: record the facade review traps and release the section claim
Memory gains the camera-side, centre-origin, read-only-map and screenshot-tracking
traps this pass cost. The asset production claim is released: the hero facade
first pass exists and awaits screenshot approval, so the section is free for the
next owner.
2026-11-07 11:43:07 +01:00
zwitschi dd683e0ea6 chore: keep review screenshots off the repository
AGENTS.md requires the PNGs to stay on disk with only Screenshots/README.md
tracked. Four shots had been committed; they are removed from the index and the
ignore rule restored, so the next review run cannot add binaries by accident.
2026-11-07 11:42:56 +01:00
zwitschi c5e30d1c36 feat: hero facade blockout for 6840 Hollywood Blvd
Tools/hero_facade_build.py builds the north elevation parametrically and writes
a JSON report naming the evidence behind every part: the 25.75 m frontage and
14.5 m height are measured, the 4.96 m column bay comes from the sibling notes
and matches that frontage, the three portal flights follow the photographs (five
risers, overturning the inherited eight-step, 12 m wide claim), and the rest is
labelled assumption.

Tools/ue_hero_facade_import.py imports it with the ue_metric_v1 preset
(2604 x 1257 x 1450 cm, 1892 tris, 4 materials) and places it on the measured
building line of /Game/Hollywood/DemoMap.

Two new review shots at review-04 and review-05, both pending approval. Camera
placement cost four renders: the first two sat inside the facade body, and the
frontage faces Unreal -Y, so a camera has to stand at Y < -1450 cm, not north of
the building line in project terms.
2026-11-07 11:42:29 +01:00
354 changed files with 19241 additions and 854 deletions

No files matched your search

+12
View File
@@ -42,6 +42,15 @@ Thumbs.db
.env.*
*.env
# Mapbox-derived data — local only. Mapbox terms restrict persisting/redistributing
# their data, so the Tilequery cache and any generated report stay untracked.
/Geo/raw/mapbox-cache/
/Geo/verification/*mapbox*
# Visual-loop iterations: generated comparisons stay uncommitted until the owner
# confirms a shot. Only Screenshots/ is committed.
/Art/reference/iterations/
# Local, private reference material — never committed (see Docs/reference-notes-hollywood.md)
/Art/reference/
/Docs/data-inventory.md
@@ -52,6 +61,9 @@ Thumbs.db
# Blender/Blosm working caches
/Art/blender/.cache/
# Rendered screenshots for visual review — committed with the repo (see
# AGENTS.md Visual review) so reviewers see what the build rendered.
# Scratch files
.tmp-*
+15
View File
@@ -0,0 +1,15 @@
# Prettier ignores — don't format generated or binary content
Binaries/
Intermediate/
Saved/
*.uasset
*.umap
*.blend
*.png
*.jpg
*.jpeg
*.gif
*.webp
# Vendored third-party content — never reformat
.agents/
+5
View File
@@ -0,0 +1,5 @@
{
"proseWrap": "never",
"tabWidth": 2,
"useTabs": false
}
+144 -53
View File
@@ -1,11 +1,10 @@
# AGENTS.md
Instructions and living roadmap for autonomous agents, AI coding assistants, and contributors working on this repository.
Working rules for autonomous agents, AI coding assistants, and contributors. Planned work is tracked in **[TODO.md](TODO.md)**; development direction is in **[ROADMAP.md](ROADMAP.md)**.
## Project Goal
Develop and test on Linux; ship Windows and Linux builds, with Linux as the primary development and CI platform. The game supports walking and driving through a real city.
The initial release is **`0.1.0`**: a walkable exterior demo around **6840 Hollywood Blvd / El Capitan Entertainment Centre**, including buildings on the south side of Hollywood Blvd between Highland Avenue and Orange Drive, plus buildings whose footprint lies within 50 m of the north-side frontage.
Develop and test on Linux; ship Windows and Linux builds, with Linux as the primary development and CI platform. The game supports walking and driving through a real city. The initial release is **`0.1.0`**: a walkable exterior demo around **6840 Hollywood Blvd / El Capitan Entertainment Centre**, including buildings on the south side of Hollywood Blvd between Highland Avenue and Orange Drive, plus buildings whose footprint lies within 50 m of the north-side frontage.
- Verify the address, building footprints, and demo boundaries **before** modeling.
- If the address, footprint, or boundary cannot be verified from permitted sources, do not model that structure. Record the unresolved item in `Geo/` with the sources checked and continue with verified structures only.
@@ -14,14 +13,14 @@ The initial release is **`0.1.0`**: a walkable exterior demo around **6840 Holly
## Toolchain
| Tool | Role |
| --- | --- |
| Unreal Engine | Rendering, gameplay, World Partition, Chaos Vehicles |
| Blender | Modeling, UVs, baking, collision meshes, character prep |
| MPFB | Human base meshes; baked materials, deform skeletons |
| QGIS | Geographic cleanup, coordinate conversion, elevation prep |
| Blosm | OSM building/street blockouts in Blender |
| Git + Git LFS | Source control, large assets, tagged releases |
| Tool | Role |
| ------------- | --------------------------------------------------------- |
| Unreal Engine | Rendering, gameplay, World Partition, Chaos Vehicles |
| Blender | Modeling, UVs, baking, collision meshes, character prep |
| MPFB | Human base meshes; baked materials, deform skeletons |
| QGIS | Geographic cleanup, coordinate conversion, elevation prep |
| Blosm | OSM building/street blockouts in Blender |
| Git + Git LFS | Source control, large assets, tagged releases |
- Pin compatible versions in `Docs/toolchain.md`.
- Validate Blender→Unreal mesh, skeleton, material, and scale transfers **before** producing assets.
@@ -39,7 +38,7 @@ The initial release is **`0.1.0`**: a walkable exterior demo around **6840 Holly
- Track `.uasset`, `.umap`, `.blend`, and large source binaries with LFS.
- Ignore `Binaries/`, `Intermediate/`, `Saved/`, and Derived Data Cache.
- Never commit Unreal Engine source, credentials, or non-redistributable assets.
- Planned remote repository: `https://git.allucanget.biz/zwitschi/gig-line.git`. Do not assume it is configured or available.
- Active remote repository (`origin`): `https://git.allucanget.biz/zwitschi/gig-line.git`. Verify connectivity before use.
## Release Policy
@@ -52,6 +51,119 @@ Semantic Versioning: `MAJOR.MINOR.PATCH`.
- During `0.x`, document breaking changes and asset migrations.
- `1.0.0` marks the stable, agreed game scope — not completion of a whole city.
## Roadmap and TODO
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.
- **[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`:
- Mark items **open** (`- [ ]`), **active/in progress** (`- [A]`, **deferred** `- [D]`), or **done** (`- [x]`). Keep the marks accurate.
- When working on a level-three (`###`) section, append `- active - <sessionId>` to its heading, using your current session ID. Remove the suffix when you stop working on that section. Mark the items you are working on `- [A]` or `- [D]`.
- Existing in-progress marks can describe unfinished work, not necessarily a live session; use the active heading and assignment details to identify current ownership.
- Each version is a `##` heading; task groups are `###` headings with `- [ ]` steps.
- When **all** steps in a version section are done: write a short descriptive entry to `CHANGELOG.md`, remove the version section from `TODO.md`, and leave its goal in `ROADMAP.md`.
- Release checks for each version are listed at the end of its `TODO.md` section; `Docs/release-checks.md` defines the checks themselves. If a check fails, do not tag the release; open a patch task in `TODO.md` and report the failing check in `CHANGELOG.md`.
- Finish each milestone's release checks **before** expanding scope. Cut map size and simulation complexity before compromising the core walking/driving experience.
- **Realism leads, performance is a floor.** When a realism improvement costs frame time, realism wins and the cost is paid — but 60 FPS at 1440p without frame generation must still be met at every release. If realism cannot fit the budget, reduce the realism scope deliberately and record it in `CHANGELOG.md`; never ship below the floor without the owner's approval. Walkability outranks realism in one direction only: a block the player falls through is not a demo.
## Git workflow
Every agent works on a dedicated git branch. **Never commit work directly to `main`** (except the coordinator's integration merges).
Before starting any task:
1. **Scan `TODO.md`** and pick a task — a level-three (`###`) section — to work on. Use `ROADMAP.md` for the goal and scope of the version you are working in. Prefer sections that are already active (they may be yours) or open items in sections that are not owned by another agent. Check the active heading and assignment details to identify ownership.
2. **Create a branch** for that task from the current `main`: `git checkout -b <topic>-<version>` (e.g. `git checkout -b facade-hero-0.1.4`). One branch per task; do not stack unrelated tasks on one branch.
3. **Record the active branch in the heading** of the section you are working on in `TODO.md`: append `- active - <sessionId>` (and, if useful, the branch name) to the level-three heading. This is the only way other agents can see that work is in progress on this section.
4. **Commit changes often** on that branch — small, focused commits with clear messages. Do not accumulate large uncommitted changes.
5. **Push to remote when a checklist item is done** — each completed `- [x]` item in that section should be committed and pushed. This keeps the remote state visible and makes progress recoverable.
6. When the section is complete, report the branch, changes, checks, and unresolved issues to the coordinator for integration (see _Parallel agent workflow_ below). Do not merge into `main` yourself unless authorized.
**Branch hygiene:**
- Branch from the current `main` revision, not from a stale local checkout.
- Keep `TODO.md` checkbox updates on your branch — they are part of the work.
- If a task needs a different camera angle, or affects visual review, follow the _Visual review_ section before pushing.
- If a task is abandoned, remove the `- active - <sessionId>` suffix from the heading in `TODO.md` and leave the branch for the coordinator to clean up.
## Parallel agent workflow
- Designate one coordinator to serialize assignments, updates to the shared `TODO.md`, and integration into `main`. Agree on task and file or asset ownership before work starts; avoid assigning the same section or binary asset to multiple agents at once.
- The coordinator records the session ID, branch name, and owned files or assets alongside the active `TODO.md` section on the shared `main` coordination copy. Publish and synchronize this state through the remote when agents use different machines. Branch-local changes are not visible to other worktrees until integrated; `TODO.md` is not an atomic lock.
- For parallel implementation, use a separate branch and Git worktree per agent, based on the agreed `main` revision. Do not switch branches in a directory another agent is using. Create branches, commits, pushes, and merges only when authorized; never overwrite another agent's uncommitted work.
- Keep ignored local references, generated files, build outputs, and Unreal editor processes in mind: worktrees do not automatically share ignored files, and shared output paths or concurrent editors can collide. Arrange required inputs and isolated outputs before running tools.
- Blender and Unreal binary assets cannot be meaningfully merged like text. Give each asset one owner; use Git LFS locks when supported by the remote, but do not assume locking is available.
- When work is ready, report the branch, changes, checks, unresolved issues, and relevant memory findings to the coordinator. Review and integrate branches into `main` one at a time, reconcile `TODO.md` and memory changes, and update ownership and status. Follow the visual review and user-approval requirements before marking visual work done.
## Memory
**[MEMORY.md](MEMORY.md)** is shared session memory for agents and collaborators. Write to it whenever you:
- discover a fact about the environment, a tool, or a data source that cost time to establish;
- hit a gotcha, trap, or surprising failure mode, and what actually caused it;
- find or fix a bug, and how it was found so it can be found again;
- make a decision that does not fit a `TODO.md` item, such as a workaround, an assumption, or a rejected approach and why. Record scope or direction decisions in `ROADMAP.md` as prose.
Keep entries short and dated, with the evidence: the command, the log line, or the file path. Anything that belongs in a formal document (setup steps, tool versions, licensing) goes in `Docs/` as well — memory is for coordination, not a replacement for the record. Do not let `MEMORY.md` become a second task list: planned work stays in `TODO.md`, and development direction stays in `ROADMAP.md`.
Memory edits on an agent's branch are not shared live. Communicate important findings to the coordinator during work and reconcile them into the shared `main` copy when integrating.
## Python and shell tooling
Every committed asset and every measured claim in this repository is produced by a script. Tools live in `Tools/` (Unreal, network, repository), `Geo/tools/` (projection, OSM/DEM parsing, metadata) and `Art/blender/*/` (Blender-side builders, next to their output).
**Fix the tool, not the artefact.** If an asset is corrected by hand, script the correction (or regenerate it) in the same change. A second manual fix of the same kind means the tool is missing a step.
**Improve over fork.** Extend the existing tool rather than adding a near-duplicate. When two or three scripts start sharing logic, factor the shared part into `Tools/_common.py`, `Geo/tools/osm_common.py` or `Art/blender/common.py`; keep the single metres→centimetres conversion where it already lives.
Rules that keep tools reusable and reviewable:
- **One script, one job.** Every input is an argument; no interactive prompts; deterministic output paths.
- **Fail loudly.** A tool that cannot do its job exits non-zero with a message. Never leave a partially written asset and a success code.
- **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.
- **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.
**Test the pure parts.** `Tools/tests/` holds stdlib `unittest` tests (no Blender or Unreal process; they must not start a game or editor). Extract the testable core — argument parsing, geometry maths, licence gating, report formatting — into functions that a test can call directly.
```bash
python3 -m unittest discover -s Tools/tests -t Tools -p 'test_*.py'
```
Keep the tools standard-library-only where practical; if a third-party import is genuinely needed, add it to `requirements-tools.txt` with a version floor and mention it in the tool's README row.
## 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.
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:
| Change | Section |
| --- | --- |
| A new or restructured module, tool, data source or external interface | `05_building_block_view.md` |
| A runtime flow, tick/order dependency, or lifecycle (possession, map load, QA self-test) | `06_runtime_view.md` |
| Packaging, engine pin, checkout/build/launch procedure | `07_deployment_view.md` |
| A pattern, convention or cross-cutting rule (units, collision policy, provenance) | `08_concepts.md` |
| A decision that constrains future work | new ADR in `09_architecture_decisions.md` |
| A quality goal or a measured budget (frame time, traversal coverage) | `10_quality_requirements.md` |
| A known risk or a risk that retired | `11_technical_risks.md` |
| A new term, tool or abbreviation | `12_glossary.md` |
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.
- **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.
## Geographic & Art Pipeline
1. Save a versioned OSM extract; document date, bounds, and source.
@@ -65,6 +177,22 @@ Semantic Versioning: `MAJOR.MINOR.PATCH`.
Always use a consistent scale test, named export presets, and repeatable import settings.
## Blender skills
`.agents/skills/blender-skills/` holds 94 Blender skill packs (MIT, blender-skills 1.3.0). They are agent instructions, not add-ons, and nothing in the pipeline depends on them.
**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:
- **Modeling / procedural assets** — `blender-modeler`, `procedural-modeling`, `geometry-nodes`, `hard-surface`.
- **Façades and architecture** — `archviz`, `historical-worlds` (1920s Hollywood Blvd direction), `realistic-style`.
- **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/`).
- **Collision** — `collision-proxy` (UCX/UHX naming, hulls, simple colliders).
- **Scene assembly / optimization** — `scene-assembly`, `asset-optimization`, `lod-pipeline`.
- **Review and QA** — `qa-review`, `references` (reference-image-match and visual-match checklists), `camera-cinematography` (matching reference camera angles).
**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.
## Licensing & Distribution
- Publish project-owned code under a chosen open-source license.
@@ -73,8 +201,6 @@ Always use a consistent scale test, named export presets, and repeatable import
- Record asset provenance and redistribution permissions in `THIRD_PARTY_NOTICES.md`.
- Verify licenses for every MPFB asset, texture, animation, vehicle, and audio file.
- An asset whose license cannot be verified must not be committed. Record it as unresolved in `THIRD_PARTY_NOTICES.md` and replace it with a redistributable asset before the release.
- Include "© OpenStreetMap contributors" and the appropriate license link; review ODbL obligations for distributed derived databases.
- Do **not** use extracted Google imagery or geometry without explicit applicable rights.
- Keep restricted assets out of the public repository; prefer redistributable replacements.
- Review Epic distribution requirements before publishing builds.
@@ -83,44 +209,9 @@ Always use a consistent scale test, named export presets, and repeatable import
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. Update `Screenshots/README.md`: one row per screenshot with what it shows and an approval state; describe any new shots or scene changes in the same change.
3. The PNGs are **never committed** — `.gitignore` keeps only `Screenshots/README.md`. Review happens on disk.
4. Wait for the user's approval before treating a visual change as done; record approval by ticking the table in `Screenshots/README.md`.
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.
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`.
## Roadmap
The living roadmap with per-version task checklists is in **[ROADMAP.md](ROADMAP.md)**.
- `ROADMAP.md` is the single, central TODO and progress list for all agents. Put planned work and actionable follow-ups there rather than creating parallel task lists.
- When working on a level-three (`###`) section, append ` - active - <sessionId>` to its heading, using your current session ID. Remove the suffix when you stop working on that section. Mark checklist items currently being worked on `- [-]` or `- [~]`; update the marks as their status changes.
- Planned work is listed as checkbox items in `ROADMAP.md`, marked **open** (`- [ ]`), **in progress/active** (`- [-]` or `- [~]`), or **done** (`- [x]`). Keep the marks accurate.
- Existing in-progress marks can describe unfinished work, not necessarily a live session; use the active heading and assignment details to identify current ownership.
- Each version is a `##` heading; task groups are `###` headings with `- [ ]` steps (`[-]` in progress, `[x]` done).
- When **all** steps in a version section are done: write a short descriptive entry to `CHANGELOG.md`, then remove the version section from `ROADMAP.md` to keep it concise.
- Finish each milestone's release checks **before** expanding scope. Cut map size and simulation complexity before compromising the core walking/driving experience.
- Define the release checks in `Docs/release-checks.md`. If a check fails, do not tag the release; open a patch task in `ROADMAP.md` and report the failing check in `CHANGELOG.md`.
## Parallel agent workflow
- Designate one coordinator to serialize assignments, updates to the shared roadmap, and integration into `main`. Agree on task and file or asset ownership before work starts; avoid assigning the same section or binary asset to multiple agents at once.
- The coordinator records the session ID, branch name, and owned files or assets alongside the active roadmap section and items on the shared `main` coordination copy. Publish and synchronize this state through the remote when agents use different machines. Branch-local changes are not visible to other worktrees until integrated; the roadmap is not an atomic lock.
- For parallel implementation, use a separate branch and Git worktree per agent, based on the agreed `main` revision. Do not switch branches in a directory another agent is using. Create branches, commits, pushes, and merges only when authorized; never overwrite another agent's uncommitted work.
- Keep ignored local references, generated files, build outputs, and Unreal editor processes in mind: worktrees do not automatically share ignored files, and shared output paths or concurrent editors can collide. Arrange required inputs and isolated outputs before running tools.
- Blender and Unreal binary assets cannot be meaningfully merged like text. Give each asset one owner; use Git LFS locks when supported by the remote, but do not assume locking is available.
- When work is ready, report the branch, changes, checks, unresolved issues, and relevant memory findings to the coordinator. Review and integrate branches into `main` one at a time, reconcile roadmap and memory changes, and update ownership and status. Follow the visual review and user-approval requirements before marking visual work done.
## Memory
**[MEMORY.md](MEMORY.md)** is shared session memory for agents and collaborators. Write to it whenever you:
- discover a fact about the environment, a tool, or a data source that cost time to establish;
- hit a gotcha, trap, or surprising failure mode, and what actually caused it;
- find or fix a bug, and how it was found so it can be found again;
- make a decision that does not fit a `ROADMAP.md` checkbox, such as a workaround, an assumption, or a rejected approach and why.
Keep entries short and dated, with the evidence: the command, the log line, or the file path. Anything that belongs in a formal document (setup steps, tool versions, licensing) goes in `Docs/` as well — memory is for coordination, not a replacement for the record. Do not let `MEMORY.md` become a second roadmap: planned work stays in `ROADMAP.md`.
Memory edits on an agent's branch are not shared live. Communicate important findings to the coordinator during work and reconcile them into the shared `main` copy when integrating.
+23 -17
View File
@@ -56,13 +56,13 @@ LD_LIBRARY_PATH="$B/blender/lib" "$B/blender/blender" -b --factory-startup --p
Spot checks against the measured cross-sections:
| Point | Terrain Z |
| --- | ---: |
| Origin, Hollywood × Highland | 0.00 |
| Orange Drive end of the block | +3.39 |
| North sidewalk mid-block | +1.43 |
| South gutter, mid-block | +0.93 |
| Road centre, mid-block | +1.38 |
| Point | Terrain Z |
| ----------------------------- | --------: |
| Origin, Hollywood × Highland | 0.00 |
| Orange Drive end of the block | +3.39 |
| North sidewalk mid-block | +1.43 |
| South gutter, mid-block | +0.93 |
| Road centre, mid-block | +1.38 |
That reproduces both the 3.4 m eastward fall and the ~2 % crown, so the DEM, the cross-sections, and this mesh agree.
@@ -80,7 +80,7 @@ LD_LIBRARY_PATH="$B/blender/lib" "$B/blender/blender" -b --factory-startup \
```
- `Road_Hollywood`: 8 120 verts, 7 812 faces, carriageway −10.5…+10.5 m, DEM elevations plus a 2 cm lift so it does not z-fight the terrain.
- `Sidewalk_South` 4.0 m wide, `Sidewalk_North` 5.3 m wide, both raised by the kerb height.
- `Sidewalk_South` 4.0 m wide, `Sidewalk_North` 5.3 m wide. The kerb upstand is **full at the road-side edge and tapers to zero at the building-side edge**, so the walk surface meets the terrain flush (before `0.1.1` the outer lip stood the full kerb height above the DEM — a 15.8 cm ledge).
- `Kerb_South`, `Kerb_North`: vertical faces, mostly **0.13 m** — the 0.15 m assumption minus nothing on the road side — and flushed to 0 within ±2.5 m of each signalised crossing with a 1.5 m blend. Measured from the built mesh: 256 steps at 0.13 m, 15 flush, a few in transition.
Crown check on the built mesh at mid-block: gutter −10.5 m → +0.99, −5 m → +1.38, centre → +1.52, +5 m → +1.58, +10.5 m → +1.52. The crown sits just north of the centreline, as the cross-sections said.
@@ -103,16 +103,20 @@ Objects are appended from the source files, not regenerated, and grouped into `S
A downward raycast on a 1 m grid along both sidewalks and the designated crossing, ignoring blockout meshes so marquees and canopies overhead do not count as ground:
| Route | Samples | Holes | Z range | Worst step |
| --- | ---: | ---: | --- | ---: |
| South sidewalk | 279 | 0 | −0.45 … +3.13 | 20.5 cm |
| North sidewalk | 279 | 0 | +0.07 … +3.83 | 15.1 cm |
| Crossing at 130 m | 26 | 0 | +1.16 … +1.80 | 8.6 cm |
| Route | Samples | Holes | Z range | Worst step |
| ----------------- | ------: | ----: | ------------- | ----------------: |
| South sidewalk | 279 | 0 | −0.48 … +3.06 | 13.4 cm (s=198) |
| North sidewalk | 279 | 0 | +0.06 … +3.76 | 6.9 cm (s=33) |
| Crossing at 130 m | 26 | 0 | +1.14 … +1.80 | 9.0 cm (off −8.5) |
At `--sample 0.25` the same routes measure 11.6 cm / 8.0 cm / 3.4 cm. The pre-check now prints the grade and the **location** of the worst step, and its coverage no longer shrinks with `--sample`.
No holes anywhere, so nothing drops the player on either side. The two flagged steps:
- **15.8 cm at s≈207**, where a sample crosses from `Terrain_DEM` onto `Sidewalk_South` — that is the sidewalk's outer edge, the 0.15 m kerb height minus the terrain. Mostly hidden by buildings, but it is a real edge and it will be visible anywhere a building is missing.
- **10–13 cm steps at s≈33, 39 and 126**, inside the sidewalk mesh: the kerb-flush transition at the crossings. That is a ~10 % ramp over 1.5 m, steeper than a real crossing ramp (8 % max). Worth grading properly when the crossings are detailed.
- ~~15.8 cm at s≈207, the sidewalk's outer edge~~ — **fixed in `0.1.1`**: the sidewalk upstand now tapers to zero at the building side, so the outer edge is flush with the terrain (north route 15.1 → 6.9 cm).
- ~~10–13 cm steps at the crossings, ~10 % ramp over 1.5 m~~ — **fixed in `0.1.1`**: the ramp run is now derived from a target grade (`--max-ramp-grade`, default 8 %), giving 1.63 m instead of the fixed 1.5 m (8.7 %). Measured at 0.25 m sampling the crossing now steps 3.4 cm (~1.4 % of the route) at the kerb line.
**Remaining finding (not a crossing, not the outer edge):** the south route still measures a **13.4 cm step at s≈198 m** (11.6 cm at 0.25 m sampling) and the north route 8.0 cm at s≈251 m. Neither is at a crossing and neither is the outer lip, so this is a local discontinuity in the sidewalk strip — most likely the DEM sampling or a centreline vertex near the block ends. Tracked under the `0.1.1` "audit any collision gaps" item rather than left undocumented.
This is a pre-check, not the `0.1.0` release check: the release check is traversal in the engine.
@@ -127,13 +131,15 @@ Blender works in metres, and the imported meshes match the OSM geometry, so the
| Building | Blender dimensions | OSM way outline | Height source |
| --- | --- | --- | --- |
| El Capitan Entertainment Centre | 25.72 × 54.47 × 14.50 m | 25.75 × 54.88 m | `height` tag 14.5 |
| El Capitan Theatre | 38.66 × 54.46 × 20.96 m | 38.59 × 54.87 m | `building:levels=6` × 3.2 m, plus roof |
| El Capitan Theatre | 38.66 × 54.46 × 20.55 m | 38.59 × 54.87 m | `building:levels=6` × **3.1 m**, plus ~1.9 m roof |
| Madame Tussaud's | 42.48 × 56.77 × 15.90 m | 42.0 m frontage | `height` tag 15.9 |
| TCL Chinese Theatre | 43.78 × 75.90 × 18.10 m | 44.0 m frontage | `height` tag 18.1 |
Widths agree to a few centimetres; depths differ by up to 0.4 m because Blosm assembles **multipolygon relations**, not the raw way I measured. Placement after the frame correction agrees with the UTM control points to 0.1–0.5 m per building — the residual is relation assembly and Blosm's edge straightening, not projection.
Buildings **without** a `height` tag get levels × 3.2 m, which is a placeholder, not evidence. Every such height is a modelling decision to be replaced from the reference photography.
Buildings **without** a `height` tag get levels × **3.1 m**. The storey height was 3.2 m until 2026-11-09, when a cross-check of every block building against Mapbox's building data (`Geo/tools/mapbox_buildings.py`) showed our figure running 0.6 m high per six levels — i.e. 3.1 m per level — and that calibration was adopted. The few buildings where Mapbox disagreed with a pure `levels × 3.1 m` fallback carry an explicit override in `Geo/tools/block_height_report.py::HEIGHT_OVERRIDES`.
Blosm adds roof mass on top of `levels × levelHeight`: the two `levels`-derived frontage buildings both land ~1.9 m above their table height (6922 39.1 m against 37.2 m, El Capitan Theatre 20.5 m against 18.6 m). That offset is the roof, not a height error — the table records the tagged height, the mesh records the storey plus its roof.
## Relation completeness
Binary file not shown.
+31
View File
@@ -0,0 +1,31 @@
{
"schema": 1,
"asset": "blockout",
"kind": "building",
"generated_utc": "2026-11-07T11:27:43+00:00",
"geographic": {
"crs": "EPSG:26911",
"source": "Geo/raw/osm-api-map-2026-11-06.osm, demo bounds per Geo/verification/2026-11-06-el-capitan-block.md",
"centre_easting_m": 376380.0,
"centre_northing_m": 3774200.0,
"elevation_navd88_m": 117.0,
"origin": {
"easting_m": 376515.389,
"northing_m": 3774224.083,
"reference": "Geo/manifests/local-origin.md"
}
},
"unreal": {
"centre_cm": [
-13538.9,
2408.3,
108.9
],
"conversion": "100 uu per metre, applied once in osm_common.to_unreal_local",
"axes": "X east, Y south (FBX preset flips north), Z up"
},
"dims_report": null,
"notes": [
"86 meshes from Blosm; affine-corrected from the spherical TM frame into EPSG:26911 (0.0 cm residual)"
]
}
@@ -40,7 +40,9 @@ def parse_args():
ap.add_argument("--osm", required=True, help="local .osm file to import")
ap.add_argument("--bounds", required=True, help="south,west,north,east in WGS84 degrees")
ap.add_argument("--out", required=True, help="output .blend path")
ap.add_argument("--level-height", type=float, default=3.2, help="metres per building level")
# 3.1 m, not 3.2: calibrated against Mapbox building data on 2026-11-09
# (see Geo/tools/mapbox_buildings.py and Geo/tools/block_height_report.py).
ap.add_argument("--level-height", type=float, default=3.1, help="metres per building level")
ap.add_argument("--origin", default=None,
help="lat,lon to use as the Blender origin (project origin). Default: Blosm centres on the extract")
ap.add_argument("--no-subdivide", action="store_true", help="skip OSM area subdivision")
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,185 @@
{
"asset": "hero-facade",
"frame": "project frame, metres, X east Y north Z up",
"bounds_m": {
"min": [
-13.08,
-12.5,
0.0
],
"max": [
13.08,
0.13,
14.5
],
"size": [
26.16,
12.63,
14.5
]
},
"parts": [
{
"object": "Hero_Mass",
"dims_m": [
24.25,
12.0,
14.5
],
"evidence": "measured frontage x assumption depth x measured height, inset behind the cornice"
},
{
"object": "Hero_Column_x6",
"dims_m": [
0.95,
0.95,
8.25
],
"evidence": "6 columns at 4.96 m centres (sibling notes x measured frontage)"
},
{
"object": "Hero_Flute_x120",
"dims_m": [
0.064,
0.064,
6.53
],
"evidence": "photographs: fluted shafts; 20 grooves per column modelled as shallow cylinders in the shadow tone rather than boolean cut grooves"
},
{
"object": "Hero_ColumnBase_x6",
"dims_m": [
1.36,
1.36,
0.22
],
"evidence": "assumption: Ionic base moulding; photographs show a wider base at the shaft foot"
},
{
"object": "Hero_Abacus_x6",
"dims_m": [
1.3,
1.3,
0.14
],
"evidence": "assumption: square abacus under the architrave, as Ionic capitals carry"
},
{
"object": "Hero_Architrave",
"dims_m": [
25.75,
1.65,
0.84
],
"evidence": "assumption: stacked entablature bands"
},
{
"object": "Hero_Cornice",
"dims_m": [
25.75,
1.75,
0.36
],
"evidence": "assumption: projecting cornice line"
},
{
"object": "Hero_Dentil_xN",
"dims_m": [
0.18,
0.3,
0.22
],
"evidence": "52 dentils at 0.45 m centres under the corona; assumption: photographed cornice reads as a dentilled course"
},
{
"object": "Hero_Entablature",
"dims_m": [
25.75,
1.5,
3.0
],
"evidence": "assumption: entablature depth"
},
{
"object": "Hero_Parapet",
"dims_m": [
25.75,
1.2,
1.8
],
"evidence": "assumption: parapet carries the motto band"
},
{
"object": "Hero_MottoBand",
"dims_m": [
22.75,
0.1,
0.85
],
"evidence": "photographs: motto panel above the colonnade, now an inset panel with a moulded surround rather than a flat stripe"
},
{
"object": "Hero_Medallion_x2",
"dims_m": [
1.1,
0.04,
1.1
],
"evidence": "photographs: two stone discs beside the motto, mounted on the parapet face"
},
{
"object": "Hero_Stylobate",
"dims_m": [
25.75,
2.4,
1.45
],
"evidence": "assumption: pedestal height; four piers and a lintel band leave three recesses"
},
{
"object": "Hero_PortalFlight_1",
"dims_m": [
3.1,
1.7,
1.25
],
"evidence": "5 steps at 0.15 m rise / 0.34 m tread (assumption; photos show 4-5, the old notes said 8)"
},
{
"object": "Hero_PortalFlight_2",
"dims_m": [
3.1,
1.7,
1.25
],
"evidence": "5 steps at 0.15 m rise / 0.34 m tread (assumption; photos show 4-5, the old notes said 8)"
},
{
"object": "Hero_PortalFlight_3",
"dims_m": [
3.1,
1.7,
1.25
],
"evidence": "5 steps at 0.15 m rise / 0.34 m tread (assumption; photos show 4-5, the old notes said 8)"
},
{
"object": "Hero_Banner_x3",
"dims_m": [
1.55,
0.06,
5.4
],
"evidence": "photographs: JIMMY KIMMEL LIVE banners on the column line"
},
{
"object": "Hero_Window_x10",
"dims_m": [
2.0,
0.08,
1.9
],
"evidence": "photographs: window bands behind the colonnade"
}
]
}
+31
View File
@@ -0,0 +1,31 @@
{
"schema": 1,
"asset": "hero-facade",
"kind": "building",
"generated_utc": "2026-11-07T11:26:11+00:00",
"geographic": {
"crs": "EPSG:26911",
"source": "OSM way 424024147 (El Capitan Entertainment Centre), 25.75 m frontage",
"centre_easting_m": 376387.944,
"centre_northing_m": 3774185.293,
"elevation_navd88_m": 116.6,
"origin": {
"easting_m": 376515.389,
"northing_m": 3774224.083,
"reference": "Geo/manifests/local-origin.md"
}
},
"unreal": {
"centre_cm": [
-12744.5,
3879.0,
68.9
],
"conversion": "100 uu per metre, applied once in osm_common.to_unreal_local",
"axes": "X east, Y south (FBX preset flips north), Z up"
},
"dims_report": "Art/blender/hero/hero-facade-2026-11-07.json",
"notes": [
"frontage 25.75 m and height 14.5 m measured; column bay 4.96 m from the sibling notes; portal flights from the photographs; everything else assumption"
]
}
+64
View File
@@ -0,0 +1,64 @@
# Reference analysis — retail frontage, 0.1.6
Phase 0 of the `references` pack (reference-image-match). Written before any geometry, because the pass that produced the hero façade skipped it and cost two rebuilds.
## Source
| | |
| --- | --- |
| Image | `Art/reference/retail-neighbours/01-lee-drug-and-neighbor.jpg` (1920×953) |
| Author / licence | Gb321, own work, **CC BY-SA 4.0** — attribution required |
| Captured | 2024-07-19 11:11 local |
| Description | "Lee Drug and 6806 Hollywood Buildings in Hollywood" |
| Coverage | Both target buildings in one frame, mid-morning sun from the south-east |
**One photograph is the whole evidence base for 6800 and 6806–6820.** It is a street-level oblique, not an elevation: verticals converge, the ground floor of 6806–6820 is partly behind a parked car, and the Lee Drug shopfront is behind a bus. Everything below that the photo does not resolve is marked _assumed_ and is carried into the part report.
**6822 Budget has no photograph.** It is a 6.0 m × 5.5 m single-storey sliver west of 6806–6820, and the TODO item already records that Commons has nothing usable. It stays a blockout extrusion and the item is deferred rather than guessed.
## Measured (not from the photo)
From `Geo/raw/osm-api-map-2026-11-06.osm` and the blockout, in the shared frame (metres, X east, Y north, Z from the project datum):
| Building | OSM way | X | Front face Y | Height | Depth |
| --- | --- | --- | --- | --- | --- |
| 6800 Lee Drug | 424024149 | −33.20 … −14.37 | −14.63 | 12.10 m | 33.5 m |
| 6806–6820 | 424024150 | −66.08 … −33.07 | −12.52 | 12.10 m | 30.8 m |
| 6822 Budget | 424024144 | −78.54 … −72.52 | −12.92 | 5.50 m | 15.4 m |
Both target buildings are 12.10 m tall, which makes the _relative_ proportions in the photo directly usable: the parapet line, the cornice and the shopfront line can be read as fractions of the frame height.
## 6800 Lee Drug — reading
| Category | Observation |
| --- | --- |
| Subject | Corner retail block at Hollywood × Highland; the street face and the corner return are both visible |
| Silhouette | Long low box, broken by a **rounded corner tower** that rises above the frontage parapet; the parapet is **stepped**, not flat |
| Storeys | Two zones, not storeys: a tall shopfront zone and a single tall display wall above it |
| Proportion (vertical) | Shopfront ≈ 0 → 0.36 H; bronze cornice ≈ 0.36 → 0.43 H; mural + lettering wall ≈ 0.43 → 0.82 H; stepped parapet ≈ 0.82 → 1.0 H (_assumed split; read off the frame, ±0.03 H_) |
| Proportion (horizontal) | Corner tower occupies ≈ 4 m of the east end; eight mural panels at roughly 2.2 m centres along the frontage |
| Colour | Cream stucco wall `#EDE6D8`, bronze/brown cornice `#6B4A2F`, near-black mural plates `#1C1A18`, dark glazing `#20262A` (_sampled from the frame_) |
| Unique features | Stepped Deco parapet with 4 horizontal reveal lines; projecting bronze cornice; row of black celebrity-portrait panels; large dark "SOUVENIRS OF …" lettering on the upper wall; shopfront glazing with slim mullions |
| Mood | 1930s Deco retail, freshly painted, bright midday |
## 6806–6820 — reading
| Category | Observation |
| --- | --- |
| Silhouette | Flat-fronted two-storey block; parapet steps up in fins at the west end |
| Proportion (vertical) | Shopfront ≈ 0 → 0.32 H; burgundy fascia band ≈ 0.32 → 0.38 H; window storey ≈ 0.38 → 0.62 H; blank wall + parapet ≈ 0.62 → 1.0 H (_assumed_) |
| Horizontal | Brown pilaster strips divide the frontage into roughly 3 m bays; seven upper windows across 33 m |
| Colour | Same cream stucco; brown pilasters `#6B4A2F`; **burgundy-green** fascia `#5B3A3A`; white window frames |
| Unique features | Banded parapet with 3 reveal lines; white-framed upper windows with security bars; shopfront glazing behind a burgundy fascia |
## Camera match (for the review shot)
The photo is a street-level oblique from the **north** side looking south-east, subject filling ~80 % of the frame width. The demo review shot must reproduce that: camera at 1.7 m on the north sidewalk, yaw toward the south frontage, 35–42° lens, both buildings in frame. A wide block view will not compare usefully against this crop.
## Assumptions to carry into the model report
1. Vertical proportions, as above — the photo's convergence makes a pixel-perfect read impossible.
2. Mural panel size and spacing; the lettering is modelled as a recessed plate, not as text.
3. Shopfront bay spacing on both buildings; the bus hides most of Lee Drug's ground floor.
4. Parapet reveal line count (4 on Lee Drug, 3 on 6806–6820).
5. Corner tower radius; only the outer curve is visible.
+83
View File
@@ -0,0 +1,83 @@
"""Export the site meshes from the assembled site .blend, one FBX per object.
blender -b --factory-startup Art/blender/site/site-2026-11-06.blend \
--python Art/blender/site/export_site_fbx.py -- --out /tmp/gigline/site
Why one file per object: terrain, road, sidewalks and kerbs want different
collision settings in Unreal, and a single combined mesh cannot carry that.
Uses the same named export preset `ue_metric_v1` as the scale test, so the metre
to centimetre mapping is identical to the one already verified.
"""
import argparse
import os
import sys
import bpy
sys.path.insert(0, os.path.join(os.path.join(os.path.dirname(__file__), "..", "..", ".."), "Tools"))
import fbx_export_preset as fbx_preset # noqa: E402
MESHES = ("Terrain_DEM", "Road_Hollywood", "Sidewalk_South", "Sidewalk_North",
"Kerb_South", "Kerb_North", "Crossing_Markings")
def main():
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
ap = argparse.ArgumentParser()
ap.add_argument("--out", default="/tmp/gigline/site")
args = ap.parse_args(argv)
os.makedirs(args.out, exist_ok=True)
print(f"[site] scene objects: {len(bpy.data.objects)}")
found = []
for name in MESHES:
obj = bpy.data.objects.get(name)
if obj is None or obj.type != "MESH":
print(f"[site] MISSING {name}")
continue
found.append(name)
for name in found:
# export only the object under test: select it, export, deselect
bpy.ops.object.select_all(action="DESELECT")
obj = bpy.data.objects[name]
obj.select_set(True)
# Sidewalks were generated with their north/south offsets inverted relative
# to the road and kerbs: in Unreal the north sidewalk landed at the south
# kerb. Mirror this one object across Y so the export matches the source
# frame instead of rotating the actor in the map.
mirrored = False # the south sidewalk is correct in the .blend; see street_mesh
if mirrored:
obj.scale.y *= -1.0
bpy.context.view_layer.objects.active = obj
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
# Object mode has no mesh context; flip winding through the mesh data.
me = obj.data
for poly in me.polygons:
verts = list(poly.vertices)
verts.reverse()
poly.vertices = verts
me.update()
_ = bpy.context.view_layer
bpy.context.view_layer.objects.active = obj
path = os.path.join(args.out, name + ".fbx")
fbx_preset.export_static(path, selection=True)
if mirrored:
# The flip is an export correction, not a change to the source: undo it.
obj.scale.y *= -1.0
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
for poly in obj.data.polygons:
verts = list(poly.vertices)
verts.reverse()
poly.vertices = verts
obj.data.update()
zs = [(obj.matrix_world @ v.co).z for v in obj.data.vertices]
print(f"[site] {name:18} verts {len(obj.data.vertices):6} z {min(zs):+.2f}..{max(zs):+.2f} m "
f"-> {path}")
print(f"[site] exported {len(found)} of {len(MESHES)} meshes")
main()
Binary file not shown.
@@ -26,7 +26,7 @@ import sys
import bpy
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "Geo", "scripts"))
sys.path.insert(0, os.path.join(HERE, "..", "..", "..", "Geo", "tools"))
import osm_common as oc # noqa: E402
ORIGIN_E = 376515.389
@@ -63,6 +63,7 @@ APPEND = {
"Art/blender/terrain/terrain-2026-11-06.blend": ["Terrain_DEM"],
"Art/blender/street/street-2026-11-06.blend": [
"Road_Hollywood", "Sidewalk_South", "Sidewalk_North", "Kerb_South", "Kerb_North",
"Crossing_Markings",
],
}
BLOCKOUT = "Art/blender/blockout/blockout-2026-11-06.blend"
@@ -164,10 +165,16 @@ def main():
def lat_lon(s, offset):
return oc.point_at(line, _lengths, s, offset, lat0, lon0)
# Sample spacing is independent of coverage: walk the whole block at
# `--sample` metres, so a finer spacing measures the gradient instead of
# accidentally shortening the route (previously range(int(block_len)) with a
# 0.25 m spacing only covered a quarter of the block).
along = int(block_len / args.sample)
across = int(26.0 / args.sample)
route_specs = {
"south sidewalk": [(s, -12.5) for s in [i * args.sample for i in range(int(block_len))]],
"north sidewalk": [(s, 13.0) for s in [i * args.sample for i in range(int(block_len))]],
"crossing at 130 m": [(130.0, o) for o in [i * args.sample - 12.5 for i in range(26)]],
"south sidewalk": [(i * args.sample, -12.5) for i in range(along)],
"north sidewalk": [(i * args.sample, 13.0) for i in range(along)],
"crossing at 130 m": [(130.0, i * args.sample - 12.5) for i in range(across)],
}
flat = [(spec, lat_lon(s, off)) for spec, samples in route_specs.items() for s, off in samples]
local = project_to_local([ll for _spec, ll in flat], find_proj())
@@ -184,16 +191,31 @@ def main():
heights.append(None)
else:
heights.append(z)
deltas = [abs(b - a) for a, b in zip(heights, heights[1:])
deltas = [(i, abs(b - a)) for i, (a, b) in enumerate(zip(heights, heights[1:]))
if a is not None and b is not None]
max_step = max(deltas) if deltas else float("nan")
worst_i, max_step = (max(deltas, key=lambda t: t[1]) if deltas else (0, float("nan")))
known = [h for h in heights if h is not None]
grade = (max_step / args.sample) * 100 if deltas else float("nan")
# Position of the worst step, as arc length / cross offset, so the cause
# can be located instead of guessed.
if label == "crossing at 130 m":
where = f"s=130 off={(worst_i * args.sample - 12.5):+.2f} m"
else:
where = f"s={worst_i * args.sample:.1f} m"
print(f" {label:18} samples {len(heights):3} holes {holes} "
f"Z {min(known):+.2f}..{max(known):+.2f} max step {max_step*100:.1f} cm")
f"Z {min(known):+.2f}..{max(known):+.2f} max step {max_step*100:.1f} cm "
f"({grade:.1f} % grade) at {where}")
out = os.path.abspath(args.out)
os.makedirs(os.path.dirname(out), exist_ok=True)
bpy.ops.wm.save_as_mainfile(filepath=out)
# *.blend is LFS-lockable, so a fresh checkout marks it read-only. Make it
# writable again or the save fails and downstream steps build from a stale
# file — which is worse than failing here.
if os.path.exists(out) and not os.access(out, os.W_OK):
os.chmod(out, 0o644)
result = bpy.ops.wm.save_as_mainfile(filepath=out)
if result != {"FINISHED"}:
raise RuntimeError(f"could not save {out}: {result}")
print(f"saved {out}")
Binary file not shown.
+31
View File
@@ -0,0 +1,31 @@
{
"schema": 1,
"asset": "street",
"kind": "street",
"generated_utc": "2026-11-07T11:27:43+00:00",
"geographic": {
"crs": "EPSG:26911",
"source": "Hollywood Blvd centreline between Orange Dr and Highland Ave, Geo/verification/2026-11-06-street-profile.md",
"centre_easting_m": 376375.6,
"centre_northing_m": 3774226.0,
"elevation_navd88_m": 117.3,
"origin": {
"easting_m": 376515.389,
"northing_m": 3774224.083,
"reference": "Geo/manifests/local-origin.md"
}
},
"unreal": {
"centre_cm": [
-13978.9,
-191.7,
138.9
],
"conversion": "100 uu per metre, applied once in osm_common.to_unreal_local",
"axes": "X east, Y south (FBX preset flips north), Z up"
},
"dims_report": null,
"notes": [
"carriageway -10.5..+10.5 m, kerbs 0.15 m assumed, sidewalks 4.0 m south / 5.3 m north"
]
}
@@ -18,6 +18,10 @@ Geometry follows the measured profile rather than a guess:
kerb (see `Geo/verification/2026-11-06-cross-sections.md`);
* kerbs are flushed to 0.02 m within ±2.5 m of each signalised crossing, with a
1.5 m blend, so the crossings are actually usable on foot;
* each signalised crossing gets continental zebra markings — 0.5 m white bars
with 0.5 m gaps across a 3 m band, laid 1 cm above the road surface — so the
mid-block crossing (OSM way 385648319, s ≈ 130 m) reads as a designated
crossing in game;
* sidewalk widths 4.0 m south and 5.3 m north, from the DEM-derived setback
arithmetic;
* the DEM supplies every elevation, so the 3.4 m fall and the ~2 % crown come
@@ -38,7 +42,7 @@ import sys
import bpy
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "Geo", "scripts"))
sys.path.insert(0, os.path.join(HERE, "..", "..", "..", "Geo", "tools"))
import dem_stats # noqa: E402
import osm_common as oc # noqa: E402
@@ -64,8 +68,13 @@ def parse_args():
ap.add_argument("--cross", type=float, default=0.75)
ap.add_argument("--kerb", type=float, default=0.15)
ap.add_argument("--ramp-half-width", type=float, default=2.5)
ap.add_argument("--ramp-blend", type=float, default=1.5)
return ap.parse_args(argv)
ap.add_argument("--ramp-blend", type=float, default=None,
help="override the ramp run; default derives it from --max-ramp-grade")
ap.add_argument("--max-ramp-grade", type=float, default=0.08,
help="maximum crossing-ramp slope (0.08 = 8%%)")
args = ap.parse_args(argv)
args.ramp_blend = ramp_blend(args.kerb, args.max_ramp_grade, args.ramp_blend)
return args
def find_proj():
@@ -121,7 +130,13 @@ class Dem:
def kerb_height(s, base, half_width, blend):
"""Full kerb, except flushed near a crossing so people can step off."""
"""Full kerb, except flushed near a crossing so people can step off.
`blend` is the horizontal run over which the kerb drops from `base` to the
0.02 m flush height. It is derived from a target maximum grade (see
`ramp_blend`) so the crossing ramps stay within a real ramp's slope instead
of the ~8.7 % the old fixed 1.5 m run produced.
"""
for crossing in CROSSINGS:
d = abs(s - crossing)
if d <= half_width:
@@ -132,6 +147,22 @@ def kerb_height(s, base, half_width, blend):
return base
def ramp_blend(kerb, max_grade, override=None):
"""Horizontal run needed for the kerb ramp to stay at or under max_grade.
0.15 m kerb at an 8 % grade needs 1.63 m of run; the previous fixed 1.5 m
gave 8.7 %, and the walk pre-check measured the transition as a 10-13 cm
step over 1.5 m. When `override` is given it wins (kept for reproducing old
meshes), otherwise the run is computed from the live kerb height so it stays
correct if the kerb height assumption changes.
"""
if override is not None:
return max(0.0, override)
if max_grade <= 0.0:
return 0.0
return max(0.0, (kerb - 0.02)) / max_grade
def strip(rows, offsets, z_at, name, material):
"""Build a quad grid from per-row sampled vertices."""
verts, faces = [], []
@@ -144,7 +175,10 @@ def strip(rows, offsets, z_at, name, material):
for r in range(len(rows) - 1):
for c in range(cols - 1):
i = r * cols + c
faces.append((i, i + 1, i + cols + 1, i + cols))
# Winding faces up: the natural (i, i+1, i+cols+1, i+cols) order
# produces downward normals (every surface faced the ground and was
# invisible in engine until the terrain corridor cut exposed it).
faces.append((i, i + cols, i + cols + 1, i + 1))
mesh = bpy.data.meshes.new(name)
mesh.from_pydata(verts, [], faces)
mesh.update()
@@ -211,13 +245,31 @@ def main():
z = dem_z(x, y)
return (z if z is not None else 0.0) + 0.02
def south_z(r, off, x, y):
def sidewalk_z(r, off, inner, outer, x, y):
"""Sidewalk surface: full kerb upstand at the road-side (inner) edge,
tapering to zero at the building-side (outer) edge so the walk surface
meets the terrain flush.
Before this taper the outer lip stood the full kerb height above the DEM,
which the walkability pre-check measured as a 15.8 cm step where a sample
crosses from Terrain_DEM onto Sidewalk_South (with a matching lip on the
north side). A 0.15 m lip over a 4.0 m / 5.3 m strip is a ~3-4 % ramp:
walkable and visually negligible, and it removes the ledge entirely.
"""
z = dem_z(x, y)
base = z if z is not None else 0.0
return base + kerb_height(rows[r][0], args.kerb, args.ramp_half_width, args.ramp_blend)
h = kerb_height(rows[r][0], args.kerb, args.ramp_half_width, args.ramp_blend)
span = inner - outer
if abs(span) < 1e-9:
return base + h
t = min(1.0, max(0.0, (off - outer) / span))
return base + h * t
def south_z(r, off, x, y):
return sidewalk_z(r, off, SOUTH_GUTTER, SOUTH_SIDEWALK, x, y)
def north_z(r, off, x, y):
return south_z(r, off, x, y)
return sidewalk_z(r, off, NORTH_GUTTER, NORTH_SIDEWALK, x, y)
road = strip(rows, road_offsets, road_z, "Road_Hollywood", asphalt)
sw_s = strip(rows, south_offsets, south_z, "Sidewalk_South", concrete)
@@ -235,7 +287,7 @@ def main():
verts.extend([(x, y, z_road), (x, y, z_walk)])
for idx in range(len(rows) - 1):
i = idx * 2
faces.append((i, i + 2, i + 3, i + 1))
faces.append((i, i + 1, i + 3, i + 2))
mesh = bpy.data.meshes.new(name)
mesh.from_pydata(verts, [], faces)
mesh.update()
@@ -247,6 +299,58 @@ def main():
kerb_faces(SOUTH_GUTTER, "Kerb_South")
kerb_faces(NORTH_GUTTER, "Kerb_North")
# Continental zebra markings: one band per signalised crossing. Bars run
# along the road (traffic direction), side by side across the carriageway;
# a pedestrian crosses perpendicular to them. Lifted 1 cm above the road
# surface (which is itself 2 cm above the DEM) to avoid z-fighting.
paint = bpy.data.materials.new("Road_Paint_White")
paint.diffuse_color = (0.85, 0.85, 0.85, 1.0)
# All three bands live in ONE object. Creating one object per crossing used
# to produce Crossing_Markings, .001 and .002; site assembly appends by the
# exact name, so only the first crossing ever reached the game.
band_verts, band_faces = [], []
def add_crossing_band(s_c):
# Stripes are emitted as two triangles each: the road crown makes the
# quad non-planar and the FBX SDK refuses to triangulate it on import
# ("unable to triangulate mesh" — the mesh is omitted entirely).
zone = 3.0 # band length along the road, m
w, g = 0.5, 0.5 # stripe width / gap, m
span = (SOUTH_GUTTER + 0.2, NORTH_GUTTER - 0.2)
n = int(zone // (w + g))
covered = n * w + (n - 1) * g # actual stripe coverage, centred on s_c
for k in range(n):
s0 = s_c - covered / 2 + k * (w + g)
base_idx = len(band_verts) # sampled positions, then vertices
pts = []
for s_edge in (s0, s0 + w):
base, normal, _ = at(s_edge)
for off in span:
x = base[0] + off * normal[0]
y = base[1] + off * normal[1]
# +6 cm above the DEM = 4 cm above the road surface: the road
# interpolates the DEM along 1 m rows while the markings sample
# only their own corners, so a 1 cm lift left parts of the
# stripes sunk under the road and invisible in game.
pts.append((x, y, (dem_z(x, y) or 0.0) + 0.06))
# pts order: (s0,off_s), (s0,off_n), (s1,off_s), (s1,off_n)
band_verts.extend(pts)
band_faces.append((base_idx + 0, base_idx + 3, base_idx + 1))
band_faces.append((base_idx + 0, base_idx + 2, base_idx + 3))
for s_c in CROSSINGS:
add_crossing_band(s_c)
mesh = bpy.data.meshes.new("Crossing_Markings")
mesh.from_pydata(band_verts, [], band_faces)
mesh.update()
mesh.materials.append(paint)
markings = bpy.data.objects.new("Crossing_Markings", mesh)
bpy.context.scene.collection.objects.link(markings)
print(f"crossing markings: {len(CROSSINGS)} bands, "
f"{len(markings.data.vertices)} verts, {len(markings.data.polygons)} faces")
for obj in (road, sw_s, sw_n):
print(f"{obj.name:16} verts {len(obj.data.vertices):5} faces {len(obj.data.polygons):5}")
@@ -261,7 +365,14 @@ def main():
out = os.path.abspath(args.out)
os.makedirs(os.path.dirname(out), exist_ok=True)
bpy.ops.wm.save_as_mainfile(filepath=out)
# *.blend is LFS-lockable, so a fresh checkout marks it read-only. Make it
# writable again or the save fails and downstream steps build from a stale
# file — which is worse than failing here.
if os.path.exists(out) and not os.access(out, os.W_OK):
os.chmod(out, 0o644)
result = bpy.ops.wm.save_as_mainfile(filepath=out)
if result != {"FINISHED"}:
raise RuntimeError(f"could not save {out}: {result}")
print(f"saved {out}")
Binary file not shown.
+31
View File
@@ -0,0 +1,31 @@
{
"schema": 1,
"asset": "terrain",
"kind": "terrain",
"generated_utc": "2026-11-07T11:27:43+00:00",
"geographic": {
"crs": "EPSG:26911",
"source": "3DEP 1 m DEM window, Geo/manifests/2026-11-06-3dep-dem.md",
"centre_easting_m": 376375.0,
"centre_northing_m": 3774225.0,
"elevation_navd88_m": 115.911,
"origin": {
"easting_m": 376515.389,
"northing_m": 3774224.083,
"reference": "Geo/manifests/local-origin.md"
}
},
"unreal": {
"centre_cm": [
-14038.9,
-91.7,
0.0
],
"conversion": "100 uu per metre, applied once in osm_common.to_unreal_local",
"axes": "X east, Y south (FBX preset flips north), Z up"
},
"dims_report": null,
"notes": [
"2 m grid, 191 x 106, building footprints cut out; Z relative to origin ground 115.911 m NAVD88"
]
}
@@ -31,7 +31,7 @@ import subprocess
import bpy
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "Geo", "scripts"))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "Geo", "tools"))
import dem_stats # noqa: E402
import osm_common as oc # noqa: E402
@@ -53,6 +53,9 @@ def parse_args():
ap.add_argument("--ymax", type=float, default=110.0)
ap.add_argument("--osm", default=None,
help="OSM extract; building outlines are cut out so rooftops never become terrain")
ap.add_argument("--corridor", type=float, default=10.7,
help="half-width in m of the Hollywood Blvd corridor to cut out so the "
"street meshes (road, kerbs, sidewalks) are not buried under the terrain")
return ap.parse_args(argv)
@@ -110,6 +113,30 @@ def pts_flat(ring):
return ring
def centreline_polyline(osm_path):
"""Hollywood Blvd centreline as a local-frame polyline (Highland -> Orange)."""
nodes, ways = oc.load(osm_path)
line, _, _, _, _ = oc.block_extent(
nodes, ways, "Hollywood Boulevard", "North Orange Drive", "North Highland Avenue")
utm = project_to_utm(line, find_proj())
return [(e - ORIGIN_E, n - ORIGIN_N) for e, n in utm]
def dist_to_polyline(x, y, pts):
best = float("inf")
for (ax, ay), (bx, by) in zip(pts, pts[1:]):
dx, dy = bx - ax, by - ay
l2 = dx * dx + dy * dy
if l2 == 0.0:
continue
t = max(0.0, min(1.0, ((x - ax) * dx + (y - ay) * dy) / l2))
px, py = ax + t * dx, ay + t * dy
d = (x - px) ** 2 + (y - py) ** 2
if d < best:
best = d
return best ** 0.5
def inside(x, y, ring):
"""Ray casting point-in-polygon."""
hit = False
@@ -146,9 +173,12 @@ def main():
f"X {args.xmin}..{args.xmax}, Y {args.ymin}..{args.ymax}")
polys = []
corridor = None
if args.osm:
polys = building_polygons(args.osm)
print(f"masking {len(polys)} building footprints")
corridor = centreline_polyline(args.osm)
print(f"cutting {args.corridor} m corridor around the Hollywood Blvd centreline")
verts = []
ok = []
@@ -168,6 +198,10 @@ def main():
verts.append((x, y, 0.0))
ok.append(False)
continue
if corridor is not None and dist_to_polyline(x, y, corridor) <= args.corridor:
verts.append((x, y, 0.0))
ok.append(False)
continue
z = values[ri * width + ci]
if nodata is not None and z == nodata:
verts.append((x, y, 0.0))
@@ -190,6 +224,12 @@ def main():
mesh = bpy.data.meshes.new("Terrain_DEM")
mesh.from_pydata(verts, [], faces)
mesh.update()
# Without a material the mesh imports into Unreal with the default white
# material and blows out under the demo sun. A muted earth tone keeps the
# ground reading as ground.
ground = bpy.data.materials.new("Ground_Earth")
ground.diffuse_color = (0.30, 0.28, 0.25, 1.0)
mesh.materials.append(ground)
obj = bpy.data.objects.new("Terrain_DEM", mesh)
bpy.context.scene.collection.objects.link(obj)
@@ -212,7 +252,14 @@ def main():
out = os.path.abspath(args.out)
os.makedirs(os.path.dirname(out), exist_ok=True)
bpy.ops.wm.save_as_mainfile(filepath=out)
# *.blend is LFS-lockable, so a fresh checkout marks it read-only. Make it
# writable again or the save fails and downstream steps build from a stale
# file — which is worse than failing here.
if os.path.exists(out) and not os.access(out, os.W_OK):
os.chmod(out, 0o644)
result = bpy.ops.wm.save_as_mainfile(filepath=out)
if result != {"FINISHED"}:
raise RuntimeError(f"could not save {out}: {result}")
print(f"saved {out}")
Binary file not shown.
+37
View File
@@ -0,0 +1,37 @@
{
"character": "GigLine walker",
"description": "lean adult pedestrian, short dark hair, olive skin, plain navy t-shirt, dark jeans, worn sneakers",
"model": "juggernautXL_ragnarok.safetensors",
"views": {
"front": {
"file": "Art/characters/blueprints/front.png",
"seed": 101,
"prompt": "GigLine walker, lean adult pedestrian, short dark hair, olive skin, plain navy t-shirt, dark jeans, worn sneakers, full body front view, neutral standing pose, arms at sides, even studio light, plain light grey background, character sheet",
"negative": "multiple people, crowd, text, watermark, signature, blurry, low quality, cropped limbs, extra limbs, deformed hands, cartoon, anime, logo",
"prompt_id": "ff81a0c1-e4d7-4740-9310-806c11600539"
},
"side": {
"file": "Art/characters/blueprints/side.png",
"seed": 202,
"prompt": "GigLine walker, lean adult pedestrian, short dark hair, olive skin, plain navy t-shirt, dark jeans, worn sneakers, full body side profile view, neutral standing pose, arms at sides, even studio light, plain light grey background, character sheet",
"negative": "multiple people, crowd, text, watermark, signature, blurry, low quality, cropped limbs, extra limbs, deformed hands, cartoon, anime, logo",
"prompt_id": "cdd0bfac-446b-4c5a-8375-37fb5c70a75e"
},
"back": {
"file": "Art/characters/blueprints/back.png",
"seed": 303,
"prompt": "GigLine walker, lean adult pedestrian, short dark hair, olive skin, plain navy t-shirt, dark jeans, worn sneakers, full body back view, neutral standing pose, arms at sides, even studio light, plain light grey background, character sheet",
"negative": "multiple people, crowd, text, watermark, signature, blurry, low quality, cropped limbs, extra limbs, deformed hands, cartoon, anime, logo",
"prompt_id": "069bc59d-93bb-41ce-a95b-f5b2e8194982"
},
"head": {
"file": "Art/characters/blueprints/head.png",
"seed": 404,
"prompt": "GigLine walker, lean adult pedestrian, short dark hair, olive skin, plain navy t-shirt, dark jeans, worn sneakers, head and shoulders portrait, front view, neutral expression, even studio light, plain light grey background",
"negative": "multiple people, crowd, text, watermark, signature, blurry, low quality, cropped limbs, extra limbs, deformed hands, cartoon, anime, logo",
"prompt_id": "39ab3ba6-5e6f-4a69-bdbb-b2faf4318dc0"
}
},
"contact_sheet": "Art/characters/blueprints/contact-sheet.png",
"note": "generated locally by Tools/comfyui_character_blueprint.py; record the model licence in THIRD_PARTY_NOTICES.md before shipping"
}
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.
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.
+288
View File
@@ -0,0 +1,288 @@
{
"source_project": "/run/media/system/DATA/Documents/06-games/security-game",
"scanned": "assets/models/blender/props + assets/models/blender_src",
"licence": "project-owned (same author); confirm before shipping",
"note": "audit only \u2014 nothing has been copied",
"props": [
{
"prop": "a_frame_sign",
"glb": "a_frame_sign.glb",
"bytes": 19120,
"blend_source": "a_frame_sign.blend",
"blend_bytes": 96742,
"triangles": 212,
"size_gltf": [
0.62,
0.965,
0.527
],
"materials": 4
},
{
"prop": "bench",
"glb": "bench.glb",
"bytes": 36268,
"blend_source": "bench.blend",
"blend_bytes": 102020,
"triangles": 560,
"size_gltf": [
1.8,
0.844,
0.522
],
"materials": 2
},
{
"prop": "bollard",
"glb": "bollard.glb",
"bytes": 10020,
"blend_source": "bollard.blend",
"blend_bytes": 92615,
"triangles": 138,
"size_gltf": [
0.25,
0.975,
0.25
],
"materials": 2
},
{
"prop": "crowd_barrier",
"glb": "crowd_barrier.glb",
"bytes": 24808,
"blend_source": "crowd_barrier.blend",
"blend_bytes": 96463,
"triangles": 388,
"size_gltf": [
2.248,
1.084,
0.613
],
"materials": 1
},
{
"prop": "double_door",
"glb": "double_door.glb",
"bytes": 33336,
"blend_source": "double_door.blend",
"blend_bytes": 101710,
"triangles": 380,
"size_gltf": [
2.165,
2.72,
0.2
],
"materials": 6
},
{
"prop": "fire_hydrant",
"glb": "fire_hydrant.glb",
"bytes": 34684,
"blend_source": "fire_hydrant.blend",
"blend_bytes": 102818,
"triangles": 610,
"size_gltf": [
0.53,
0.815,
0.47
],
"materials": 2
},
{
"prop": "handrail",
"glb": "handrail.glb",
"bytes": 15656,
"blend_source": "handrail.blend",
"blend_bytes": 93785,
"triangles": 268,
"size_gltf": [
2.209,
1.784,
0.1
],
"materials": 1
},
{
"prop": "marquee",
"glb": "marquee.glb",
"bytes": 126272,
"blend_source": "marquee.blend",
"blend_bytes": 144786,
"triangles": 2804,
"size_gltf": [
6,
2.044,
1.67
],
"materials": 3
},
{
"prop": "newspaper_box",
"glb": "newspaper_box.glb",
"bytes": 27304,
"blend_source": "newspaper_box.blend",
"blend_bytes": 99783,
"triangles": 300,
"size_gltf": [
0.44,
0.916,
0.386
],
"materials": 5
},
{
"prop": "palm_tree",
"glb": "palm_tree.glb",
"bytes": 20912,
"blend_source": "palm_tree.blend",
"blend_bytes": 100466,
"triangles": 400,
"size_gltf": [
4.0,
6.99,
3.9
],
"materials": 3
},
{
"prop": "planter_box",
"glb": "planter_box.glb",
"bytes": 63276,
"blend_source": "planter_box.blend",
"blend_bytes": 114862,
"triangles": 818,
"size_gltf": [
1.0,
1.118,
1.0
],
"materials": 4
},
{
"prop": "round_planter",
"glb": "round_planter.glb",
"bytes": 56664,
"blend_source": "round_planter.blend",
"blend_bytes": 113201,
"triangles": 808,
"size_gltf": [
0.759,
1.175,
0.736
],
"materials": 3
},
{
"prop": "security_podium",
"glb": "security_podium.glb",
"bytes": 18968,
"blend_source": "security_podium.blend",
"blend_bytes": 97217,
"triangles": 184,
"size_gltf": [
0.64,
1.167,
0.487
],
"materials": 5
},
{
"prop": "sedan",
"glb": "sedan.glb",
"bytes": 50348,
"blend_source": "sedan.blend",
"blend_bytes": 111241,
"triangles": 928,
"size_gltf": [
1.85,
1.32,
4.625
],
"materials": 6
},
{
"prop": "stanchion",
"glb": "stanchion.glb",
"bytes": 19048,
"blend_source": "stanchion.blend",
"blend_bytes": 99736,
"triangles": 434,
"size_gltf": [
1.095,
1.01,
0.34
],
"materials": 3
},
{
"prop": "street_lamp",
"glb": "street_lamp.glb",
"bytes": 22740,
"blend_source": "street_lamp.blend",
"blend_bytes": 99309,
"triangles": 428,
"size_gltf": [
1.3,
4.985,
0.5
],
"materials": 2
},
{
"prop": "street_tree",
"glb": "street_tree.glb",
"bytes": 119588,
"blend_source": "street_tree.blend",
"blend_bytes": 134001,
"triangles": 1506,
"size_gltf": [
2.191,
3.634,
2.133
],
"materials": 4
},
{
"prop": "traffic_cone",
"glb": "traffic_cone.glb",
"bytes": 13328,
"blend_source": "traffic_cone.blend",
"blend_bytes": 95292,
"triangles": 210,
"size_gltf": [
0.38,
0.69,
0.38
],
"materials": 3
},
{
"prop": "trash_bin",
"glb": "trash_bin.glb",
"bytes": 18584,
"blend_source": "trash_bin.blend",
"blend_bytes": 95841,
"triangles": 228,
"size_gltf": [
0.527,
0.92,
0.536
],
"materials": 3
},
{
"prop": "wall_lantern",
"glb": "wall_lantern.glb",
"bytes": 13752,
"blend_source": "wall_lantern.blend",
"blend_bytes": 93826,
"triangles": 180,
"size_gltf": [
0.22,
0.33,
0.365
],
"materials": 2
}
]
}
@@ -0,0 +1,20 @@
{
"tool": "Tools/generate_character_textures.py",
"asset": "T_Character_Hair",
"size": 512,
"procedural": true,
"meta": {
"strands": 190,
"base": [
44,
32,
26
],
"highlight": [
96,
74,
58
]
},
"note": "generated, not photographed; no third-party licence"
}
Binary file not shown.
@@ -0,0 +1,20 @@
{
"tool": "Tools/generate_character_textures.py",
"asset": "T_Character_SkinDetail",
"size": 512,
"procedural": true,
"meta": {
"octaves": {
"fine": 96,
"mid": 24,
"broad": 7
},
"detail": 9,
"tint": [
222,
178,
152
]
},
"note": "generated, not photographed; no third-party licence"
}
Binary file not shown.
+367 -3
View File
@@ -2,8 +2,372 @@
All notable changes to this project are documented here.
Format: [Keep a Changelog](https://keepachangelog.com/) style, versions follow [Semantic Versioning](https://semver.org/). Entries are added when a version section in `AGENTS.md` is completed.
Format: [Keep a Changelog](https://keepachangelog.com/) style, versions follow [Semantic Versioning](https://semver.org/). Entries are added when a version section in `TODO.md` is completed.
## Unreleased
## [Unreleased]
No completed releases yet.
Nothing yet.
## [0.2.3] — 2026-11-09
Camera collision worked in the code and never in the game.
### Fixed
- **The third-person boom never shortened.** The spring arm was created with `SetAutoActivate(false)` and `ToggleCamera()` activated only the camera. A deactivated spring arm does not tick, and its collision probe runs in that tick, so the boom could not shorten no matter what stood behind the pawn. Both `ToggleCamera` and `BeginPlay` now activate the arm with the rig.
### Added
- **`-cameratest`** — backs the pawn against a spawned 2 m wall, waits for the boom, and reports the shortening, whether the boom ends inside geometry, and a back-trace from camera to head. It writes `review-demo-camera-collision.png` so the placement can be checked by eye.
### Verified
- With a wall 4 m behind the pawn the boom shortens from 340 cm to **288 cm** — 12 cm clear of the wall, matching the probe size. Recorded as a release check.
- 8 review shots verify; 95 tool tests pass.
### Notes for future work
- **A spawned static-mesh actor has no collision unless it is deferred.** Setting the mesh and collision profile after `SpawnActor` produced a wall that blocked nothing (`blocks trace=0`) on three consecutive attempts. `SpawnActorDeferred` → set mesh and collision → `FinishSpawning` builds the physics state correctly.
- **`TargetArmLength` is not rewritten when the boom shortens.** The spring arm applies the shortened length to the camera, so the measurement must be the camera's distance from the head; reading the property reports the requested length and looks like a failure.
## [0.2.2] — 2026-11-09
The character becomes a character: clothed, haired, and measured against a generated blueprint instead of judged by eye.
**Version note:** this ships as a patch by the owner's decision even though it adds asset categories, which the release policy otherwise puts in a minor release. Recorded here so the numbering is not mistaken for scope later.
### 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.
### Changed
- **The sun was raised** from pitch -62 to -74. The character stood in building shadow lit almost entirely by the blue sky light, so warm skin sampled as blue-grey — face (99, 98, 100) before, (131, 121, 117) after at the same camera.
- **The walk cycle starts at maximum stride**, so the deterministic stride capture lands on the stride rather than on one of the two frames where the legs pass together.
- Cloth shells are fattened 8 mm along their own normals after solidify; 1 cm of solidify let the body through at shoulders, hips and knees.
### Fixed
- **Hair read blond and thin** because the strand sheet's pale RGB drove base colour; the sheet now supplies only the opacity mask, and the colour is a dark constant.
- **Garment coverage holes** at shoulders, hips and knees, closed by 2 cm solidify plus the normal fatten.
- **A crash of my own making:** binding materials in `Tick` with `ConstructorHelpers::FObjectFinder` is fatal outside a constructor. Moved to `BeginPlay` with `LoadObject`.
- **Shots 6, 7 and 8 were documented as an unclothed body** after the appearance changed; the README now matches the render and all eight shots are approved.
### Verified
- 8 review shots verify; **all eight approved** by the owner 2026-11-09.
- Blueprint comparison improved from mean ΔE **31.9 → 21.8** and palette ΔE **25.4 → 19.7** once the character was clothed.
- 95 tool tests pass (13 new for the colour maths, masks and palettes).
### Known limitations
- **The garments are still shells over body faces**, not simulated cloth: no wrinkles, no drape, and no separation from the body in motion.
- **`T_Character_Diffuse` cannot be used.** Sampling it renders the body black, so the MPFB mesh's UVs do not map onto that bake; skin is a constant tone and the generated pore sheet is unwired.
- **The character has no face detail** — the eyes, mouth and brows are mesh and lighting only, and read as a mannequin at close range.
- **Camera collision remains implemented but unexercised.**
## [0.2.1] — 2026-11-09
Rig fixes. Two defects from the `0.2.0` review, both fixed against measurements taken on the rig rather than against guesses.
### Fixed
- **The fingers splayed.** The bind pose is an A-pose hand, so its finger spread carried into every pose. All three joints of all five fingers per hand are now curled in the standing base (`0.28 / 0.34 / 0.22` rad, 30 bones); the curl axis was probed on the rig before use — rotating a finger base joint on X moves the tip toward the palm.
- **The arms-down rotation was hard-coded.** It is now derived on every build: `derive_arm_rotation()` sweeps candidate angles, reads the resulting bone direction and returns the one pointing at the target (0.60 rad, direction dot 1.000 in this build). The old 0.55 was a hand-swept constant that would have rotted on any rig change.
- **Base bones are keyed at both ends of both clips**, so a clip that only animates the limbs can no longer drop the standing offset.
### Known limitations
- **The stride capture regressed.** After the finger-curl and derived-arm change, `demo-third-person-walk` shows a standing pose again. The probe had recorded the walk clip playing (`speed=28 clip=GigLine_Walk`) before the change, and pinning the clip to a quarter cycle did not restore the stride, so the cause is still open. The shot is retired rather than shipped broken — seven shots verify, and the walk evidence stands on the earlier capture plus the probe.
- Camera collision remains implemented but unexercised against a wall.
## [0.2.0] — 2026-11-09
Third-person body. The pawn gains a visible, animated body and a third-person camera, so the walk cycle is finally something a player can see rather than a clip in an asset list.
### 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.
- **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`.
### Changed
- Character export preset now **bakes animation** (`bake_anim`, all actions). Without it the exporter wrote a static bind pose and Unreal imported a mesh with no clips and no error.
- `Docs/controls.md` documents the V toggle and the review switches; the in-game HUD line reads `WASD move · Mouse look · Space jump · V view · R reset · Esc pause`.
### Fixed
- **`PlayAnimation` on a hidden component never took effect.** The body is hidden at construction and only unhidden by the toggle, so the first call could land before the component was ready; the code now sets `AnimationSingleNode`, calls `SetAnimation` + `Play`, and re-asserts while the instance is missing.
- **The animation switch restarted the clip every frame**, freezing the body on frame 0.
- **The importer silently refused to replace the existing assets.** Changing the arm rotation and rebuilding returned the identical bone quaternion every time; `replace_existing` and `delete_asset` both failed because the pawn's constructor hard-references the clips. Importing under a new name (`CharacterV2`) finally landed the change — that is what unblocked the pose after a dozen failed rebuilds.
- **The review runner copied stale screenshots** from `Saved/`, so a shot retired from the camera list reappeared with byte-identical content from an older run. The folder is cleared before each capture.
- **Walking was never enabled for the stride shot**: the call sat in the shot sequencer's `BeginPlay`, which runs before the player pawn exists, so the cast silently found nothing and `speed` stayed at 0.
### Verified
- **8 review shots verify**; shots 6, 7 and 8 approved by the owner.
- **1440p, no frame generation, VSync off**: 759 frames, avg **2.64 ms (379 fps)**, p50 2.54, p95 3.39, p99 3.76, max 4.05 ms — about 2 % over `0.1.5` and 6.3× the 60 fps floor. Series: `Docs/perf/0.2.0-walk-frametimes.csv`.
- `-qaselftest`: walk, reset 0 cm from spawn, pause/unpause, transect 1456 samples / 0 holes, wall probe 0/35, façade probe 0/9 — all PASS.
- 82 tool tests pass.
### Known limitations
- **The fingers splay** — the bind's finger spread carries into the pose. Deferred to `0.2.2`.
- **The arms-down rotation was found by sweeping**, not derived: 0.55 rad puts the arms at the sides, 0.65 against the hip, 0.75 into the torso. A derived pose is a `0.2.2` job.
- **Camera collision is implemented but unproven** — the spawn point has open space behind it, so no shot exercises the boom shortening.
- The third-person body has no head or hand IK and no turn-in-place; it is a first playable, not a finished character.
## [0.1.6] — 2026-11-09
Retail frontage. The two retail buildings beside the hero are modelled from their own photograph instead of the generic kit, and the photograph is treated as evidence: an analysis is written before any geometry, and every dimension the image does not resolve is carried into the part report as an assumption.
### 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.
- **`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`.
### Changed
- **The generic façade kit skips the two modelled buildings** (`MODELLED` list in `ue_apply_facade_kit.py`). It applies to every `Frontage_*`/`North_*` actor, so it was pasting a second, contradicting façade language over the new trim.
- **Retail trim stages as `RetailTrim_*`** and is spawned through the demo builder, so it is regenerable rather than hand-placed.
- Trim stucco, white and glass slots are mapped onto the demo's look-dev materials so the walls sit in the same noise and texture family as the rest of the block; the tones sampled from the photograph (bronze, burgundy, mural, lettering) stay as authored.
### 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.
- **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.
- **6806–6820 parapet started at 0.62 H**, leaving a 4.6 m blank band; the window storey and parapet moved up to follow the photograph.
- **Mural panels, parapet and lettering were sized from the photograph's proportions, not the inherited defaults**: the first pass left a parapet that read as one blank white band.
### Verified
- `render_screenshots.sh --verify`: **5/5 shots OK**; scene audit **0 failures** (every mesh materialed, all spans in tolerance, no duplicate labels).
- Owner approved `demo-retail-frontage` (second pass) 2026-11-09; all five demo shots now carry approval.
- 82 tool tests pass.
### Known limitations
- **Wall lettering and the painted band do not read at 40 m.** A plate at 0.10 m still vanishes; this is a decal/texture problem, not geometry. Moved to `0.9.0 — Realistic buildings`.
- **The parapet has no moulded profile** and the corner tower is a fluted cylinder with two cap rings — both need the façade system in `0.9.0`.
- **6822 Budget is untouched**: no photograph of it exists. Deferred and blocked in `0.9.0`.
- The only reference for these two buildings is a single street-level oblique with a bus across Lee Drug's shopfront and a car across 6806's; several vertical proportions are assumptions recorded in the part report.
## [0.1.5] — 2026-11-09
Façade finishing. The kit gets the hero detail it was missing, and a class of silent defect is found and gated: the map had been rendering with **604 of 620 mesh actors unmaterialed** without a single error.
### 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.
- **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.
### Changed
- **Motto band is a real recess** now — moulded surround 6 cm proud, panel 12 cm behind — instead of an 8 cm dark stripe. Hero: 148 parts / 3 044 tris → 271 / 5 492, still 14.50 m.
- **Kit layout for wide and tall buildings**: bays are capped at 9 with the module widened (6922 was 13 bays across 65.8 m — a window every 4.96 m on a 37 m block), and the storey spread is derived from the available height so the top band always lands inside the volume.
### Fixed
- **604 of 620 mesh actors rendered untextured.** `ensure_material` deleted and recreated each site material on every run, orphaning every reference held by the kit pieces and the theatre trim; the map looked only slightly flat. Materials are now updated in place.
- **The hero could be teleported 13 m east onto the wrong side of the boulevard.** `ue_hero_facade_import.py` carried its own copy of the pre-`0.1.0` hero position; running the importer alone moved the building, invisible in a render and visible only as a reference-score drop. Placement now has one definition and the import asserts the staged span (`span ok: X -14644..-12028`).
- **53 duplicated kit actor labels** (both shop piers of a bay shared one name).
### Verified
- Scene audit: **0 failures** — every mesh materialed, all 7 verified spans within tolerance, no duplicate labels.
- Packaged `-qaselftest`: walk, reset, pause/unpause, seam transect 1456 samples / 0 holes, wall probe 0/35, façade probe 0/9 — all PASS.
- **1440p re-measured with the kit**: 2.76 ms average (**363 fps**), p99 3.64 ms, against `0.1.0`'s 2.42 ms (414 fps) — the 625 kit actors and 8 textures cost about 12 % and leave roughly six times the 60 fps floor. Series at `Docs/perf/0.1.5-kit-walk-frametimes.csv`.
- 78 tool tests pass.
### Known limitations
- **Retail neighbours 6822 and 6806–6820 are not modelled**: Commons holds no usable reference for either, and substituting the El Capitan Theatre's photographs would misattribute their façades. Tracked in `TODO.md`.
- Cornice dentil spacing, abacus and plinth dimensions remain recorded assumptions.
- Textures are generated, not authored — no decals, signage text or weathering maps.
- The vision pre-check stays advisory; it has rejected shots the owner approved.
## [0.1.4] — 2026-11-09
Building façades. The block goes from flat extruded boxes with flat colours to coursed, textured surfaces assembled from a reusable module kit, and the El Capitan Theatre gets the marquee and blade sign that make it read as itself.
### Added
- **Façade module kit** at the measured **4.96 m bay** / **3.10 m storey**: pilaster, sill, lintel, window bay, shopfront bay, door bay, cornice, parapet and coping — 16 `SM_` meshes, each validated for manifold topology (12 tris each) and assembled onto **eight buildings, 625 pieces**, from each building's own world bounds.
- **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.
### Changed
- **Elevation cameras** are levelled (pitch 0, so verticals stay parallel) and use a 42–55° lens to fill the frame from across the street.
- **`Screenshots/` holds demo-map captures only**; the review staging map is no longer rendered for review. Each shot carries its expected content and an approval state.
### Fixed
- **Blockout frame**: the `0.1.2` regeneration had run without `--origin`, shifting the whole frontage ≈119 m in X and 2.4 m in Y and dropping the hero out of the 6902/6838 gap. Regenerated with the affine fit (residual 0.0 cm) and every span re-verified against the block verification notes.
- **Kit placed ~12 km from its buildings** — it used the mesh-local bounding box, which excludes the actor offset.
- **Marquee mirrored across the boulevard** (Blender +Y is Unreal −Y) and **hanging 5 cm above the pavement** (absolute Z instead of pavement-relative).
- **Window bays invisible** inside the solid wall; the surround now projects with glazing inset.
- **Glazing was not glass**: kit pieces kept the grey material the FBX import creates.
- **Hero windows floated 1.44 m in front of the wall**; parapet medallions were flat discs on the coping.
- **Imports could report success while saving nothing** (read-only LFS map), and the north-buildings import failed silently on a missing `import sys`.
### Verified
- Packaged build `-qaselftest`: walk 761 cm, reset, pause/unpause, seam transect 1456 samples / 0 holes, wall probe 0/35, façade occlusion probe 0/9 — all PASS. Run twice; the harness is time-based and one loaded run reported a false failure.
- All eight buildings probed against the verified world spans.
- 67 tool tests pass.
### Known limitations
- Kit pieces are boxes; cornice dentil spacing, abacus and plinth dimensions are recorded assumptions.
- Textures are generated, not authored: no decals, signage text or weathering maps.
- The vision pre-check remains advisory — it is unreliable on this content.
- Retail neighbours, hero fluting and the blockout cleaning pass move to `0.1.5`.
- Performance has not been re-measured since `0.1.0`; 625 kit actors are unmeasured.
## [0.1.3] — 2026-11-09
Packaging and setup fixes. No gameplay or visual scope; this patch makes the pipeline and its documentation tell the truth about what `0.1.x` taught.
### 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.
### Changed
- `Docs/build.md`: content-pipeline table now lists every step as it exists (seven site FBXs, frontage, north buildings, hero, map) and the shared FBX preset; packaging archive corrected to `Builds/GigLineQA`; the packaged log location and `-qaselftest` usage are documented; new **traps** table covers the read-only save, the hero `--out`-is-the-FBX mistake, the MPFB add-on path, the shot actor quitting under QA, and the null-RHI trace trap.
- `Docs/setup.md`: the fresh-checkout build is marked verified (2026-11-08) and its blocker closed; a new section documents the two optional keys (`OPENROUTER_API_KEY`, `MAPBOX_ACCESS_TOKEN`) and that neither is needed by the build, the tests or a release.
- `Docs/Architecture/07_deployment_view.md` and the combined `architecture.md`: build-output path corrected and marked as living outside the repository.
### Verified
- 54 tool tests pass (5 new for `ue_common`).
- Packaged build still launches into `DemoMap` and `-qaselftest` passes every phase.
## [0.1.2] — 2026-11-09
Scale and façade correction patch. Buildings and the hero elevation now match the reference photography and the cross-checked height data; no new gameplay scope.
### 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.
- **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).
### Changed
- **Storey height 3.2 → 3.1 m**, calibrated against Mapbox building data, with explicit overrides for three rear outbuildings; the block cross-check now reports 66 matches and no disagreements.
- **Hero colonnade 47.6 % → 53.1 %** of the 14.50 m elevation, inside the photographed 50–60 % band (thinner entablature and parapet).
- Blockout regenerated at the new storey height; frontage re-exported and re-imported (6922 42.1 → 39.1 m, El Capitan Theatre 20.0 → 20.5 m).
- FBX exports now carry smoothing groups (`FACE` instead of `OFF`), so round geometry is no longer faceted and imports stop warning.
### Fixed
- **Windows floated 1.44 m in front of the wall**, ahead of the colonnade; they now sit on the wall plane behind the columns.
- **Parapet medallions were flat discs lying on the coping** instead of discs on the parapet face.
- **The hero façade had no usable collision** — the player could walk through it. It is now complex-as-simple; auto-generated convex hulls were tried and rejected because they bulged into the street.
- **The three "guessed" block heights were never buildings**: two `building=roof` canopies and a transit shelter. They are classified as non-building volumes and the block now reports zero guessed heights.
### Verified
- Packaged build `-qaselftest`: walk, reset and pause pass; seam transect 1456 samples / 0 holes; wall probe 0/35; façade probe 0/9.
- Screenshots re-rendered for every change and approved by the owner.
### Known limitations
- Cornice dentil spacing, the base and abacus dimensions are recorded assumptions: the reference photography does not resolve them at our scale.
- Façade surfaces remain flat blockout materials; the textured pass is `0.1.4`.
## [0.1.1] — 2026-11-08
Collision-fidelity patch. No new scope: street geometry corrections and the QA tooling that measures them.
### Fixed
- **Sidewalk outer edge ledge.** The sidewalk stood the full 0.15 m kerb height above the DEM at both edges, leaving a 15.8 cm lip where the walk route crossed from terrain onto the south sidewalk. The upstand now tapers from full at the road-side edge to zero at the building-side edge; the north route's worst step fell from 15.1 cm to 6.9 cm.
- **Crossing ramps too steep.** The kerb-flush run was a fixed 1.5 m, giving ~8.7 % (measured 10–13 cm over 1.5 m). The run is now derived from a target grade (`--max-ramp-grade`, default 8 % ⇒ 1.63 m); the crossing steps 3.4 cm at 0.25 m sampling.
### Added
- **Walk pre-check improvements** (`Art/blender/site/site_assembly.py`): sampling coverage is independent of `--sample` (it previously shrank the route), and the report now prints the grade and the arc-length/offset **location** of the worst step, so a step can be located rather than guessed.
### Changed
- **Kerb height recorded as final** at 0.15 m modelled / 0.13 m as built — a standard 6 in LA kerb; neither the 3DEP DEM nor the reference photography can resolve it further (±2 cm), which is far below the character's 45 cm step height.
### Verified
- Site meshes rebuilt and re-imported (7/7, collision flags intact), DemoMap rebuilt, packaged Development build `EXIT=0`.
- `-qaselftest` on the packaged build: walk 761 cm in 2 s at steady Z 456, reset 0 cm, pause/unpause, transect 1456 samples across 16 rows / **0 holes**, wall probe **0/35** blocking hits.
### Known limitations
- One residual **13.4 cm surface step at s≈198 m** on the south sidewalk (8.0 cm at s≈251 m north). It is not a gap and is below the 45 cm `MaxStepHeight`, so it does not block traversal; the cause is a sidewalk-strip discontinuity near a centreline vertex, recorded in `Art/blender/README.md` as a smoothness refinement.
- Kerb radii at junctions, driveway aprons, drainage inlets and pavement joints remain unmodelled (unchanged from `0.1.0`).
Nothing yet.
## [0.1.0] — 2026-11-08
The first playable release: a walkable exterior demo of the **6840 Hollywood Blvd** (El Capitan Entertainment Centre) block in Unreal Engine, built on OpenStreetMap layout data and USGS elevation, with the hero façade and the verified block frontage staged on a walkable street.
### Added
- **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`).
- **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.
- **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.
- **"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`.
- **Game target would not compile** — removed editor-only API use (`UStaticMesh::PostEditChange`, `AActor::GetActorLabel`).
- **`demo-north-buildings` screenshot** framed 85 % sky (camera 9.5 km west of the buildings); re-pointed to the building mass.
### Verified
- Both sides of the demo are traversable: 1456-sample seam transect, 0 holes, no blocking collision or invisible walls (`-qaselftest` on the packaged build).
- Placement and street scale match the reference photography and Geo measurements (road 24.3 m, sidewalk offsets 12.5/13.2 m, hero frontage 26.0 m).
- **414 fps average / p99 3.01 ms at 2560×1440** (Epic preset, TSR, VSync off, no frame generation) — far above the 60 fps target.
- Fresh checkout builds and packages with the documented external engine (UE 5.8.4).
### Known limitations
- Buildings other than the hero are flat blockout volumes; façade detail, materials and Blosm style scripts are planned for `0.1.4`.
- Kerb height (0.13 m as built vs 0.15 m assumed), kerb radii, driveway aprons and graded crossing ramps are deferred to `0.1.1`; crossing ramps are kerb flushes.
- One block only; no vehicles, NPCs, audio, or settings UI yet.
[Unreleased]: https://git.allucanget.biz/zwitschi/gig-line/compare/v0.1.5...main
[0.1.5]: https://git.allucanget.biz/zwitschi/gig-line/releases/tag/v0.1.5
[0.1.4]: https://git.allucanget.biz/zwitschi/gig-line/releases/tag/v0.1.4
[0.1.3]: https://git.allucanget.biz/zwitschi/gig-line/releases/tag/v0.1.3
[0.1.2]: https://git.allucanget.biz/zwitschi/gig-line/releases/tag/v0.1.2
[0.1.1]: https://git.allucanget.biz/zwitschi/gig-line/releases/tag/v0.1.1
[0.1.0]: https://git.allucanget.biz/zwitschi/gig-line/releases/tag/v0.1.0
@@ -0,0 +1,38 @@
# 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 |
@@ -0,0 +1,32 @@
# 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. |
+22
View File
@@ -0,0 +1,22 @@
# 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 |
+40
View File
@@ -0,0 +1,40 @@
# 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 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.
- **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.
- **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`.
+106
View File
@@ -0,0 +1,106 @@
# 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). Tool rules (one script one job, result files for commandlets, clean worktree for generators, documented inventory, tests for the pure parts) are in [`AGENTS.md`](../../AGENTS.md#python-and-shell-tooling).
### 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).
+53
View File
@@ -0,0 +1,53 @@
# 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.
+40
View File
@@ -0,0 +1,40 @@
# 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.
+55
View File
@@ -0,0 +1,55 @@
# 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.
@@ -0,0 +1,73 @@
# 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.
@@ -0,0 +1,45 @@
# 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 |
+10
View File
@@ -0,0 +1,10 @@
# 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. |
+21
View File
@@ -0,0 +1,21 @@
# 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. |
+13
View File
@@ -0,0 +1,13 @@
**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).
+570
View File
@@ -0,0 +1,570 @@
---
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).
+72 -9
View File
@@ -1,6 +1,6 @@
# Build
Fresh-checkout build procedure. Completed 2026-11-07 after the pinned engine was installed and a blank `Game/GigLine` build was packaged and launch-tested.
Fresh-checkout build, content-pipeline and packaging procedure for the walkable demo. Begun 2026-11-07 from a blank build and extended through the `0.1.x` patches; the release checks that exercise it are in [release-checks.md](release-checks.md).
## Constraints
@@ -10,8 +10,7 @@ Fresh-checkout build procedure. Completed 2026-11-07 after the pinned engine was
## Engine
Pinned install: **Unreal Engine 5.8.4** (CL 58902052, promoted build) at
`/run/media/system/DATA/Engines/UE_5.8.4`. The engine bundles **no** Linux clang; the system clang (23.1.4, Fedora 45) is the game-code toolchain. Game targets must use `BuildSettingsVersion.V7` to share the installed editor's build environment.
Pinned install: **Unreal Engine 5.8.4** (CL 58902052, promoted build) at `/run/media/system/DATA/Engines/UE_5.8.4`. The engine bundles **no** Linux clang; the system clang (23.1.4, Fedora 45) is the game-code toolchain. Game targets must use `BuildSettingsVersion.V7` to share the installed editor's build environment.
## Fresh-checkout build
@@ -42,14 +41,14 @@ cd /run/media/system/DATA/Engines/UE_5.8.4
Engine/Build/BatchFiles/RunUAT.sh BuildCookRun \
-project="/run/media/system/DATA/Documents/06-games/gig-line/Game/GigLine/GigLine.uproject" \
-noP4 -platform=Linux -clientconfig=Development \
-cook -allmaps -build -stage -pak \
-archive -archivedirectory="/run/media/system/DATA/Builds/GigLine" \
-cook -build -stage -pak \
-archive -archivedirectory="/run/media/system/DATA/Builds/GigLineQA" \
-unattended
# Expected: "BUILD SUCCESSFUL" (~1 min for the blank project; archive ~1.1 GiB)
# Launch test: /run/media/system/DATA/Builds/GigLine/Linux/GigLine.sh
# Expected: "AutomationTool exiting with ExitCode=0 (Success)" and "EXIT=0", ~90 s
# Launch test (plain, no -map): /run/media/system/DATA/Builds/GigLineQA/Linux/GigLine.sh
```
The archive directory is deliberately **outside** the repository; never archive into the checkout.
**Launch test must open the DemoMap.** A plain `./GigLine.sh` should load `/Game/Hollywood/DemoMap` (set via `DefaultEngine.ini` `GameMapsSettings`). If it instead opens `/Engine/Maps/Templates/OpenWorld`, the game shows an empty non-interactive "stuck" scene — check that `GameDefaultMap` is in `DefaultEngine.ini`, not `DefaultGame.ini`. Confirm the loaded map in the engine log: `.../Linux/GigLine/Saved/Logs/GigLine.log` (`LoadMap: /Game/Hollywood/DemoMap`). The archive directory is deliberately **outside** the repository; never archive into the checkout.
## Headless content checks
@@ -68,6 +67,39 @@ Both content-acceptance tests run headless and are the template for all asset im
# Note: 5.8 swallows Python print() in the commandlet; scripts write result files to /tmp.
```
## Content pipeline (current)
The hero block and the demo level are built by a fixed sequence. Each step writes a result file under `/tmp/gigline/`; read it, because the commandlet's own stdout can be swallowed.
| Step | Tool | Output |
| --- | --- | --- |
| OSM blockout | `Art/blender/blockout/blosm_blockout.py` | `Art/blender/blockout/blockout-*.blend` |
| Terrain | `Art/blender/terrain/terrain_from_dem.py` | `Art/blender/terrain/terrain-*.blend` |
| 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/` |
**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`.
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).
Two rules that the pipeline enforces rather than documents:
- **One conversion.** Metres to Unreal centimetres happens only in `Geo/tools/osm_common.py::to_unreal_local`. Sidecars (`<asset>.geo.json`) record the result and `Geo/tools/asset_metadata.py audit` re-derives it.
- **Collision is decided at import,** not later: terrain, road and sidewalks are complex-as-simple; kerbs and props stay simple. A simple hull over a kerbed street ramps straight across the kerb.
## Known engine quirks (2026-11-07)
- One handled `PSOPrecache` ensure fires at packaged-build exit only; not blocking.
@@ -76,4 +108,35 @@ Both content-acceptance tests run headless and are the template for all asset im
## Packaged Linux build
- 2026-11-07: blank `Game/GigLine` Development build packaged via the command above; 1.1 GiB archive at `/run/media/system/DATA/Builds/GigLine/Linux`; `GigLine.sh` launched on the desktop session, Vulkan swapchain created, 759 frames rendered across a 60 s smoke test. Controls document to be written with the first playable (`0.1.0` Gameplay section).
Current QA archive: `/run/media/system/DATA/Builds/GigLineQA`. A plain `./GigLine.sh` opens `/Game/Hollywood/DemoMap`; controls are in [controls.md](controls.md) and shown in-game bottom-right. The packaged log is written **relative to the wrapper's working directory**, i.e. `<archive>/Linux/GigLine/Saved/Logs/GigLine.log` — not into the repository.
Behaviour check without a human at the keyboard:
```bash
cd /run/media/system/DATA/Builds/GigLineQA/Linux
./GigLine.sh -windowed -ResX=1280 -ResY=720 -nosplash -qaselftest
# then read <archive>/Linux/GigLine/Saved/Logs/GigLine.log for "GigLine QA:" lines
```
`-qaselftest` walks for 2 s, resets, pauses/unpauses, sweeps a 16-row seam transect, fires a ground-referenced 21 m wall probe along the street, fires 40 m façade-occlusion traces sideways from the road centre wherever a building is staged, and writes a frame-time series to `Saved/Profiling/`. Every phase logs `... PASS`/`FAIL` and the run ends with `RESULT`.
## Traps that cost real time in `0.1.x`
| Symptom | Cause | Fix |
| --- | --- | --- |
| A builder reports success but the asset is unchanged | `.blend`/`.umap` are LFS-tracked and check out **read-only**; Blender and Unreal fail the save quietly | `chmod u+w <file>` first — `ue_build_*`/`ue_hero_facade_import` and every Blender builder need it |
| `File format is not supported` when reopening a `.blend` | `hero_facade_build.py --out` writes the **FBX**; putting the `.blend` path there overwrites it with FBX bytes | `--out /tmp/…fbx --save-blend Art/…blend --report Art/…json` |
| `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`) |
## 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.
- 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.
- The DemoMap is PIE-ready as committed: `GlobalDefaultGameMode` + `GameDefaultMap=/Game/Hollywood/DemoMap`, and the map stages a PlayerStart at `(-13336,-1150,600)` on the cut sidewalk with the walkable `AGigLineCharacter`.
Loaded 100 of 354 files, more files were not shown because too many files have changed in this diff. Show more