13 Commits
Author SHA1 Message Date
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
139 changed files with 5959 additions and 791 deletions

No files matched your search

+9
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
+54
View File
@@ -67,6 +67,7 @@ Rules for `TODO.md`:
- 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
@@ -110,6 +111,59 @@ Keep entries short and dated, with the evidence: the command, the log line, or t
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.
+13 -26
View File
@@ -80,10 +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. 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).
- `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.
@@ -106,32 +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.48 … +3.06 | 13.4 cm (s=198) |
| 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) |
| 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`.
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, 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.
- ~~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.
**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.
@@ -146,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.
+3 -1
View File
@@ -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.
+49 -13
View File
@@ -3,18 +3,18 @@
"frame": "project frame, metres, X east Y north Z up",
"bounds_m": {
"min": [
-13.02,
-13.08,
-12.5,
0.0
],
"max": [
13.02,
0.07,
13.08,
0.13,
14.5
],
"size": [
26.04,
12.57,
26.16,
12.63,
14.5
]
},
@@ -33,10 +33,37 @@
"dims_m": [
0.95,
0.95,
7.45
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": [
@@ -55,12 +82,21 @@
],
"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.4
3.0
],
"evidence": "assumption: entablature depth"
},
@@ -69,7 +105,7 @@
"dims_m": [
25.75,
1.2,
2.2
1.8
],
"evidence": "assumption: parapet carries the motto band"
},
@@ -77,19 +113,19 @@
"object": "Hero_MottoBand",
"dims_m": [
22.75,
0.08,
0.1,
0.85
],
"evidence": "photographs: motto panel above the colonnade"
"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,
1.1,
0.02
0.04,
1.1
],
"evidence": "photographs: two stone discs beside the motto"
"evidence": "photographs: two stone discs beside the motto, mounted on the parapet face"
},
{
"object": "Hero_Stylobate",
+4 -14
View File
@@ -15,6 +15,9 @@ 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")
@@ -60,20 +63,7 @@ def main():
_ = bpy.context.view_layer
bpy.context.view_layer.objects.active = obj
path = os.path.join(args.out, name + ".fbx")
bpy.ops.export_scene.fbx(
filepath=path,
use_selection=True,
object_types={"MESH"},
apply_unit_scale=True,
apply_scale_options="FBX_SCALE_NONE",
axis_forward="-Y",
axis_up="Z",
use_space_transform=True,
use_mesh_modifiers=True,
mesh_smooth_type="OFF",
path_mode="AUTO",
bake_space_transform=False,
)
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
+173 -90
View File
@@ -8,127 +8,210 @@ Format: [Keep a Changelog](https://keepachangelog.com/) style, versions follow [
Nothing yet.
## [0.1.1] — 2026-11-08
## [0.1.5] — 2026-11-09
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.
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
- **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.
- **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
- **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.
- **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
- 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.
- 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
- 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`).
- **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.
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.
- **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.
- **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).
- 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.
- 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.1...main
[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
@@ -20,11 +20,14 @@ Full requirements and progress are in [TODO.md](../../TODO.md); version directio
| Priority | Quality Goal | Scenario |
| --- | --- | --- |
| 1 | **Performance efficiency** | 60 FPS at 2560×1440 on RTX 5070 Ti, documented preset, no frame generation |
| 2 | **Terrain walkability** | Zero raycast holes across both sidewalks and the carriageway (84-sample transect passes) |
| 3 | **Geographic fidelity** | Building placement, street scale, and façade identity match reference photography |
| 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
+2 -1
View File
@@ -30,7 +30,8 @@ All modelling and geographic processing live outside the game project in `Art/`,
## Quality achievement
- **Performance**: dynamic-only lighting (no build step, no static shadow maps), world-space-noise materials (cheap procedural variation), minimal overdraw (simple street-level geometry).
- **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.
+4 -1
View File
@@ -60,8 +60,11 @@ Python scripts in `Tools/` drive the content pipeline:
| `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/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).
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
+1 -1
View File
@@ -7,7 +7,7 @@
| 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/GigLine` (packaged Linux build, ~1.1 GiB) |
| 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.
+25 -15
View File
@@ -2,24 +2,34 @@
## Quality Requirements Overview
| Category | Requirement | Priority |
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 |
| --- | --- | --- |
| Performance efficiency | 60 FPS at 2560×1440, documented preset, no frame generation | 1 |
| Reliability / Terrain | Zero raycast holes across sidewalks and carriageway (84-sample transect) | 1 |
| Accuracy / Fidelity | Building placement matches reference photography within ~1 m | 2 |
| Accuracy / Fidelity | Street scale (lane width, sidewalk width) within ~10 cm of measured profile | 2 |
| Usability | WASD + mouse look, jump, reset (R), pause (Escape) — no tutorial needed | 2 |
| Maintainability | Fresh checkout builds with documented engine installation | 2 |
| Portability | Linux build packages and runs; Windows build ships | 3 |
| 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 | Player walks both sides of the demo | No fall-through, character stays at ground Z | 0 raycast holes in 84-sample transect |
| QS-2 | Player holds W for 2 s on flat road | Character moves forward | ≥ 200 cm displacement, Z constant within ±5 cm |
| QS-3 | Player presses R | Character teleports to spawn | < 100 cm from `StartLocation` |
| QS-4 | Player presses Escape | Game pauses, cursor appears | `IsGamePaused()` = true |
| QS-5 | Player presses Escape while paused | Game unpauses, cursor hides | `IsGamePaused()` = false |
| QS-6 | Game runs on dev workstation at 1440p | Frame rate stable | ≥ 60 FPS, 99th percentile ≥ 55 FPS |
| QS-7 | Fresh checkout + engine install | Build succeeds | Exit code 0, runnable binary produced |
| 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.
+38 -20
View File
@@ -27,11 +27,14 @@ Full requirements and progress are in [TODO.md](../../TODO.md); version directio
| Priority | Quality Goal | Scenario |
| --- | --- | --- |
| 1 | **Performance efficiency** | 60 FPS at 2560×1440 on RTX 5070 Ti, documented preset, no frame generation |
| 2 | **Terrain walkability** | Zero raycast holes across both sidewalks and the carriageway (84-sample transect passes) |
| 3 | **Geographic fidelity** | Building placement, street scale, and façade identity match reference photography |
| 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
@@ -129,7 +132,9 @@ All modelling and geographic processing live outside the game project in `Art/`,
## Quality achievement
- **Performance**: dynamic-only lighting (no build step, no static shadow maps), world-space-noise materials (cheap procedural variation), minimal overdraw (simple street-level geometry).
- **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.
@@ -199,6 +204,9 @@ Python scripts in `Tools/` drive the content pipeline:
| `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/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).
@@ -297,7 +305,7 @@ Source data: `Art/blender/` (`.blend` files), `Geo/` (OSM extracts, DEM, verific
| 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/GigLine` (packaged Linux build, ~1.1 GiB) |
| 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.
@@ -447,27 +455,37 @@ Rule: place a building only against a neighbour or street mesh, never in isolati
## Quality Requirements Overview
| Category | Requirement | Priority |
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 |
| --- | --- | --- |
| Performance efficiency | 60 FPS at 2560×1440, documented preset, no frame generation | 1 |
| Reliability / Terrain | Zero raycast holes across sidewalks and carriageway (84-sample transect) | 1 |
| Accuracy / Fidelity | Building placement matches reference photography within ~1 m | 2 |
| Accuracy / Fidelity | Street scale (lane width, sidewalk width) within ~10 cm of measured profile | 2 |
| Usability | WASD + mouse look, jump, reset (R), pause (Escape) — no tutorial needed | 2 |
| Maintainability | Fresh checkout builds with documented engine installation | 2 |
| Portability | Linux build packages and runs; Windows build ships | 3 |
| 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 | Player walks both sides of the demo | No fall-through, character stays at ground Z | 0 raycast holes in 84-sample transect |
| QS-2 | Player holds W for 2 s on flat road | Character moves forward | ≥ 200 cm displacement, Z constant within ±5 cm |
| QS-3 | Player presses R | Character teleports to spawn | < 100 cm from `StartLocation` |
| QS-4 | Player presses Escape | Game pauses, cursor appears | `IsGamePaused()` = true |
| QS-5 | Player presses Escape while paused | Game unpauses, cursor hides | `IsGamePaused()` = false |
| QS-6 | Game runs on dev workstation at 1440p | Frame rate stable | ≥ 60 FPS, 99th percentile ≥ 55 FPS |
| QS-7 | Fresh checkout + engine install | Build succeeds | Exit code 0, runnable binary produced |
| 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.
# Risks and Technical Debts
+50 -37
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,20 +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 (plain, no -map): /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
```
**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.
**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
@@ -76,9 +69,7 @@ Both content-acceptance tests run headless and are the template for all asset im
## 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.
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 |
| --- | --- | --- |
@@ -86,21 +77,28 @@ can be swallowed.
| 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` | six FBXs, one per mesh |
| Hero façade | `Tools/hero_facade_build.py` | `Art/blender/hero/hero-facade-*.blend` + JSON report |
| 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.
- **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)
@@ -110,20 +108,35 @@ Two rules that the pipeline enforces rather than documents:
## 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.
Use to isolate whether the "no controls respond" bug is the packaged standalone's input intake or the Enhanced Input wiring generally.
- `Tools/run_pie.sh` opens the editor on `/Game/Hollywood/DemoMap`. Press the
green **Play** (PIE) button and walk / mouse-look.
- `Tools/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`.
- 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`.
+10 -16
View File
@@ -1,36 +1,30 @@
# Controls (`0.1.0` walkable demo)
Keyboard and mouse. The same list is shown in-game, bottom-right of the frame
(`AGigLineHUD`), and the packaged Linux build ships with these bindings.
Keyboard and mouse. The same list is shown in-game, bottom-right of the frame (`AGigLineHUD`), and the packaged Linux build ships with these bindings.
| Action | Key | Notes |
| --- | --- | --- |
| Move forward | `W` | 4 m/s walk |
| Move back | `S` | |
| Strafe right | `D` | |
| Strafe left | `A` | |
| Move back | `S` | |
| Strafe right | `D` | |
| Strafe left | `A` | |
| Look | Mouse | Captured on launch; `Escape` releases it |
| Jump | `Space` | |
| Jump | `Space` | |
| Reset to start | `R` | Returns to the grounded spawn point |
| Pause / release mouse | `Escape` | Second press resumes and recaptures the mouse |
## Notes
- Movement is relative to where you are looking (controller yaw), so the capsule
stays upright on the sloped street.
- The mouse is captured on launch, so looking works without clicking first.
`Escape` toggles the capture as part of pause.
- Bindings are Enhanced Input actions in `/Game/Input` built by
`Tools/ue_create_input_assets.py`; the mapping context is `IMC_Default`.
- Movement is relative to where you are looking (controller yaw), so the capsule stays upright on the sloped street.
- The mouse is captured on launch, so looking works without clicking first. `Escape` toggles the capture as part of pause.
- Bindings are Enhanced Input actions in `/Game/Input` built by `Tools/ue_create_input_assets.py`; the mapping context is `IMC_Default`.
## Testing input without a physical keyboard
`Tools/input_test.py` injects OS-level keyboard/mouse events through a virtual
`/dev/uinput` device (see `Tools/input_emulation.py`), e.g.:
`Tools/input_test.py` injects OS-level keyboard/mouse events through a virtual `/dev/uinput` device (see `Tools/input_emulation.py`), e.g.:
```bash
python3 Tools/input_test.py --sequence move:2,look:3
```
It drives the focused game window exactly like real hardware, which is how the
controls were regression-tested on the packaged build.
It drives the focused game window exactly like real hardware, which is how the controls were regression-tested on the packaged build.
+9 -26
View File
@@ -37,22 +37,12 @@ Important nuance: ODbL governs the **database** (the OSM data as a collection an
## 4. Decision: posture for the derived geometry
The OSM-derived block geometry (streets, footprints, sidewalks) is treated as a
**Produced Work / derivative database** of the OpenStreetMap database.
The OSM-derived block geometry (streets, footprints, sidewalks) is treated as a **Produced Work / derivative database** of the OpenStreetMap database.
- The **share-alike obligation is accepted for the data layer only**. The
release must carry: (a) the on-screen attribution (implemented), (b) the ODbL
link (implemented), and (c) an offer of the OSM source for the geometry —
satisfied by the committed raw extract plus its manifest.
- **Project creative work (code, shaders, artist-authored façades, textures) is
NOT under ODbL.** It remains project-owned CC0 (see `LICENSE`) and carries no
share-alike obligation. Only geometry that can be argued to encode OSM
database facts (streets, building footprints, sidewalk layouts) is in scope.
- **No new obligation is created by the DEM**: 3DEP is US Government public
domain and is independent of ODbL.
- **Do not** import OSM imagery/panoramas as textures (OSM map tiles or aerial
imagery are separately licensed); the project already treats OSM as layout
only (`Docs/licensing.md` rule 3).
- The **share-alike obligation is accepted for the data layer only**. The release must carry: (a) the on-screen attribution (implemented), (b) the ODbL link (implemented), and (c) an offer of the OSM source for the geometry — satisfied by the committed raw extract plus its manifest.
- **Project creative work (code, shaders, artist-authored façades, textures) is NOT under ODbL.** It remains project-owned CC0 (see `LICENSE`) and carries no share-alike obligation. Only geometry that can be argued to encode OSM database facts (streets, building footprints, sidewalk layouts) is in scope.
- **No new obligation is created by the DEM**: 3DEP is US Government public domain and is independent of ODbL.
- **Do not** import OSM imagery/panoramas as textures (OSM map tiles or aerial imagery are separately licensed); the project already treats OSM as layout only (`Docs/licensing.md` rule 3).
Release checklist items this lands:
@@ -63,20 +53,13 @@ Release checklist items this lands:
## 5. Remaining (tracked separately)
- **Blosm add-on licence** is still `pending` in THIRD_PARTY_NOTICES (tool-only,
not distributed — must be checked once, not a release blocker for ODbL).
- Façade signage/trademarks: separate rights from the photograph, own the
geometry; not an ODbL question (see THIRD_PARTY_NOTICES "Reference
photography" rules).
- **Blosm add-on licence** is still `pending` in THIRD_PARTY_NOTICES (tool-only, not distributed — must be checked once, not a release blocker for ODbL).
- Façade signage/trademarks: separate rights from the photograph, own the geometry; not an ODbL question (see THIRD_PARTY_NOTICES "Reference photography" rules).
## 6. Sources checked
- ODbL text: <https://opendatacommons.org/licenses/odbl/1-0/>
- OSM licence summary: <https://www.openstreetmap.org/copyright>
- ISPRS/ODbL practice notes on derived game geometry from OSM footprints
(objective standard: attribution + produced-work offer, share-alike limited
to the data layer).
- ISPRS/ODbL practice notes on derived game geometry from OSM footprints (objective standard: attribution + produced-work offer, share-alike limited to the data layer).
Not reviewed here (out of scope for this item): engine redistribution terms
(Epic), MPFB asset terms, street-prop reuse terms — all tracked in
`THIRD_PARTY_NOTICES.md`.
Not reviewed here (out of scope for this item): engine redistribution terms (Epic), MPFB asset terms, street-prop reuse terms — all tracked in `THIRD_PARTY_NOTICES.md`.
+728
View File
@@ -0,0 +1,728 @@
frame,ms
0,2.599
1,2.877
2,2.709
3,2.646
4,3.168
5,2.729
6,2.524
7,2.726
8,2.639
9,2.814
10,2.819
11,2.708
12,2.822
13,2.703
14,2.365
15,2.796
16,2.899
17,2.972
18,2.908
19,2.356
20,2.781
21,2.574
22,3.148
23,2.306
24,2.668
25,2.855
26,2.588
27,3.321
28,3.419
29,3.121
30,3.391
31,2.355
32,2.679
33,3.463
34,2.614
35,2.922
36,2.429
37,2.830
38,2.842
39,2.782
40,3.252
41,2.731
42,2.577
43,2.363
44,2.786
45,2.547
46,2.594
47,2.652
48,2.836
49,2.792
50,2.552
51,2.792
52,2.718
53,3.333
54,2.165
55,2.880
56,2.613
57,2.460
58,2.919
59,2.770
60,2.551
61,2.622
62,2.712
63,2.818
64,3.155
65,2.472
66,2.861
67,2.651
68,2.573
69,2.892
70,2.689
71,2.729
72,2.668
73,2.753
74,2.812
75,2.606
76,2.686
77,2.842
78,2.660
79,2.903
80,2.486
81,2.591
82,3.626
83,2.455
84,2.475
85,2.922
86,2.603
87,2.728
88,3.057
89,2.731
90,3.632
91,1.950
92,2.694
93,3.020
94,2.230
95,2.796
96,2.707
97,2.807
98,2.658
99,2.697
100,2.453
101,3.051
102,2.758
103,2.662
104,2.599
105,2.733
106,2.751
107,2.894
108,2.501
109,2.753
110,2.873
111,2.439
112,2.634
113,2.682
114,2.913
115,2.692
116,2.735
117,2.758
118,2.726
119,2.907
120,2.568
121,2.764
122,2.815
123,2.530
124,2.684
125,2.719
126,2.846
127,2.879
128,2.647
129,2.770
130,2.477
131,2.657
132,2.675
133,2.917
134,2.700
135,2.493
136,2.678
137,2.786
138,3.004
139,2.697
140,2.642
141,2.624
142,2.684
143,2.717
144,2.743
145,2.740
146,2.825
147,2.849
148,2.991
149,2.297
150,3.084
151,2.459
152,3.214
153,2.260
154,2.550
155,2.621
156,2.660
157,2.739
158,2.574
159,3.078
160,2.735
161,2.607
162,2.793
163,2.798
164,2.584
165,2.637
166,2.627
167,2.726
168,2.656
169,2.788
170,2.674
171,2.782
172,2.766
173,2.613
174,2.724
175,2.770
176,2.709
177,2.700
178,2.600
179,2.686
180,2.747
181,2.841
182,2.601
183,2.666
184,2.846
185,2.599
186,2.806
187,3.126
188,2.521
189,2.634
190,2.716
191,2.584
192,2.714
193,2.923
194,2.277
195,2.706
196,2.606
197,2.758
198,2.747
199,2.958
200,2.798
201,2.387
202,2.596
203,2.861
204,2.801
205,2.521
206,2.869
207,2.884
208,2.515
209,2.592
210,3.723
211,2.474
212,2.736
213,2.774
214,2.769
215,3.007
216,2.768
217,2.716
218,2.694
219,2.722
220,2.862
221,2.587
222,2.833
223,2.792
224,2.746
225,2.585
226,2.741
227,2.585
228,2.802
229,2.674
230,2.742
231,2.718
232,2.881
233,2.707
234,2.585
235,2.810
236,2.948
237,2.718
238,2.773
239,2.571
240,2.788
241,2.702
242,2.634
243,2.788
244,2.678
245,2.914
246,2.527
247,2.637
248,2.877
249,2.693
250,2.644
251,2.670
252,2.700
253,2.877
254,2.638
255,2.617
256,2.774
257,2.660
258,2.731
259,2.671
260,2.838
261,2.726
262,2.671
263,2.744
264,2.735
265,2.684
266,3.850
267,2.120
268,2.669
269,2.729
270,2.712
271,2.817
272,2.926
273,2.678
274,2.786
275,2.927
276,2.493
277,2.846
278,2.610
279,2.644
280,2.577
281,2.939
282,2.727
283,2.595
284,2.819
285,2.778
286,2.774
287,2.628
288,2.711
289,2.699
290,2.715
291,2.763
292,2.710
293,2.892
294,2.721
295,2.575
296,3.608
297,2.811
298,2.739
299,2.751
300,2.608
301,2.913
302,3.052
303,2.862
304,2.653
305,2.727
306,2.646
307,2.888
308,3.338
309,2.743
310,2.639
311,2.681
312,2.846
313,2.766
314,3.184
315,2.834
316,2.728
317,2.581
318,2.770
319,3.351
320,3.157
321,2.508
322,2.835
323,2.754
324,2.411
325,3.398
326,2.802
327,2.611
328,3.058
329,2.407
330,2.640
331,3.645
332,2.729
333,2.580
334,2.902
335,2.523
336,3.005
337,3.173
338,2.840
339,2.610
340,2.576
341,2.706
342,3.405
343,2.849
344,2.678
345,2.796
346,2.782
347,2.636
348,3.475
349,2.798
350,2.745
351,2.564
352,2.792
353,2.714
354,3.496
355,2.841
356,2.465
357,2.708
358,2.692
359,2.934
360,3.304
361,2.561
362,2.689
363,2.749
364,2.780
365,2.940
366,3.025
367,2.706
368,2.881
369,2.619
370,2.716
371,3.133
372,2.874
373,2.816
374,2.414
375,2.737
376,2.797
377,2.651
378,2.920
379,3.032
380,2.606
381,2.710
382,2.649
383,2.700
384,3.469
385,2.224
386,2.567
387,2.733
388,2.511
389,2.950
390,3.979
391,2.798
392,2.487
393,2.740
394,2.741
395,2.718
396,3.291
397,2.769
398,2.697
399,2.796
400,2.641
401,2.652
402,3.151
403,2.673
404,2.741
405,2.606
406,2.665
407,2.859
408,2.739
409,2.586
410,2.910
411,2.745
412,2.698
413,2.670
414,2.797
415,2.829
416,2.643
417,2.776
418,2.772
419,2.530
420,2.567
421,2.826
422,2.782
423,2.738
424,2.640
425,2.967
426,2.600
427,3.020
428,2.502
429,2.642
430,2.625
431,2.770
432,2.659
433,2.717
434,2.698
435,2.780
436,2.992
437,2.686
438,2.560
439,3.048
440,2.721
441,2.507
442,2.694
443,2.695
444,2.892
445,3.443
446,2.633
447,2.908
448,2.823
449,2.346
450,2.599
451,2.914
452,2.774
453,3.391
454,2.384
455,2.585
456,2.667
457,2.704
458,2.674
459,2.755
460,2.718
461,2.804
462,2.583
463,2.694
464,2.797
465,3.079
466,2.434
467,2.723
468,3.671
469,2.211
470,2.567
471,2.701
472,2.717
473,2.657
474,2.581
475,2.889
476,3.006
477,2.595
478,2.703
479,2.498
480,2.694
481,2.915
482,2.655
483,2.686
484,2.833
485,2.717
486,2.723
487,2.451
488,2.913
489,2.784
490,2.684
491,2.651
492,2.809
493,2.819
494,2.676
495,2.851
496,2.775
497,2.638
498,2.520
499,2.824
500,2.930
501,2.757
502,2.608
503,2.808
504,2.508
505,2.819
506,2.654
507,2.748
508,2.705
509,2.798
510,2.559
511,2.694
512,2.821
513,2.706
514,2.612
515,2.928
516,2.475
517,2.830
518,2.658
519,2.784
520,2.572
521,2.935
522,2.688
523,2.600
524,2.675
525,2.828
526,2.774
527,2.848
528,2.764
529,2.379
530,2.962
531,2.989
532,2.528
533,2.639
534,2.906
535,2.543
536,2.582
537,2.949
538,2.813
539,2.504
540,2.754
541,2.638
542,2.695
543,2.662
544,2.922
545,2.616
546,2.810
547,2.675
548,2.551
549,2.894
550,2.754
551,2.632
552,2.748
553,2.827
554,2.751
555,2.759
556,2.684
557,2.766
558,2.948
559,2.461
560,2.621
561,2.772
562,2.985
563,2.582
564,2.897
565,2.574
566,2.661
567,2.742
568,2.797
569,2.632
570,2.755
571,2.878
572,2.759
573,3.812
574,2.651
575,2.862
576,2.577
577,2.634
578,3.173
579,2.418
580,2.714
581,2.746
582,2.794
583,2.774
584,2.583
585,3.659
586,3.222
587,2.489
588,2.868
589,2.558
590,2.586
591,2.599
592,2.930
593,2.401
594,2.540
595,2.889
596,2.733
597,2.664
598,2.877
599,2.588
600,2.677
601,2.897
602,2.672
603,2.780
604,2.740
605,2.780
606,2.493
607,2.794
608,2.631
609,2.768
610,2.870
611,2.626
612,2.862
613,2.602
614,2.868
615,2.627
616,2.638
617,2.702
618,2.904
619,2.918
620,2.487
621,3.225
622,2.350
623,2.820
624,2.837
625,2.408
626,2.593
627,3.526
628,2.554
629,2.746
630,2.684
631,2.729
632,2.762
633,2.768
634,3.034
635,2.514
636,2.732
637,2.739
638,2.692
639,2.832
640,2.810
641,2.484
642,2.632
643,3.284
644,2.311
645,3.079
646,2.656
647,2.683
648,2.622
649,2.668
650,2.779
651,2.796
652,2.874
653,2.427
654,2.644
655,2.872
656,2.867
657,2.404
658,2.917
659,2.773
660,2.857
661,2.789
662,2.443
663,2.651
664,2.839
665,2.814
666,2.575
667,2.796
668,2.709
669,2.873
670,2.670
671,2.936
672,2.496
673,2.608
674,2.641
675,2.793
676,2.765
677,2.736
678,2.679
679,2.987
680,2.884
681,2.469
682,2.774
683,2.948
684,2.598
685,3.478
686,2.856
687,4.623
688,3.038
689,2.507
690,2.717
691,2.682
692,2.807
693,2.778
694,2.699
695,2.802
696,2.605
697,2.650
698,2.789
699,2.791
700,3.015
701,2.539
702,2.790
703,2.698
704,2.637
705,2.657
706,2.800
707,2.720
708,2.702
709,2.845
710,2.583
711,2.780
712,2.782
713,2.593
714,2.695
715,2.747
716,2.871
717,2.671
718,2.676
719,2.991
720,2.727
721,2.576
722,2.539
723,2.770
724,2.712
725,2.818
726,2.615
1 frame ms
2 0 2.599
3 1 2.877
4 2 2.709
5 3 2.646
6 4 3.168
7 5 2.729
8 6 2.524
9 7 2.726
10 8 2.639
11 9 2.814
12 10 2.819
13 11 2.708
14 12 2.822
15 13 2.703
16 14 2.365
17 15 2.796
18 16 2.899
19 17 2.972
20 18 2.908
21 19 2.356
22 20 2.781
23 21 2.574
24 22 3.148
25 23 2.306
26 24 2.668
27 25 2.855
28 26 2.588
29 27 3.321
30 28 3.419
31 29 3.121
32 30 3.391
33 31 2.355
34 32 2.679
35 33 3.463
36 34 2.614
37 35 2.922
38 36 2.429
39 37 2.830
40 38 2.842
41 39 2.782
42 40 3.252
43 41 2.731
44 42 2.577
45 43 2.363
46 44 2.786
47 45 2.547
48 46 2.594
49 47 2.652
50 48 2.836
51 49 2.792
52 50 2.552
53 51 2.792
54 52 2.718
55 53 3.333
56 54 2.165
57 55 2.880
58 56 2.613
59 57 2.460
60 58 2.919
61 59 2.770
62 60 2.551
63 61 2.622
64 62 2.712
65 63 2.818
66 64 3.155
67 65 2.472
68 66 2.861
69 67 2.651
70 68 2.573
71 69 2.892
72 70 2.689
73 71 2.729
74 72 2.668
75 73 2.753
76 74 2.812
77 75 2.606
78 76 2.686
79 77 2.842
80 78 2.660
81 79 2.903
82 80 2.486
83 81 2.591
84 82 3.626
85 83 2.455
86 84 2.475
87 85 2.922
88 86 2.603
89 87 2.728
90 88 3.057
91 89 2.731
92 90 3.632
93 91 1.950
94 92 2.694
95 93 3.020
96 94 2.230
97 95 2.796
98 96 2.707
99 97 2.807
100 98 2.658
101 99 2.697
102 100 2.453
103 101 3.051
104 102 2.758
105 103 2.662
106 104 2.599
107 105 2.733
108 106 2.751
109 107 2.894
110 108 2.501
111 109 2.753
112 110 2.873
113 111 2.439
114 112 2.634
115 113 2.682
116 114 2.913
117 115 2.692
118 116 2.735
119 117 2.758
120 118 2.726
121 119 2.907
122 120 2.568
123 121 2.764
124 122 2.815
125 123 2.530
126 124 2.684
127 125 2.719
128 126 2.846
129 127 2.879
130 128 2.647
131 129 2.770
132 130 2.477
133 131 2.657
134 132 2.675
135 133 2.917
136 134 2.700
137 135 2.493
138 136 2.678
139 137 2.786
140 138 3.004
141 139 2.697
142 140 2.642
143 141 2.624
144 142 2.684
145 143 2.717
146 144 2.743
147 145 2.740
148 146 2.825
149 147 2.849
150 148 2.991
151 149 2.297
152 150 3.084
153 151 2.459
154 152 3.214
155 153 2.260
156 154 2.550
157 155 2.621
158 156 2.660
159 157 2.739
160 158 2.574
161 159 3.078
162 160 2.735
163 161 2.607
164 162 2.793
165 163 2.798
166 164 2.584
167 165 2.637
168 166 2.627
169 167 2.726
170 168 2.656
171 169 2.788
172 170 2.674
173 171 2.782
174 172 2.766
175 173 2.613
176 174 2.724
177 175 2.770
178 176 2.709
179 177 2.700
180 178 2.600
181 179 2.686
182 180 2.747
183 181 2.841
184 182 2.601
185 183 2.666
186 184 2.846
187 185 2.599
188 186 2.806
189 187 3.126
190 188 2.521
191 189 2.634
192 190 2.716
193 191 2.584
194 192 2.714
195 193 2.923
196 194 2.277
197 195 2.706
198 196 2.606
199 197 2.758
200 198 2.747
201 199 2.958
202 200 2.798
203 201 2.387
204 202 2.596
205 203 2.861
206 204 2.801
207 205 2.521
208 206 2.869
209 207 2.884
210 208 2.515
211 209 2.592
212 210 3.723
213 211 2.474
214 212 2.736
215 213 2.774
216 214 2.769
217 215 3.007
218 216 2.768
219 217 2.716
220 218 2.694
221 219 2.722
222 220 2.862
223 221 2.587
224 222 2.833
225 223 2.792
226 224 2.746
227 225 2.585
228 226 2.741
229 227 2.585
230 228 2.802
231 229 2.674
232 230 2.742
233 231 2.718
234 232 2.881
235 233 2.707
236 234 2.585
237 235 2.810
238 236 2.948
239 237 2.718
240 238 2.773
241 239 2.571
242 240 2.788
243 241 2.702
244 242 2.634
245 243 2.788
246 244 2.678
247 245 2.914
248 246 2.527
249 247 2.637
250 248 2.877
251 249 2.693
252 250 2.644
253 251 2.670
254 252 2.700
255 253 2.877
256 254 2.638
257 255 2.617
258 256 2.774
259 257 2.660
260 258 2.731
261 259 2.671
262 260 2.838
263 261 2.726
264 262 2.671
265 263 2.744
266 264 2.735
267 265 2.684
268 266 3.850
269 267 2.120
270 268 2.669
271 269 2.729
272 270 2.712
273 271 2.817
274 272 2.926
275 273 2.678
276 274 2.786
277 275 2.927
278 276 2.493
279 277 2.846
280 278 2.610
281 279 2.644
282 280 2.577
283 281 2.939
284 282 2.727
285 283 2.595
286 284 2.819
287 285 2.778
288 286 2.774
289 287 2.628
290 288 2.711
291 289 2.699
292 290 2.715
293 291 2.763
294 292 2.710
295 293 2.892
296 294 2.721
297 295 2.575
298 296 3.608
299 297 2.811
300 298 2.739
301 299 2.751
302 300 2.608
303 301 2.913
304 302 3.052
305 303 2.862
306 304 2.653
307 305 2.727
308 306 2.646
309 307 2.888
310 308 3.338
311 309 2.743
312 310 2.639
313 311 2.681
314 312 2.846
315 313 2.766
316 314 3.184
317 315 2.834
318 316 2.728
319 317 2.581
320 318 2.770
321 319 3.351
322 320 3.157
323 321 2.508
324 322 2.835
325 323 2.754
326 324 2.411
327 325 3.398
328 326 2.802
329 327 2.611
330 328 3.058
331 329 2.407
332 330 2.640
333 331 3.645
334 332 2.729
335 333 2.580
336 334 2.902
337 335 2.523
338 336 3.005
339 337 3.173
340 338 2.840
341 339 2.610
342 340 2.576
343 341 2.706
344 342 3.405
345 343 2.849
346 344 2.678
347 345 2.796
348 346 2.782
349 347 2.636
350 348 3.475
351 349 2.798
352 350 2.745
353 351 2.564
354 352 2.792
355 353 2.714
356 354 3.496
357 355 2.841
358 356 2.465
359 357 2.708
360 358 2.692
361 359 2.934
362 360 3.304
363 361 2.561
364 362 2.689
365 363 2.749
366 364 2.780
367 365 2.940
368 366 3.025
369 367 2.706
370 368 2.881
371 369 2.619
372 370 2.716
373 371 3.133
374 372 2.874
375 373 2.816
376 374 2.414
377 375 2.737
378 376 2.797
379 377 2.651
380 378 2.920
381 379 3.032
382 380 2.606
383 381 2.710
384 382 2.649
385 383 2.700
386 384 3.469
387 385 2.224
388 386 2.567
389 387 2.733
390 388 2.511
391 389 2.950
392 390 3.979
393 391 2.798
394 392 2.487
395 393 2.740
396 394 2.741
397 395 2.718
398 396 3.291
399 397 2.769
400 398 2.697
401 399 2.796
402 400 2.641
403 401 2.652
404 402 3.151
405 403 2.673
406 404 2.741
407 405 2.606
408 406 2.665
409 407 2.859
410 408 2.739
411 409 2.586
412 410 2.910
413 411 2.745
414 412 2.698
415 413 2.670
416 414 2.797
417 415 2.829
418 416 2.643
419 417 2.776
420 418 2.772
421 419 2.530
422 420 2.567
423 421 2.826
424 422 2.782
425 423 2.738
426 424 2.640
427 425 2.967
428 426 2.600
429 427 3.020
430 428 2.502
431 429 2.642
432 430 2.625
433 431 2.770
434 432 2.659
435 433 2.717
436 434 2.698
437 435 2.780
438 436 2.992
439 437 2.686
440 438 2.560
441 439 3.048
442 440 2.721
443 441 2.507
444 442 2.694
445 443 2.695
446 444 2.892
447 445 3.443
448 446 2.633
449 447 2.908
450 448 2.823
451 449 2.346
452 450 2.599
453 451 2.914
454 452 2.774
455 453 3.391
456 454 2.384
457 455 2.585
458 456 2.667
459 457 2.704
460 458 2.674
461 459 2.755
462 460 2.718
463 461 2.804
464 462 2.583
465 463 2.694
466 464 2.797
467 465 3.079
468 466 2.434
469 467 2.723
470 468 3.671
471 469 2.211
472 470 2.567
473 471 2.701
474 472 2.717
475 473 2.657
476 474 2.581
477 475 2.889
478 476 3.006
479 477 2.595
480 478 2.703
481 479 2.498
482 480 2.694
483 481 2.915
484 482 2.655
485 483 2.686
486 484 2.833
487 485 2.717
488 486 2.723
489 487 2.451
490 488 2.913
491 489 2.784
492 490 2.684
493 491 2.651
494 492 2.809
495 493 2.819
496 494 2.676
497 495 2.851
498 496 2.775
499 497 2.638
500 498 2.520
501 499 2.824
502 500 2.930
503 501 2.757
504 502 2.608
505 503 2.808
506 504 2.508
507 505 2.819
508 506 2.654
509 507 2.748
510 508 2.705
511 509 2.798
512 510 2.559
513 511 2.694
514 512 2.821
515 513 2.706
516 514 2.612
517 515 2.928
518 516 2.475
519 517 2.830
520 518 2.658
521 519 2.784
522 520 2.572
523 521 2.935
524 522 2.688
525 523 2.600
526 524 2.675
527 525 2.828
528 526 2.774
529 527 2.848
530 528 2.764
531 529 2.379
532 530 2.962
533 531 2.989
534 532 2.528
535 533 2.639
536 534 2.906
537 535 2.543
538 536 2.582
539 537 2.949
540 538 2.813
541 539 2.504
542 540 2.754
543 541 2.638
544 542 2.695
545 543 2.662
546 544 2.922
547 545 2.616
548 546 2.810
549 547 2.675
550 548 2.551
551 549 2.894
552 550 2.754
553 551 2.632
554 552 2.748
555 553 2.827
556 554 2.751
557 555 2.759
558 556 2.684
559 557 2.766
560 558 2.948
561 559 2.461
562 560 2.621
563 561 2.772
564 562 2.985
565 563 2.582
566 564 2.897
567 565 2.574
568 566 2.661
569 567 2.742
570 568 2.797
571 569 2.632
572 570 2.755
573 571 2.878
574 572 2.759
575 573 3.812
576 574 2.651
577 575 2.862
578 576 2.577
579 577 2.634
580 578 3.173
581 579 2.418
582 580 2.714
583 581 2.746
584 582 2.794
585 583 2.774
586 584 2.583
587 585 3.659
588 586 3.222
589 587 2.489
590 588 2.868
591 589 2.558
592 590 2.586
593 591 2.599
594 592 2.930
595 593 2.401
596 594 2.540
597 595 2.889
598 596 2.733
599 597 2.664
600 598 2.877
601 599 2.588
602 600 2.677
603 601 2.897
604 602 2.672
605 603 2.780
606 604 2.740
607 605 2.780
608 606 2.493
609 607 2.794
610 608 2.631
611 609 2.768
612 610 2.870
613 611 2.626
614 612 2.862
615 613 2.602
616 614 2.868
617 615 2.627
618 616 2.638
619 617 2.702
620 618 2.904
621 619 2.918
622 620 2.487
623 621 3.225
624 622 2.350
625 623 2.820
626 624 2.837
627 625 2.408
628 626 2.593
629 627 3.526
630 628 2.554
631 629 2.746
632 630 2.684
633 631 2.729
634 632 2.762
635 633 2.768
636 634 3.034
637 635 2.514
638 636 2.732
639 637 2.739
640 638 2.692
641 639 2.832
642 640 2.810
643 641 2.484
644 642 2.632
645 643 3.284
646 644 2.311
647 645 3.079
648 646 2.656
649 647 2.683
650 648 2.622
651 649 2.668
652 650 2.779
653 651 2.796
654 652 2.874
655 653 2.427
656 654 2.644
657 655 2.872
658 656 2.867
659 657 2.404
660 658 2.917
661 659 2.773
662 660 2.857
663 661 2.789
664 662 2.443
665 663 2.651
666 664 2.839
667 665 2.814
668 666 2.575
669 667 2.796
670 668 2.709
671 669 2.873
672 670 2.670
673 671 2.936
674 672 2.496
675 673 2.608
676 674 2.641
677 675 2.793
678 676 2.765
679 677 2.736
680 678 2.679
681 679 2.987
682 680 2.884
683 681 2.469
684 682 2.774
685 683 2.948
686 684 2.598
687 685 3.478
688 686 2.856
689 687 4.623
690 688 3.038
691 689 2.507
692 690 2.717
693 691 2.682
694 692 2.807
695 693 2.778
696 694 2.699
697 695 2.802
698 696 2.605
699 697 2.650
700 698 2.789
701 699 2.791
702 700 3.015
703 701 2.539
704 702 2.790
705 703 2.698
706 704 2.637
707 705 2.657
708 706 2.800
709 707 2.720
710 708 2.702
711 709 2.845
712 710 2.583
713 711 2.780
714 712 2.782
715 713 2.593
716 714 2.695
717 715 2.747
718 716 2.871
719 717 2.671
720 718 2.676
721 719 2.991
722 720 2.727
723 721 2.576
724 722 2.539
725 723 2.770
726 724 2.712
727 725 2.818
728 726 2.615
+67 -91
View File
@@ -13,7 +13,19 @@ Record results in the table at the end of this file, with the date, the exact bu
3. **Licensing.** Every new asset has provenance in `THIRD_PARTY_NOTICES.md`, or is recorded as unresolved and kept out of the build.
4. **Version discipline.** Patch releases contain fixes, optimisation, docs, or tuning only — no new scope.
## Performance check
## `0.1.0` checks
1. **Realism first.** Building placement, street width, sidewalk depth, material read and site identity match the reference photography. Compare side by side and note every deliberate deviation. "Looks right" is not evidence.
2. Player traverses both the south-side frontage and the north-side frontage of the demo without falling through geometry, including the designated crossing. Walk the route twice: once normally, once with collision debug visualisation enabled.
3. No blocking collision, no invisible walls, no gaps at sidewalk/road seams.
4. Fresh checkout builds against the documented external engine installation.
5. Linux build packaged and launched; controls documented in `Docs/` and shown in-game.
6. **Performance last.** 60 FPS at 1440p per the performance check below — a floor every release meets, not the goal that shapes the content.
7. Tag `v0.1.0`, write the `CHANGELOG.md` entry, then remove the completed section from `TODO.md` (its goal stays in `ROADMAP.md`).
## Performance check (last)
Performance is the **last** check on purpose: realism leads the design and the frame budget only decides how much of it fits. It is still a floor — a release is not tagged below 60 FPS at 1440p without the owner's explicit approval.
Measured at 1440p on the development workstation: Bazzite, Ryzen 7 7700X, 64 GB RAM, RTX 5070 Ti 16 GB.
@@ -22,33 +34,20 @@ Measured at 1440p on the development workstation: Bazzite, Ryzen 7 7700X, 64 GB
- Target: 60 FPS at 1440p without frame generation.
- Capture a frame-time graph over a fixed traversal route, not a single sample. State the route.
## `0.1.0` checks
1. Player traverses both the south-side frontage and the north-side frontage of the demo without falling through geometry, including the designated crossing. Walk the route twice: once normally, once with collision debug visualisation enabled.
2. Building placement, street width, sidewalk depth, and site identity match the reference photography. Compare side by side and note every deliberate deviation. "Looks right" is not evidence.
3. No blocking collision, no invisible walls, no gaps at sidewalk/road seams.
4. 60 FPS at 1440p per the performance check above.
5. Fresh checkout builds against the documented external engine installation.
6. Linux build packaged and launched; controls documented in `Docs/` and shown in-game.
7. Tag `v0.1.0`, write the `CHANGELOG.md` entry, then remove the completed section from `TODO.md` (its goal stays in `ROADMAP.md`).
## Check-record template
| Version | Check | Result | Evidence | Date |
| ------- | ----------------------- | ------ | -------- | ---- |
| `0.1.0` | Traversal, both sides | PASS — Blender pre-check + in-engine swept transect, 0 holes, ground Z 317..395 | `-qaselftest`, TODO 0.1.0 notes | 2026-11-08 |
| Version | Check | Result | Evidence | Date |
| --- | --- | --- | --- | --- |
| `0.1.0` | Traversal, both sides | PASS — Blender pre-check + in-engine swept transect, 0 holes, ground Z 317..395 | `-qaselftest`, TODO 0.1.0 notes | 2026-11-08 |
| `0.1.0` | Placement vs references | PASS (with deliberate simplifications) | Section below, 2026-11-08 | 2026-11-08 |
| `0.1.0` | Collision and packaging | PASS — no blocking collision, no invisible walls, no seam gaps (see audit note) | `-qaselftest` on the packaged build, 2026-11-08 | 2026-11-08 |
| `0.1.0` | 60 FPS at 1440p | PASS — 2.42 ms avg (414 fps), p99 3.01 ms at 2560×1440, TSR on, VSync off, no FG | `-qaselftest` perf capture, `Docs/perf/0.1.0-walk-frametimes.csv` | 2026-11-08 |
| `0.1.0` | Fresh checkout build | PASS — clean clone of `main` (`4bc41e9`), `git lfs pull` + `check-lfs.sh` OK (130 LFS files), both `GigLineEditor` and `GigLine` targets built with the documented engine | `/tmp/fresh-check`, 2026-11-08 | 2026-11-08 |
| `0.1.0` | Packaged Linux build | PASS — packaged Development build launches straight into DemoMap, no missing-asset/linker errors; controls shown in-game and in `Docs/controls.md` | `Docs/perf/0.1.0-packaged-hud-controls.png`, 2026-11-08 | 2026-11-08 |
| `0.1.0` | 60 FPS at 1440p | PASS — 2.42 ms avg (414 fps), p99 3.01 ms at 2560×1440, TSR on, VSync off, no FG | `-qaselftest` perf capture, `Docs/perf/0.1.0-walk-frametimes.csv` | 2026-11-08 |
| `0.1.0` | Fresh checkout build | PASS — clean clone of `main` (`4bc41e9`), `git lfs pull` + `check-lfs.sh` OK (130 LFS files), both `GigLineEditor` and `GigLine` targets built with the documented engine | `/tmp/fresh-check`, 2026-11-08 | 2026-11-08 |
| `0.1.0` | Packaged Linux build | PASS — packaged Development build launches straight into DemoMap, no missing-asset/linker errors; controls shown in-game and in `Docs/controls.md` | `Docs/perf/0.1.0-packaged-hud-controls.png`, 2026-11-08 | 2026-11-08 |
## `0.1.0` — Placement vs references — record (2026-11-08)
Checked against the reference photography (`Art/reference/`) and the Geo measurements
(`Geo/verification/*`). Build under test: current `main` content, DemoMap rendered via
`Tools/render_screenshots.sh` (2026-11-08, Vulkan SM5, 1920×1080); engine positions
probed from the DemoMap (commandlet, 2026-11-08).
Checked against the reference photography (`Art/reference/`) and the Geo measurements (`Geo/verification/*`). Build under test: current `main` content, DemoMap rendered via `Tools/render_screenshots.sh` (2026-11-08, Vulkan SM5, 1920×1080); engine positions probed from the DemoMap (commandlet, 2026-11-08).
### Placement and scale (in-engine ↔ reference)
@@ -80,69 +79,43 @@ probed from the DemoMap (commandlet, 2026-11-08).
- **Crossing ramps are kerb flushes, not graded ramps** — the `0.1.1` Crossing ramp gradient item.
- Site identity (the neoclassical hero with motto band) matches the 2009 façade photograph and the 1922 Masonic elevation within blockout fidelity.
Pre-check method: `Tools/judge_screenshot.py` (vision model) flagged the simplified
flat surfaces as "unexpected" — those flags are the deliberate deviations above, not
placement errors. Final verdict is by human review of the shots in `Screenshots/demo/`.
Pre-check method: `Tools/judge_screenshot.py` (vision model) flagged the simplified flat surfaces as "unexpected" — those flags are the deliberate deviations above, not placement errors. Final verdict is by human review of the shots in `Screenshots/demo/`.
## `0.1.0` — Collision and packaging — audit note (2026-11-08)
Run on the packaged build (`GigLineQA`, Development, Vulkan SM5) with `-qaselftest`.
- **Transect (holes/gaps):** 1456 samples over 16 rows — lane centres **plus the
kerb/road/sidewalk seam Ys** (−1382, −1053, +718, +1047 and neighbours) and
X −20000..−2000. Result **0 holes**. (The old 84-sample check covered only 3
lane centres; the seam rows are what this check adds.)
- **Invisible-wall probe:** horizontal traces 30 m long, grounded at 160 cm above
the local ground, along the road and both seam rows. Result **0/35 blocking hits
inside 21 m** — no invisible walls or misplaced blockers in the street envelope.
(First pass used a fixed Z 160 start; the street is at Z ~366, so the probe
started inside the earth and reported every row as a dist-0 blocker. Grounding
the probe fixed that measurement bug.)
- **Walk / reset / pause:** 761 cm in 2 s (Z steady 456), reset 0 cm, pause
`IsGamePaused()=1`, unpause 0 — all PASS.
- Packaging: the packaged build launches, reaches gameplay, and the QA run quits
cleanly. The `GigLineShotActor` screenshot rounds are suppressed under
`-qaselftest` so they cannot quit the run before the audit phases.
- Caveat recorded: ground Z extremes (−12 m / 3000 cm) are trace-start/terrain-edge
artifacts of the widened extent, not walkable holes — the walkable corridor is
continuous (walk + transect both PASS).
- **Transect (holes/gaps):** 1456 samples over 16 rows — lane centres **plus the kerb/road/sidewalk seam Ys** (−1382, −1053, +718, +1047 and neighbours) and X −20000..−2000. Result **0 holes**. (The old 84-sample check covered only 3 lane centres; the seam rows are what this check adds.)
- **Invisible-wall probe:** horizontal traces 30 m long, grounded at 160 cm above the local ground, along the road and both seam rows. Result **0/35 blocking hits inside 21 m** — no invisible walls or misplaced blockers in the street envelope.
- **Facade occlusion probe:** from the road centre, 40 m traces sideways at walking height toward each frontage, required only where the map stages a building (the spans are listed in `AGigLineCharacter` and mirror `Tools/ue_build_demo_map.py`). Result **0/9 directions without a blocking wall** — the player cannot walk through the hero or the frontage. This probe found the hero façade had **no simple collision** at all (complex-only, so traces and capsule sweeps passed through it); it is now `UseComplexAsSimple`. Auto-generated convex hulls were tried first and rejected: on a 95-part, 26 m façade they bulged into the street (the wall probe went 0/35 → 2/35). (First pass used a fixed Z 160 start; the street is at Z ~366, so the probe started inside the earth and reported every row as a dist-0 blocker. Grounding the probe fixed that measurement bug.)
- **Walk / reset / pause:** 761 cm in 2 s (Z steady 456), reset 0 cm, pause `IsGamePaused()=1`, unpause 0 — all PASS.
- Packaging: the packaged build launches, reaches gameplay, and the QA run quits cleanly. The `GigLineShotActor` screenshot rounds are suppressed under `-qaselftest` so they cannot quit the run before the audit phases.
- Caveat recorded: ground Z extremes (−12 m / 3000 cm) are trace-start/terrain-edge artifacts of the widened extent, not walkable holes — the walkable corridor is continuous (walk + transect both PASS).
## `0.1.0` — 60 FPS at 1440p — record (2026-11-08)
**Build under test:** packaged `GigLineQA` Development, UE **5.8.4**, Vulkan SM5,
NVIDIA 615.78.08 (RTX 5070 Ti), Bazzite, Ryzen 7 7700X, 64 GB RAM.
**Build under test:** packaged `GigLineQA` Development, UE **5.8.4**, Vulkan SM5, NVIDIA 615.78.08 (RTX 5070 Ti), Bazzite, Ryzen 7 7700X, 64 GB RAM.
**Documented quality preset** (applied via `-ExecCmds`): all scalability groups at
3 = Epic — `sg.ViewDistanceQuality 3, sg.ShadowQuality 3,
sg.GlobalIlluminationQuality 3, sg.ReflectionQuality 3, sg.PostProcessQuality 3,
sg.TextureQuality 3, sg.EffectsQuality 3, sg.FoliageQuality 3, sg.ShadingQuality 3`.
**Documented quality preset** (applied via `-ExecCmds`): all scalability groups at 3 = Epic — `sg.ViewDistanceQuality 3, sg.ShadowQuality 3, sg.GlobalIlluminationQuality 3, sg.ReflectionQuality 3, sg.PostProcessQuality 3, sg.TextureQuality 3, sg.EffectsQuality 3, sg.FoliageQuality 3, sg.ShadingQuality 3`.
**Upscaling state (recorded from the run):** `r.AntiAliasingMethod=4` → **TSR on**,
default screen percentage, `r.VSync=0` (**off**), frame generation **off/unavailable**
(`r.Streamline.FrameGeneration.Mode` absent → -999).
**Upscaling state (recorded from the run):** `r.AntiAliasingMethod=4` → **TSR on**, default screen percentage, `r.VSync=0` (**off**), frame generation **off/unavailable** (`r.Streamline.FrameGeneration.Mode` absent → -999).
**Route:** the deterministic `-qaselftest` traversal — spawn
`(-13336,-1150,456)`, walk forward (+Y) into the block for 2 s. Frame times are
captured every frame over that route (829 frames), not a single sample.
**Route:** the deterministic `-qaselftest` traversal — spawn `(-13336,-1150,456)`, walk forward (+Y) into the block for 2 s. Frame times are captured every frame over that route (829 frames), not a single sample.
**Result at 2560×1440:**
| Metric | Value |
| --- | --- |
| Frames captured | 829 |
| Average | **2.42 ms → 414 fps** |
| p50 | 2.38 ms |
| p95 | 2.71 ms |
| p99 | 3.01 ms (≈332 fps) |
| Max | 4.00 ms |
| Metric | Value |
| --------------- | --------------------- |
| Frames captured | 829 |
| Average | **2.42 ms → 414 fps** |
| p50 | 2.38 ms |
| p95 | 2.71 ms |
| p99 | 3.01 ms (≈332 fps) |
| Max | 4.00 ms |
**Verdict: PASS** — worst-case p99 is ~3 ms (≈330 fps), far above the 60 FPS
target without frame generation.
**Verdict: PASS** — worst-case p99 is ~3 ms (≈330 fps), far above the 60 FPS target without frame generation.
**Variance note:** two back-to-back runs measured ~9.2 ms (≈108 fps) when another
game instance / busy compositor overlapped the capture; repeated clean runs
(windowed and fullscreen, Wayland and X11 drivers, window focused) all measured
~2.4–2.5 ms. The high outlier is recorded here rather than hidden.
**Variance note:** two back-to-back runs measured ~9.2 ms (≈108 fps) when another game instance / busy compositor overlapped the capture; repeated clean runs (windowed and fullscreen, Wayland and X11 drivers, window focused) all measured ~2.4–2.5 ms. The high outlier is recorded here rather than hidden.
**Reproduce:**
@@ -154,10 +127,25 @@ game instance / busy compositor overlapped the capture; repeated clean runs
Committed frame-time series: `Docs/perf/0.1.0-walk-frametimes.csv`.
### `0.1.5` re-measure with the façade kit in the map
Same route, same preset, packaged Development build, **2560×1440**:
| Metric | `0.1.0` | `0.1.5` (kit + textures) |
| ------- | ----------------: | -----------------------: |
| Average | 2.42 ms (414 fps) | **2.76 ms (363 fps)** |
| p50 | 2.38 ms | 2.73 ms |
| p95 | 2.71 ms | 3.17 ms |
| p99 | 3.01 ms | 3.64 ms |
| Max | 4.00 ms | 4.62 ms |
The **625 kit actors and 8 generated textures cost about 12 %** of frame time and still leave **363 fps average, p99 3.64 ms** — roughly six times the 60 fps floor. Frame-time series: `Docs/perf/0.1.5-kit-walk-frametimes.csv`.
**Measurement trap, again:** runs that reported ~105 fps were two game instances overlapping (a previous launch had not exited). Confirm a single instance (`pgrep -af Binaries/Linux/GigLine`) before trusting a figure — 363 fps reproduced twice on a clean run.
## `0.1.0` — Fresh checkout build — record (2026-11-08)
Followed the documented steps in [`Docs/build.md`](build.md) exactly, into a clean
directory with no prior state:
Followed the documented steps in [`Docs/build.md`](build.md) exactly, into a clean directory with no prior state:
```bash
git clone https://git.allucanget.biz/zwitschi/gig-line.git /tmp/fresh-check
@@ -170,35 +158,23 @@ Engine/Build/BatchFiles/Linux/Build.sh GigLine Linux Development \
-project=/tmp/fresh-check/Game/GigLine/GigLine.uproject -WaitMutex
```
- Clone of `main` at `4bc41e9`; LFS objects fetched (`DemoMap.umap` is real UE
package content, not a pointer).
- `GigLineEditor` → **Result: Succeeded** (28.2 s); produced
`/tmp/fresh-check/Game/GigLine/Binaries/Linux/libUnrealEditor-GigLine.so`.
- Clone of `main` at `4bc41e9`; LFS objects fetched (`DemoMap.umap` is real UE package content, not a pointer).
- `GigLineEditor` → **Result: Succeeded** (28.2 s); produced `/tmp/fresh-check/Game/GigLine/Binaries/Linux/libUnrealEditor-GigLine.so`.
- `GigLine` (game) → **Result: Succeeded** (26.9 s).
- Engine: documented external install at `/run/media/system/DATA/Engines/UE_5.8.4`
(UE 5.8.4), system clang; no machine-specific paths beyond the documented
`-project=` argument.
- Engine: documented external install at `/run/media/system/DATA/Engines/UE_5.8.4` (UE 5.8.4), system clang; no machine-specific paths beyond the documented `-project=` argument.
**Verdict: PASS.**
## `0.1.0` — Packaged Linux build + documented controls — record (2026-11-08)
**Package:** `RunUAT BuildCookRun … -platform=Linux -clientconfig=Development
-cook -build -stage -pak -archive -archivedirectory=/run/media/system/DATA/Builds/GigLineQA`
→ `EXIT=0` (Success).
**Package:** `RunUAT BuildCookRun … -platform=Linux -clientconfig=Development -cook -build -stage -pak -archive -archivedirectory=/run/media/system/DATA/Builds/GigLineQA` → `EXIT=0` (Success).
**Launch:** `./GigLine.sh -windowed` (plain, no map argument) →
`LogLoad: LoadMap: /Game/Hollywood/DemoMap?Name=Player` — the packaged game opens
the walkable demo directly (the default-map fix holds in a package, not just the
editor). No `LogLinker`/missing-asset/failed-to-find errors in the run.
**Launch:** `./GigLine.sh -windowed` (plain, no map argument) → `LogLoad: LoadMap: /Game/Hollywood/DemoMap?Name=Player` — the packaged game opens the walkable demo directly (the default-map fix holds in a package, not just the editor). No `LogLinker`/missing-asset/failed-to-find errors in the run.
**Controls documented and shown in-game:**
- `Docs/controls.md` — full binding table (WASD, mouse look, Space, R, Esc) plus
how to test input headlessly.
- `AGigLineHUD` now draws a control legend bottom-right every frame:
"WASD move · Mouse look · Space jump · R reset · Esc pause".
- Verified in the packaged capture: the bottom-right text bbox is 426×12 px
(a single right-aligned line), attributed text bottom-left. Screenshot:
`Docs/perf/0.1.0-packaged-hud-controls.png`.
- `Docs/controls.md` — full binding table (WASD, mouse look, Space, R, Esc) plus how to test input headlessly.
- `AGigLineHUD` now draws a control legend bottom-right every frame: "WASD move · Mouse look · Space jump · R reset · Esc pause".
- Verified in the packaged capture: the bottom-right text bbox is 426×12 px (a single right-aligned line), attributed text bottom-left. Screenshot: `Docs/perf/0.1.0-packaged-hud-controls.png`.
**Verdict: PASS.**
+80
View File
@@ -0,0 +1,80 @@
# `0.1.4` façade review — signoff
Four demo-map captures from the `facades-0.1.4` branch, after the module kit, the textured materials and the theatre marquee went in. **All seven items signed off 2026-11-09** (owner review of the local renders).
Reference photographs to compare against: `Art/reference/hollywood/` and `Art/reference/el-capitan-*` (local, not committed). Regenerate the set with `Tools/render_screenshots.sh --verify`; measure it with `python3 Tools/visual_loop.py --compare --iteration 5`.
## Signoff
- [x] **1. Block from the north sidewalk** — hero between its neighbours, sidewalk and road read correctly
- [x] **2. Street level** — colonnade, shopfronts and window rhythm hold up at eye height
- [x] **3. Crossing** — zebra bars, kerb line and the frontage behind
- [x] **4. North buildings** — TCL and Tussaud's framed across the boulevard
- [x] **5. Materials** — stone reads as coursed stone, glazing as glass, not flat colour
- [x] **6. Theatre trim** — marquee and blade sign proportions believable at street level
- [x] **7. Approve** `v0.1.4` (kit + materials + theatre trim); remaining geometry moves to the next version
## 1. Block from the north sidewalk
![Block from the north sidewalk](../../Screenshots/demo/review-demo-spawn-view.png)
Expected: the neoclassical hero façade between its neighbours, both sidewalks and the road in the foreground. _Metric: 0.68 similarity, edges 0.28× reference._
## 2. Street level
![Street level](../../Screenshots/demo/review-demo-street-level.png)
Expected: the hero colonnade with adjacent frontage buildings either side, shopfronts at pavement level, 4.96 m bay rhythm. _Metric: 0.33, edges 0.27×._
## 3. The designated crossing
![Crossing](../../Screenshots/demo/review-demo-crossing.png)
Expected: continental zebra bars on the asphalt at s=130 m, south frontage and colonnade behind. _Metric: 0.42, edges 0.34×._
## 4. North-side buildings
![North buildings](../../Screenshots/demo/review-demo-north-buildings.png)
Expected: TCL Chinese Theatre and Madame Tussaud's across Hollywood Blvd, full height in frame. _Metric: 0.34, edges 0.14×._
---
## Appendix — what was checked, and what was broken
### Verified
- **Behaviour**, packaged build, `-qaselftest`: walk 761 cm in 2 s (Z steady 456), reset 0 cm, 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 (`walk 246 cm`, `pause FAIL`), an immediate re-run passed — re-run before believing a single FAIL.
- **Placement**, probed from the live map against `Geo/verification/2026-11-06-el-capitan-block.md`: 6922 −24276…−17700, 6902 −17749…−14591, **hero −14644…−12028 between them**, theatre −12082…−8210, 6822 / 6806 / 6800 −7854…−1437, TCL −22889…−18442, Tussaud's −27071…−22799. Blockout affine residual **0.0 cm**.
- **Budgets**: hero 148 parts / 3 044 tris · kit 16 meshes × 12 tris · 625 kit actors ≈ 7 500 tris · 8 × 512² textures (196 KB) · 67 tool tests pass.
- **Topology**: every kit piece manifold, no loose geometry.
### Defects found this cycle
Every one of these rendered convincingly; the placement ones were caught by coordinate probes, not by looking.
| Sev | Defect | Root cause |
| --- | --- | --- |
| Blocker | Frontage shifted ≈119 m X / 2.4 m Y, hero outside its gap | Blockout regenerated without `--origin` |
| Blocker | Kit placed ~12 km from its buildings | Used mesh-local bbox; actors carry the offset |
| Blocker | Marquee on the far side of the boulevard | Blender +Y is Unreal −Y |
| Major | Marquee canopy 5 cm above the pavement | Absolute Z; pavement is 4.56 m above datum |
| Major | Window bays invisible, inside the solid wall | Piece modelled entirely at +Y |
| Major | Glazing wasn't glass | Grey `M_Kit_*` from the FBX import, never overridden |
| Major | Hero windows floated 1.44 m in front of the wall | Wrong Y in the builder |
| Major | Parapet medallions were discs on the coping | Built with the Z-axis cylinder helper |
| Major | Hero had no usable collision | Complex-only; traces use simple |
| Minor | "No smoothing group information" on every import | `mesh_smooth_type="OFF"` in six exporters |
| Minor | `save_map()` returned False silently | LFS `.umap` read-only, importer never chmod'd |
| Minor | North-buildings import silently no-op | Missing `import sys` |
| Minor | Kit folder held 24 assets (8 orphan materials) | No stale-asset sweep |
| Minor | Edge-density metric counted PIL's border ring | `FIND_EDGES` leaves a 128 ring |
| Minor | QA wall probe grazed the theatre wall | Scan row 1 cm from the building line |
### Limitations worth knowing
- Kit pieces are boxes; dentil spacing, abacus and plinth dimensions are recorded assumptions, not measurements.
- Textures are generated, not authored — no decals, signage text or weathering.
- The vision pre-check stays advisory: it has rejected shots the owner approved.
- The low edge ratios in the metrics are partly structural — a wide block view against a tight façade crop always scores low. Weigh the images, not the scores.
- Performance has not been re-measured since `0.1.0`; the 625 kit actors are unmeasured (tracked in `TODO.md` under `0.1.5`).
+13 -2
View File
@@ -83,13 +83,24 @@ Without `git lfs install`, adding an LFS-matched file does not error — it comm
Sizes: the engine lives outside the repository (pinned install path recorded in `toolchain.md`). The repository holds the Unreal project, Blender sources, geo sources, and tooling.
**Fresh checkout not yet tested end to end.** The build procedure in [build.md](build.md) was produced from a working tree, not from a clean clone. Exercise it before `v0.1.0` is tagged — it is a release check on the board.
**Fresh checkout verified 2026-11-08.** A clean clone of `main` with `git lfs pull` built both `GigLineEditor` and `GigLine` against the documented external engine (recorded in [release-checks.md](release-checks.md)). Re-run it before a release tag if the engine pin or the module's dependencies change.
### Optional keys (local only)
Two tools talk to paid services and read their keys from the gitignored `.env`:
| Variable | Used by | Needed for |
| --- | --- | --- |
| `OPENROUTER_API_KEY` | `Tools/judge_screenshot.py`, `Tools/facade_detail_concepts.py` | vision pre-check and AI concept plates |
| `MAPBOX_ACCESS_TOKEN` | `Geo/tools/mapbox_buildings.py` | the second building-height source |
Nothing in the build, the tests or the release path needs either key. Never commit `.env`; `git check-ignore .env` should succeed.
## Known blockers
| Blocker | Owner | Needed for |
| --- | --- | --- |
| Delete the fallback 5.2.0 engine archive (`/run/media/system/DATA/Downloads/Linux_Unreal_Engine_5.2.0.zip`) once the 5.8.4 pin is trusted through asset production | agent | Disk hygiene (19.6 GiB), pin safety |
| Fresh-checkout build exercised against the documented procedure | agent | `v0.1.0` release check |
| ~~Fresh-checkout build exercised against the documented procedure~~ | — | Done 2026-11-08 |
Closed 2026-11-07: Epic account and engine download, QGIS `lts` removal, Blosm and MPFB on Blender 5.2 (headless invocation fix in `Art/blender/README.md`), engine validation on Bazzite, full project bootstrap. Earlier closed: Bazzite update and reboot, driver and Vulkan verification, `git-lfs`/`clang`/`cmake`/`ninja` layering.
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.
@@ -379,7 +379,11 @@ void AGigLineCharacter::StartQASelfTest()
// trace at a fixed Z (e.g. 160) is wrong: the street is at Z ~366
// there, so a 160 cm probe starts inside the earth (dist 0).
int32 Walls = 0, WallSamples = 0;
static const float TraceYs[] = {-1400.0f, -167.0f, 1000.0f, -1382.0f, 1047.0f};
// Sample the street envelope only: the carriageway plus the walkable
// part of each sidewalk. Y 1000 used to be in this list and sits ~1 cm
// from the frontage building line, so it grazed the theatre wall once
// the kit's proud shopfronts went in - a probe artefact, not a blocker.
static const float TraceYs[] = {-1400.0f, -167.0f, 800.0f, -1382.0f, 718.0f};
for (float WallY : TraceYs)
{
for (float X0 = -19500.0f; X0 <= -3000.0f; X0 += 2500.0f)
@@ -416,6 +420,81 @@ void AGigLineCharacter::StartQASelfTest()
TEXT("GigLine QA: wall probe %d/%d traces with blocking hit inside 21 m -> %s"),
Walls, WallSamples,
Walls == 0 ? TEXT("PASS") : TEXT("FAIL"));
// Facade occlusion probe: walkable street is only half the story —
// the player must also NOT be able to walk through the buildings.
// From the road centre, fire 40 m sideways at walking height toward
// each frontage and require a blocking hit. A miss means the
// building has no collision (or only non-blocking collision) and the
// player would pass through the wall.
// Expect a wall only where the map actually stages a building.
// Spans below mirror the actors placed by Tools/ue_build_demo_map.py
// (frontage + hero) and the two north buildings; update them if
// that list changes. Gaps between buildings are legitimate misses.
struct FSpan { float Min, Max; };
static const FSpan SouthSpans[] = {
{-24276.f, -17700.f}, // 6922
{-17749.f, -14591.f}, // 6902
{-14638.f, -12034.f}, // 6840 hero
{-12082.f, -8210.f}, // 6838 El Capitan Theatre
{-7854.f, -7252.f}, // 6822 Budget
{-6608.f, -3307.f}, // 6806-6820
{-3320.f, -1437.f}, // 6800 Lee Drug
};
static const FSpan NorthSpans[] = {
{-27071.f, -22799.f}, // Madame Tussaud's
{-22889.f, -18442.f}, // TCL Chinese Theatre
};
auto InSpans = [](const FSpan* Spans, int32 Count, float X)
{
for (int32 i = 0; i < Count; ++i)
{
if (X >= Spans[i].Min && X <= Spans[i].Max) { return true; }
}
return false;
};
int32 Facades = 0, FacadeSamples = 0;
for (float X0 = -19000.0f; X0 <= -3000.0f; X0 += 2000.0f)
{
for (float Dir : {1.0f, -1.0f}) // +Y south frontage, -Y north side
{
const bool bExpectWall = (Dir > 0.0f)
? InSpans(SouthSpans, UE_ARRAY_COUNT(SouthSpans), X0)
: InSpans(NorthSpans, UE_ARRAY_COUNT(NorthSpans), X0);
if (!bExpectWall)
{
continue;
}
FHitResult GroundHit2;
FCollisionQueryParams GQ2(TEXT("GigLineFacadeGround"), false, this);
if (!W->LineTraceSingleByChannel(GroundHit2,
FVector(X0, 0.0f, 3000.f), FVector(X0, 0.0f, -1000.f),
ECC_Visibility, GQ2))
{
continue;
}
const float ProbeZ2 = GroundHit2.ImpactPoint.Z + 160.f;
FHitResult Hit2;
FCollisionQueryParams Q2(TEXT("GigLineFacadeProbe"), false, this);
const bool bHit2 = W->LineTraceSingleByChannel(Hit2,
FVector(X0, 0.0f, ProbeZ2),
FVector(X0, Dir * 4000.f, ProbeZ2),
ECC_Visibility, Q2);
++FacadeSamples;
const bool bBlocking = bHit2 && Hit2.GetActor() != nullptr;
if (!bBlocking)
{
++Facades;
UE_LOG(LogTemp, Log,
TEXT("GigLine QA: facade probe miss at X %.0f dir %.0f (no blocking hit in 40 m)"),
X0, Dir);
}
}
}
UE_LOG(LogTemp, Log,
TEXT("GigLine QA: facade probe %d/%d directions without a blocking wall -> %s"),
Facades, FacadeSamples,
Facades == 0 ? TEXT("PASS") : TEXT("FAIL"));
}
// 7.0: verdict, then stop ticking.
@@ -1,6 +1,7 @@
#include "GigLineShotActor.h"
#include "Camera/CameraActor.h"
#include "Camera/CameraComponent.h"
#include "Engine/World.h"
#include "GameFramework/PlayerController.h"
#include "UnrealClient.h"
@@ -83,6 +84,15 @@ void AGigLineShotActor::NextShot()
Params.ObjectFlags |= RF_Transient;
CurrentCamera = GetWorld()->SpawnActor<ACameraActor>(
ACameraActor::StaticClass(), ShotLocations[CurrentShot], ShotRotations[CurrentShot], Params);
if (ShotFieldOfView.IsValidIndex(CurrentShot) && ShotFieldOfView[CurrentShot] > 0.0f)
{
if (UCameraComponent* Cam = CurrentCamera->GetCameraComponent())
{
// Long lens for elevations: fills the frame from across the street
// without pitching the camera, so verticals stay parallel.
Cam->SetFieldOfView(ShotFieldOfView[CurrentShot]);
}
}
PlayerController->SetViewTarget(CurrentCamera);
SettleCountdown = SettleSeconds;
@@ -36,6 +36,16 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TArray<FString> ShotNames;
/**
* Optional per-shot field of view (degrees), same index as ShotLocations.
* Zero or missing entries keep the engine default (90 deg). Elevation shots
* taken from across the street need a long lens: a level camera 45 m out with
* the default FOV leaves the building small in frame, and tilting it up to fill
* the frame is what breaks the verticals.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TArray<float> ShotFieldOfView;
/** Seconds to let the view target settle before the capture. Resolution comes from the runner's -ResX/-ResY. */
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float SettleSeconds = 0.35f;
+1
View File
@@ -27,6 +27,7 @@ Tools that build 3D content from these sources live next to their output in `Art
| `elevation_probe.py` | Samples 3DEP for relief and records the baseline for the release checks |
| `dem_stats.py` | Stdlib reader for the tiled GeoTIFF the 3DEP service returns; GDAL does not run on this host |
| `asset_metadata.py` | Writes and audits `<asset>.geo.json` sidecars: position, provenance, conversion |
| `mapbox_buildings.py` | Cross-checks every building's height against Mapbox `mapbox-streets-v8` building data (Tilequery per centroid), merging our OSM height + source with theirs and flagging matches, close, disagreements, non-building volumes. Needs `MAPBOX_ACCESS_TOKEN` in `.env`; caches responses under `Geo/raw/mapbox-cache/`, and **both the cache and its report are gitignored** (Mapbox terms restrict redistribution) |
## Unit boundary
+57 -13
View File
@@ -4,9 +4,21 @@
Usage:
Geo/tools/block_height_report.py Geo/raw/osm-api-map-2026-11-06.osm
`height` and `building:levels` are the only height evidence OSM carries. Where
neither exists, Blosm falls back to `defaultLevels * levelHeight`, which is a
placeholder: the demo renders a constant-height block instead of a real skyline.
Height evidence, in priority order:
1. the OSM `height` tag (a direct measurement);
2. an explicit override in `HEIGHT_OVERRIDES` (currently Mapbox-derived — see below);
3. `building:levels` x `LEVEL_HEIGHT`;
4. a flagged placeholder guess.
`LEVEL_HEIGHT` is 3.1 m, not the 3.2 m first assumed: cross-checking every block
building against Mapbox's building data (`Geo/tools/mapbox_buildings.py`) showed
our `levels x 3.2 m` figure running 0.6 m high per six levels, i.e. 3.1 m per
level. That calibration was adopted on 2026-11-09.
`HEIGHT_OVERRIDES` carries the few buildings where Mapbox disagrees with a guess;
each entry records why, so the number is not orphaned from its evidence.
This tool lists every building in the demo bounds with its height source, so the
substitutions are visible and measurable rather than buried in the model.
@@ -19,7 +31,45 @@ import sys
import osm_common as oc
LEVEL_HEIGHT = 3.2
LEVEL_HEIGHT = 3.1
# Per-way height overrides, applied after the OSM height tag and before the
# levels/guess derivation. Keep `reason` short but specific: a height without its
# evidence is a guess wearing a costume.
HEIGHT_OVERRIDES = {
# Rear outbuildings tagged only `building=yes`/`apartments`; the two-level
# fallback rendered them as 6.4 m blocks. Mapbox's building layer gives 3.0 m
# for the same footprints (single storey plus parapet), adopted 2026-11-09.
1490910971: (3.0, "Mapbox building layer: 3.0 m (single storey)"),
1492058875: (3.0, "Mapbox building layer: 3.0 m (single storey)"),
1492747608: (3.0, "Mapbox building layer: 3.0 m (single storey)"),
}
def classify_height(tags, level_height=LEVEL_HEIGHT, way_id=None):
"""Return (height_m, source) for one OSM way's tags.
Extracted from `main()` so the classification is testable: the roof/shelter
rule and the "height tag beats levels beats guess" order are the parts that
actually decide what the blockout should render.
"""
# Not building volumes: elevated roof/canopy structures and transit shelters.
# Found by cross-checking against Mapbox's building data
# (Geo/tools/mapbox_buildings.py), which returns roof/canopy/shelter for the
# same footprints instead of a storey count.
building = tags.get("building")
if building in ("roof", "canopy") or tags.get("amenity") == "shelter":
kind = building if building in ("roof", "canopy") else "shelter"
return 0.0, f"not a building volume ({kind})"
if tags.get("height"):
return float(tags["height"]), "height tag"
if way_id is not None and int(way_id) in HEIGHT_OVERRIDES:
height, reason = HEIGHT_OVERRIDES[int(way_id)]
return height, f"override: {reason}"
if tags.get("building:levels"):
return (float(tags["building:levels"]) * level_height,
f"levels x {level_height} m")
return 2 * level_height, f"GUESS: default 2 levels x {level_height} m"
def main():
@@ -43,14 +93,7 @@ def main():
pts = oc.way_points(nodes, refs)
if not pts or not in_block(pts):
continue
tag_height = tags.get("height")
levels = tags.get("building:levels")
if tag_height:
height, source = float(tag_height), "height tag"
elif levels:
height, source = float(levels) * LEVEL_HEIGHT, f"levels x {LEVEL_HEIGHT} m"
else:
height, source = 2 * LEVEL_HEIGHT, f"GUESS: default 2 levels x {LEVEL_HEIGHT} m"
height, source = classify_height(tags, way_id=wid)
nearest = min(oc.project(p, line, lat0, lon0, lengths)[0] for p in pts)
side = "S" if oc.project(pts[0], line, lat0, lon0, lengths)[2] < 0 else "N"
rows.append((side, nearest, wid, tags.get("name", "") or tags.get("addr:housenumber", ""),
@@ -60,7 +103,8 @@ def main():
guessed = [r for r in rows if r[5].startswith("GUESS")]
print(f"{len(rows)} buildings in the block, {len(guessed)} with a guessed height\n")
for side, setback, wid, name, height, source in rows:
flag = " <-- guessed" if source.startswith("GUESS") else ""
flag = (" <-- guessed" if source.startswith("GUESS")
else (" <-- not a building volume" if source.startswith("not a building") else ""))
print(f"{side} way {wid:>10} setback {setback:5.1f} m height {height:5.1f} m "
f"({source:26}) {name[:34]}{flag}")
+317
View File
@@ -0,0 +1,317 @@
#!/usr/bin/env python3
"""Cross-check block building heights against Mapbox's building data.
Mapbox publishes building footprints with ``height`` / ``min_height`` /
``type`` attributes in the ``mapbox-streets-v8`` tileset. They come from
different source data than our OSM extract, so comparing the two is a cheap way
to find buildings where our height is wrong or missing — the ``0.1.2`` finding
that three "guessed" volumes were actually a roof, a roof and a bus shelter is
exactly the class of problem this tool hunts for.
Usage
-----
# token from the environment or the ignored .env file
python3 Geo/tools/mapbox_buildings.py \
--osm Geo/raw/osm-api-map-2026-11-06.osm \
--out Geo/verification/2026-11-09-mapbox-buildings.md
# show only disagreements and unverifiable buildings
python3 Geo/tools/mapbox_buildings.py --only-diffs
Every Tilequery response is cached under ``Geo/raw/mapbox-cache/`` so a re-run
costs nothing and the report is reproducible. The cache is gitignored along with
``Geo/raw/``.
Design notes
------------
* Standard library only (``urllib``), like the rest of ``Geo/tools``.
* One request per building, spaced by ``--delay`` (default 1.2 s) — Mapbox bills
per request and rate-limits bursts.
* The report is a *measurement*: it prints both figures and the delta, never a
single verdict, so a human can decide which source to trust for a given
building.
* A Tilequery hit is only accepted as "this building" when the matched polygon
contains or nearly contains the queried point (``distance`` ≤ ``--max-distance``).
Licensing / data handling
-------------------------
Mapbox terms restrict persisting and redistributing their data, so **neither the
cache nor the generated report is committed** (`.gitignore` covers
`Geo/raw/mapbox-cache/` and `Geo/verification/*mapbox*`). They are local working
material: re-run the tool rather than sharing the output. The token is read from
the environment or the ignored `.env` and is never written to the cache, the
report or the JSON.
"""
from __future__ import annotations
import argparse
import json
import math
import os
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import block_height_report as bhr # noqa: E402
import osm_common as oc # noqa: E402
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
CACHE_DIR = os.path.join(ROOT, "Geo", "raw", "mapbox-cache")
TILESET = "mapbox.mapbox-streets-v8"
LAYER = "building"
API = "https://api.mapbox.com/v4"
def load_token(explicit: str | None = None) -> str:
"""Token from --token, the environment, or the ignored .env file."""
if explicit:
return explicit.strip()
token = os.environ.get("MAPBOX_ACCESS_TOKEN")
if token:
return token.strip()
env_path = os.path.join(ROOT, ".env")
if os.path.exists(env_path):
with open(env_path, encoding="utf-8") as handle:
for line in handle:
line = line.strip()
if line.startswith("MAPBOX_ACCESS_TOKEN="):
return line.split("=", 1)[1].strip().strip('"').strip("'")
raise SystemExit(
"no Mapbox token: pass --token, set MAPBOX_ACCESS_TOKEN, or add it to .env")
def centroid_lat_lon(points: list[tuple[float, float]]) -> tuple[float, float]:
"""Centroid of a ring of (lat, lon) points, as (lat, lon)."""
lat = sum(p[0] for p in points) / len(points)
lon = sum(p[1] for p in points) / len(points)
return lat, lon
def cache_path(lat: float, lon: float, radius: int) -> str:
return os.path.join(CACHE_DIR, f"{lat:.6f}_{lon:.6f}_r{radius}.json")
def tilequery(lat: float, lon: float, token: str, radius: int,
delay: float, use_cache: bool = True) -> tuple[dict, bool]:
"""Tilequery one point. Returns (payload, from_cache)."""
path = cache_path(lat, lon, radius)
if use_cache and os.path.exists(path):
with open(path, encoding="utf-8") as handle:
return json.load(handle), True
query = urllib.parse.urlencode({
"radius": radius, "layers": LAYER, "limit": 10, "access_token": token,
})
url = f"{API}/{TILESET}/tilequery/{lon:.6f},{lat:.6f}.json?{query}"
request = urllib.request.Request(url, headers={"User-Agent": "gig-line-geo/1.0"})
payload = None
for attempt in range(4):
try:
with urllib.request.urlopen(request, timeout=30) as response:
payload = json.load(response)
break
except urllib.error.HTTPError as exc:
if exc.code == 429 or exc.code >= 500:
wait = delay * (2 ** attempt)
sys.stderr.write(f" HTTP {exc.code}; retrying in {wait:.1f}s\n")
time.sleep(wait)
continue
raise
except urllib.error.URLError as exc:
sys.stderr.write(f" network error ({exc.reason}); retrying\n")
time.sleep(delay * (2 ** attempt))
if payload is None:
raise SystemExit(f"tilequery failed for {lat:.6f},{lon:.6f}")
if token in json.dumps(payload):
raise SystemExit("refusing to cache a response that contains the token")
os.makedirs(CACHE_DIR, exist_ok=True)
with open(path, "w", encoding="utf-8") as handle:
json.dump(payload, handle)
time.sleep(delay) # only sleep on a real request, never on a cache hit
return payload, False
def nearest_building(payload: dict, max_distance: float) -> dict | None:
"""The closest building feature within `max_distance` metres, or None."""
best = None
for feature in payload.get("features", []):
props = feature.get("properties", {})
if props.get("tilequery", {}).get("layer") != LAYER:
continue
distance = props.get("tilequery", {}).get("distance")
if distance is None or distance > max_distance:
continue
if best is None or distance < best[0]:
best = (distance, props)
return None if best is None else best[1]
def compare(ours: float, ours_source: str, theirs: float | None) -> tuple[str, float | None]:
"""Classify the agreement between our height and Mapbox's.
Returns (verdict, delta_m). Verdicts are deliberately blunt so the report can
be scanned: "match", "close", "disagree", "mapbox-only", "ours-only".
"""
ours_is_volume = not ours_source.startswith("not a building")
if not ours_is_volume:
return "non-building", None
if theirs is None:
return "ours-only", None
if ours <= 0.0:
return "mapbox-only", theirs
delta = theirs - ours
if abs(delta) <= 0.5:
return "match", delta
if abs(delta) <= 2.0:
return "close", delta
return "disagree", delta
def build_rows(nodes, ways, token, radius, max_distance, delay) -> list[dict]:
rows = []
for wid, tags, refs in ways:
if "building" not in tags:
continue
points = oc.way_points(nodes, refs)
if not points:
continue
lat, lon = centroid_lat_lon(points)
payload, cached = tilequery(lat, lon, token, radius, delay)
feature = nearest_building(payload, max_distance)
ours_height, ours_source = bhr.classify_height(tags, way_id=wid)
theirs = None if feature is None else feature.get("height")
verdict, delta = compare(ours_height, ours_source, theirs)
rows.append({
"way": wid,
"name": tags.get("name") or tags.get("addr:housenumber") or "",
"kind": tags.get("building"),
"osm_height_m": round(ours_height, 2) if ours_height else 0.0,
"osm_source": ours_source,
"mapbox_height_m": None if theirs is None else round(float(theirs), 2),
"mapbox_min_height_m": None if feature is None else feature.get("min_height"),
"mapbox_type": None if feature is None else feature.get("type"),
"mapbox_distance_m": None if feature is None
else round(feature.get("tilequery", {}).get("distance", 0.0), 1),
"verdict": verdict,
"delta_m": None if delta is None else round(delta, 2),
"cached": cached,
"lat": round(lat, 6),
"lon": round(lon, 6),
})
return rows
def render_markdown(rows: list[dict], args) -> str:
order = {"disagree": 0, "mapbox-only": 1, "ours-only": 2,
"non-building": 3, "close": 4, "match": 5}
rows = sorted(rows, key=lambda r: (order.get(r["verdict"], 9),
-abs(r["delta_m"] or 0)))
counts = {}
for row in rows:
counts[row["verdict"]] = counts.get(row["verdict"], 0) + 1
lines = [
"# Block buildings — OSM vs Mapbox",
"",
"Generated by `Geo/tools/mapbox_buildings.py`; re-run it to reproduce.",
"Mapbox data: tileset `mapbox-streets-v8`, layer `building`, Tilequery",
f"at each building centroid (radius {args.radius} m, accept ≤ "
f"{args.max_distance} m). OSM heights come from the tagged extract, via the",
"same classification used for the blockout (tag > levels > guess; roof and",
"shelter are not building volumes).",
"",
f"Verdicts: {', '.join(f'{k} {v}' for k, v in sorted(counts.items()))}.",
"",
"| Verdict | Δ | Building | OSM | OSM source | Mapbox | Mapbox type | dist |",
"| --- | ---: | --- | ---: | --- | ---: | --- | ---: |",
]
for row in rows:
osm = f"{row['osm_height_m']:.1f} m"
mb = "—" if row["mapbox_height_m"] is None else f"{row['mapbox_height_m']:.1f} m"
delta = "—" if row["delta_m"] is None else f"{row['delta_m']:+.1f} m"
dist = "—" if row["mapbox_distance_m"] is None else f"{row['mapbox_distance_m']:.0f} m"
name = row["name"] or f"way {row['way']}"
lines.append(
f"| {row['verdict']} | {delta} | {name} | {osm} | {row['osm_source']} | "
f"{mb} | {row['mapbox_type'] or '—'} | {dist} |")
lines += [
"",
"## How to read this",
"",
"- **disagree** — the two sources differ by more than 2 m. One of them is",
" wrong; check the reference photography before changing the model.",
"- **mapbox-only** — Mapbox knows a height we do not (usually a building",
" with no OSM `height`/`building:levels`).",
"- **ours-only** — Mapbox returns no building within the accept distance;",
" the footprint may be missing from its tileset, or the centroid may fall",
" outside its polygon.",
"- **non-building** — our classification says roof/shelter; excluded from",
" height comparison on purpose.",
"",
"Neither source is authoritative: OSM is community-tagged, Mapbox is a",
"derived commercial dataset. Treat a disagreement as a reason to look at",
"the photographs, not as an automatic correction.",
"",
]
if args.only_diffs:
lines.insert(0, "> Filtered: only disagreements and unverifiable buildings.\n")
return "\n".join(lines)
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--osm", default="Geo/raw/osm-api-map-2026-11-06.osm")
ap.add_argument("--out", help="Markdown report path; omit to print to stdout")
ap.add_argument("--json", help="Also write the merged rows as JSON here")
ap.add_argument("--token", help="Mapbox access token (default: env/.env)")
ap.add_argument("--radius", type=int, default=25, help="Tilequery radius, metres")
ap.add_argument("--max-distance", type=float, default=8.0,
help="accept a hit only within this distance of the centroid")
ap.add_argument("--delay", type=float, default=1.2, help="seconds between requests")
ap.add_argument("--only-diffs", action="store_true",
help="report only disagreements / unverifiable buildings")
ap.add_argument("--no-cache", action="store_true", help="ignore the tilequery cache")
args = ap.parse_args()
token = load_token(args.token)
nodes, ways = oc.load(args.osm)
rows = build_rows(nodes, ways, token, args.radius, args.max_distance, args.delay)
if args.no_cache:
for row in rows:
row["cached"] = False
if args.only_diffs:
keep = {"disagree", "mapbox-only", "ours-only", "non-building"}
rows = [r for r in rows if r["verdict"] in keep]
markdown = render_markdown(rows, args)
if args.out:
os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True)
with open(args.out, "w", encoding="utf-8") as handle:
handle.write(markdown + "\n")
print(f"wrote {args.out} ({len(rows)} buildings)")
else:
print(markdown)
if args.json:
payload = {
"generated_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"osm": args.osm,
"mapbox": {"tileset": TILESET, "layer": LAYER,
"radius_m": args.radius, "max_distance_m": args.max_distance},
"buildings": rows,
}
with open(args.json, "w", encoding="utf-8") as handle:
json.dump(payload, handle, indent=2)
print(f"wrote {args.json}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+261 -97
View File
@@ -239,123 +239,287 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO.
## 2026-11-08 — reference photography gaps closed (local-only)
- Closed the two missing reference angles for the hero façade, both from Wikimedia
Commons (gitignored deliverables under `Art/reference/`, provenance in sidecars):
- `el-capitan-entrance/` — `File:Hollywood El Capitan entrance.jpg` (+ detail),
Andreas Praefcke, CC BY 3.0. This is the isolated hero (6840) entrance/door
recess that the notes doc had claimed Commons didn't hold. Query that found it:
`--query "El Capitan entrance"`.
- `el-capitan-night/` — `File:El Capitan theater with Tron marquee.jpg`
(CC BY-SA 3.0) + West Side Story premiere (Adam Jones, CC BY 2.0). The theatre is
the building NEXT to 6840; treat as proximity evidence only, not the hero.
- Commons throttles hard: HTTP 429 after a batch. Fetch fewer files with
`--delay`/separate calls and wait ~15-20s between runs (`queue.py` tool default
1 req/s is insufficient once rate limited).
- Task was claimed on branch `ref-photo-0.1.2`; delivers only the TODO closure (all
actual reference files are gitignored, so no code branch content). Reported to
coordinator for merge/cleanup.
- Closed the two missing reference angles for the hero façade, both from Wikimedia Commons (gitignored deliverables under `Art/reference/`, provenance in sidecars):
- `el-capitan-entrance/` — `File:Hollywood El Capitan entrance.jpg` (+ detail), Andreas Praefcke, CC BY 3.0. This is the isolated hero (6840) entrance/door recess that the notes doc had claimed Commons didn't hold. Query that found it: `--query "El Capitan entrance"`.
- `el-capitan-night/` — `File:El Capitan theater with Tron marquee.jpg` (CC BY-SA 3.0) + West Side Story premiere (Adam Jones, CC BY 2.0). The theatre is the building NEXT to 6840; treat as proximity evidence only, not the hero.
- Commons throttles hard: HTTP 429 after a batch. Fetch fewer files with `--delay`/separate calls and wait ~15-20s between runs (`queue.py` tool default 1 req/s is insufficient once rate limited).
- Task was claimed on branch `ref-photo-0.1.2`; delivers only the TODO closure (all actual reference files are gitignored, so no code branch content). Reported to coordinator for merge/cleanup.
## 2026-11-08 — security-game scan: concepts copied, licensing ignored (owner)
- Scanned `/run/media/system/DATA/Documents/06-games/security-game` for reference
material vs our `Art/reference/`. Result: its **photo set** (`docs/screens/refs/
hollywood/`, 9 files) is already fully duplicated in our `Art/reference/hollywood/`
(with SIDECAR/BOARD copies) — nothing new there.
- What we lacked: its **AI concept renders** (`docs/screens/concepts/
hollywood_boulevard/`, 12 PNGs + `.refgen.json`). Copied into
`Art/reference/el-capitan-concepts/` with a folder README. **Owner explicitly
said ignore licensing completely** — they are gitignored local development
reference, never committed or shipped; `.refgen.json` still records the model
and source photo per file for traceability.
- Use concepts for mood/lighting intent only; geometry truth stays with the photo
refs (facade, entrance, night marquee, TCL/Tussauds, Masonic 1922, etc.).
- The security-game `realism/` A/B screenshots and `concepts/joke_shop/` are its
own engine/venue renders — not relevant to our 6840 modeling.
- Scanned `/run/media/system/DATA/Documents/06-games/security-game` for reference material vs our `Art/reference/`. Result: its **photo set** (`docs/screens/refs/ hollywood/`, 9 files) is already fully duplicated in our `Art/reference/hollywood/` (with SIDECAR/BOARD copies) — nothing new there.
- What we lacked: its **AI concept renders** (`docs/screens/concepts/ hollywood_boulevard/`, 12 PNGs + `.refgen.json`). Copied into `Art/reference/el-capitan-concepts/` with a folder README. **Owner explicitly said ignore licensing completely** — they are gitignored local development reference, never committed or shipped; `.refgen.json` still records the model and source photo per file for traceability.
- Use concepts for mood/lighting intent only; geometry truth stays with the photo refs (facade, entrance, night marquee, TCL/Tussauds, Masonic 1922, etc.).
- The security-game `realism/` A/B screenshots and `concepts/joke_shop/` are its own engine/venue renders — not relevant to our 6840 modeling.
## 2026-11-08 — ODbL review closed; remaining active TODO items finished
- Wrote `Docs/odbl-review.md` (decision 2026-11-08): the shipped block geometry
(streets/footprints/sidewalks) is a Produced Work of the OpenStreetMap database
(ODbL 1.0). Obligations held: on-screen attribution via AGigLineHUD ("©
OpenStreetMap contributors" + "Data © OSM, ODbL 1.0 — openstreetmap.org/copyright"),
ODbL link, and the raw extract committed in Geo/ with a manifest (date, bbox,
SHA-256). Share-alike is limited to the data layer; project code/art remain CC0
(Docs/licensing.md decision). 3DEP DEM is US Government public domain, unrelated
to ODbL.
- Closed both [A] items in TODO: (1) sidewalks/roads/collision — collision
verified; remaining kerb radii/aprons/kerb-height explicitly tracked as 0.1.1
patch tasks (Sidewalk outer edge, Crossing ramp gradient, Kerb height);
(2) ODbL review — recorded. The [D] Blosm-import (blockout cleaning) item stays
deferred (tracked separately).
- Wrote `Docs/odbl-review.md` (decision 2026-11-08): the shipped block geometry (streets/footprints/sidewalks) is a Produced Work of the OpenStreetMap database (ODbL 1.0). Obligations held: on-screen attribution via AGigLineHUD ("© OpenStreetMap contributors" + "Data © OSM, ODbL 1.0 — openstreetmap.org/copyright"), ODbL link, and the raw extract committed in Geo/ with a manifest (date, bbox, SHA-256). Share-alike is limited to the data layer; project code/art remain CC0 (Docs/licensing.md decision). 3DEP DEM is US Government public domain, unrelated to ODbL.
- Closed both [A] items in TODO: (1) sidewalks/roads/collision — collision verified; remaining kerb radii/aprons/kerb-height explicitly tracked as 0.1.1 patch tasks (Sidewalk outer edge, Crossing ramp gradient, Kerb height); (2) ODbL review — recorded. The [D] Blosm-import (blockout cleaning) item stays deferred (tracked separately).
- Branch: odbl-review-0.1.0 (docs + TODO closure only).
## 2026-11-08 — placement release check; north-buildings camera was 9.5 km off
- Release check 2 (placement vs references): rendered demo, probed DemoMap world
positions, compared to Geo references. Matches: road 24.3 m (ref crossings
20.9-25.9), sidewalk centreline offsets 12.5/13.2 m (ref 12.6/13.6), hero
frontage 26.0 m / setback ~13 m (ref 25.75/14.5), six south-frontage buildings
+ TCL/Tussaud's in verified OSM positions.
- **Bug found:** the `demo-north-buildings` camera sat at X -22800 (the buildings'
own X), aiming at its own position -> 85%-sky screenshot. Probed real geometry:
TCL bbox X -22889..-18442, Y -9431..-1802; Tussaud's X -27071..-22799,
Y -7586..-1888; hero at X -13336, Y +1307 (frontage 26 m). Camera now at
(X -22750, Y +1600) looking north; sky fraction 85% -> 41%.
- Getting actor world bounds: spawn at loc(0,0,0) means static mesh bbox IS the
world footprint (FBX carries the geo offset). Probe by loading the level
explicitly (load_level) — a fresh commandlet starts on Untitled otherwise.
- Release check 2 (placement vs references): rendered demo, probed DemoMap world positions, compared to Geo references. Matches: road 24.3 m (ref crossings 20.9-25.9), sidewalk centreline offsets 12.5/13.2 m (ref 12.6/13.6), hero frontage 26.0 m / setback ~13 m (ref 25.75/14.5), six south-frontage buildings
- TCL/Tussaud's in verified OSM positions.
- **Bug found:** the `demo-north-buildings` camera sat at X -22800 (the buildings' own X), aiming at its own position -> 85%-sky screenshot. Probed real geometry: TCL bbox X -22889..-18442, Y -9431..-1802; Tussaud's X -27071..-22799, Y -7586..-1888; hero at X -13336, Y +1307 (frontage 26 m). Camera now at (X -22750, Y +1600) looking north; sky fraction 85% -> 41%.
- Getting actor world bounds: spawn at loc(0,0,0) means static mesh bbox IS the world footprint (FBX carries the geo offset). Probe by loading the level explicitly (load_level) — a fresh commandlet starts on Untitled otherwise.
## 2026-11-08 — collision/seam audit done; QA self-test extended
- `-qaselftest` transect widened from 84 samples (3 lane centres) to **1456
samples / 16 rows** including kerb/road/sidewalk seam Ys (probed: N side
-1912..-1053, kerb -1382..-1053, road -1382..+1047, kerb +718..+1047, S side
+718..+1447). 0 holes.
- **New wall probe:** ground-referenced horizontal 30 m trace at +160 cm. First
attempt used a fixed Z 160 and started INSIDE the earth (street is at Z ~366)
-> every row reported a dist-0 blocker. Always ground the probe first.
- **Guard:** GigLineShotActor must skip rounds under `-qaselftest`, else
bQuitWhenDone closes the game before the transect phase (observed: game exited
at t=2s with "ShotActor: done (4 shots)" + RequestExit).
- Results on packaged build: walk 761cm/2s, reset 0cm, pause/unpause, transect
0 holes, wall 0/35 -> all PASS. Evidence recorded in Docs/release-checks.md.
- `-qaselftest` transect widened from 84 samples (3 lane centres) to **1456 samples / 16 rows** including kerb/road/sidewalk seam Ys (probed: N side -1912..-1053, kerb -1382..-1053, road -1382..+1047, kerb +718..+1047, S side +718..+1447). 0 holes.
- **New wall probe:** ground-referenced horizontal 30 m trace at +160 cm. First attempt used a fixed Z 160 and started INSIDE the earth (street is at Z ~366) -> every row reported a dist-0 blocker. Always ground the probe first.
- **Guard:** GigLineShotActor must skip rounds under `-qaselftest`, else bQuitWhenDone closes the game before the transect phase (observed: game exited at t=2s with "ShotActor: done (4 shots)" + RequestExit).
- Results on packaged build: walk 761cm/2s, reset 0cm, pause/unpause, transect 0 holes, wall 0/35 -> all PASS. Evidence recorded in Docs/release-checks.md.
## 2026-11-08 — setup logs gated behind a cvar (diagnostics item)
- The remaining spawn/input setup logs (settle-timer "start captured", "GigLine
IN:" pushed/bound/errors) are now gated behind `gigline.SetupLogs`
(TAutoConsoleVariable, default **0** via `DefaultEngine.ini [ConsoleVariables]`).
Re-enable at runtime: `gigline.SetupLogs 1`.
- The `-qaselftest` QA logs stay ungated (deliberate release-check feature per
Docs/release-checks.md). Verified packaged build: 0 setup-log lines, QA suite
still all PASS (walk/reset/pause/transect 0 holes/wall 0-35).
- Branch qa-diagnostics-0.1.0. SIM/GROUND probes were removed earlier in the
DefaultKeyMappings fix (PR #8).
- The remaining spawn/input setup logs (settle-timer "start captured", "GigLine IN:" pushed/bound/errors) are now gated behind `gigline.SetupLogs` (TAutoConsoleVariable, default **0** via `DefaultEngine.ini [ConsoleVariables]`). Re-enable at runtime: `gigline.SetupLogs 1`.
- The `-qaselftest` QA logs stay ungated (deliberate release-check feature per Docs/release-checks.md). Verified packaged build: 0 setup-log lines, QA suite still all PASS (walk/reset/pause/transect 0 holes/wall 0-35).
- Branch qa-diagnostics-0.1.0. SIM/GROUND probes were removed earlier in the DefaultKeyMappings fix (PR #8).
## 2026-11-08 — v0.1.0 released
- All six `0.1.0` release checks pass and are recorded in `Docs/release-checks.md`:
traversal (seam transect 0 holes), placement vs references, collision+packaging
(wall probe 0/35), 60 fps at 1440p (2.42 ms avg; TSR on, VSync off, no FG),
fresh-checkout build, packaged Linux build with controls shown in-game.
- `CHANGELOG.md` gets the `0.1.0` entry; the completed `0.1.0` section was removed
from `TODO.md` (goal stays in `ROADMAP.md`); tag `v0.1.0` cut on `main`.
- All six `0.1.0` release checks pass and are recorded in `Docs/release-checks.md`: traversal (seam transect 0 holes), placement vs references, collision+packaging (wall probe 0/35), 60 fps at 1440p (2.42 ms avg; TSR on, VSync off, no FG), fresh-checkout build, packaged Linux build with controls shown in-game.
- `CHANGELOG.md` gets the `0.1.0` entry; the completed `0.1.0` section was removed from `TODO.md` (goal stays in `ROADMAP.md`); tag `v0.1.0` cut on `main`.
- Owner approved the regenerated screenshot set (PR #15) on 2026-11-08.
- THIRD_PARTY_NOTICES switched to checkbox statuses (`[x]`/`[P]`/`[ ]`); all tools
incl. Blosm, QGIS and Git LFS are now verified as tool-only.
- Next work: the `0.1.1` collision-detail patch (sidewalk outer edge 15.8 cm step,
crossing ramp gradient 10 %, kerb height assumption, traversal regression).
- THIRD_PARTY_NOTICES switched to checkbox statuses (`[x]`/`[P]`/`[ ]`); all tools incl. Blosm, QGIS and Git LFS are now verified as tool-only.
- Next work: the `0.1.1` collision-detail patch (sidewalk outer edge 15.8 cm step, crossing ramp gradient 10 %, kerb height assumption, traversal regression).
## 2026-11-08 — v0.1.1 released (collision patch)
- Sidewalk upstand now tapers full-at-kerb → zero at the building edge (removes the
15.8 cm outer ledge). Crossing ramp run derives from `--max-ramp-grade` (8 % ⇒
1.63 m) instead of a fixed 1.5 m/8.7 %. Street + site rebuilt, site re-imported,
DemoMap rebuilt, packaged build verified.
- Pre-check tool fixes: coverage no longer shrinks with `--sample` (it used
`range(int(block_len))`), and it reports grade + location of the worst step.
- Residual, documented not hidden: 13.4 cm surface step at s≈198 (south) / 8.0 cm
at s≈251 (north) — a strip discontinuity near a centreline vertex, below the
45 cm MaxStepHeight, so not blocking.
- Kerb height finalised at 0.15 m modelled / 0.13 m built (6 in LA standard; DEM
and photos can't resolve finer).
- Sidewalk upstand now tapers full-at-kerb → zero at the building edge (removes the 15.8 cm outer ledge). Crossing ramp run derives from `--max-ramp-grade` (8 % ⇒ 1.63 m) instead of a fixed 1.5 m/8.7 %. Street + site rebuilt, site re-imported, DemoMap rebuilt, packaged build verified.
- Pre-check tool fixes: coverage no longer shrinks with `--sample` (it used `range(int(block_len))`), and it reports grade + location of the worst step.
- Residual, documented not hidden: 13.4 cm surface step at s≈198 (south) / 8.0 cm at s≈251 (north) — a strip discontinuity near a centreline vertex, below the 45 cm MaxStepHeight, so not blocking.
- Kerb height finalised at 0.15 m modelled / 0.13 m built (6 in LA standard; DEM and photos can't resolve finer).
## 2026-11-09 — Mapbox building data as a second height source
- `Geo/tools/mapbox_buildings.py` (new, stdlib `urllib`) Tilequeries `mapbox.mapbox-streets-v8` layer `building` at every building centroid in the extract and merges the answer with our OSM height + source. Token comes from `MAPBOX_ACCESS_TOKEN` in the ignored `.env`; responses cache to `Geo/raw/mapbox-cache/`.
- **Mapbox terms restrict persisting/redistributing their data**, so `.gitignore` now covers `/Geo/raw/mapbox-cache/` and `/Geo/verification/*mapbox*`. The tool also refuses to cache a response that contains the token.
- **First run (72 buildings): 58 match, 5 close, 3 disagree, 6 non-building.**
- It independently classifies our roof/canopy/shelter volumes as `roof`/ `shelter` — corroborating the `0.1.2` finding that they are not buildings.
- Remaining disagreements are untyped `building=yes`/`apartments` rear structures where our fallback guess (2 × 3.2 m = 6.4 m) meets Mapbox's 3.0 m.
- Every "close" case is a `building:levels × 3.2 m` building where Mapbox is **0.6 m lower per 6 levels** → Mapbox implies **3.1 m per level**, not 3.2. Worth revisiting `LEVEL_HEIGHT` if a measured storey height ever lands.
- Its query API surface: `height`, `min_height`, `type`, `extrude` per polygon.
## 2026-11-09 — Mapbox calibration adopted into the blockout
- Owner decision: adopt Mapbox. Applied:
- `LEVEL_HEIGHT` 3.2 → **3.1 m** in `Geo/tools/block_height_report.py`, and the Blosm `--level-height` default in `Art/blender/blockout/blosm_blockout.py`.
- `HEIGHT_OVERRIDES` in block_height_report: ways 1490910971, 1492058875, 1492747608 → 3.0 m (Mapbox single-storey; the 2-level guess gave 6.4 m).
- `classify_height(tags, level_height, way_id)` now takes the way id so overrides apply; `mapbox_buildings.py` passes it, so the cross-check agrees with the height table.
- Re-running the cross-check: **66 match, 6 non-building, 0 close/disagree**.
- Geometry: blockout regenerated at 3.1 (object names preserved — the frontage exporter maps `element.020`/`element`/`element.019`/named buildings, all still present, 6/6 exported), frontage re-imported, DemoMap rebuilt, packaged build `-qaselftest` all PASS.
- **Blosm adds roof mass** on top of `levels × levelHeight`: 6922 39.1 m vs table 37.2, El Capitan Theatre 20.5 m vs 18.6 — both ~+1.9 m. That is a roof, not a height error; the table is the tagged height, the mesh is storey + roof.
- Gotcha: regenerating the blockout is safe for names, but any future change to the import must re-verify the 6/6 frontage export and the `element.*` mapping.
## 2026-11-09 — hero colonnade proportion fixed (47.6 % → 53.1 %)
- The hero's colonnade (columns 2.00–7.95 + capitals 7.95–8.90) was 6.90 m of the 14.50 m elevation = **47.6 %**, below the photographed 50–60 %, because the entablature (3.40 m) and parapet (2.20 m) used 5.60 m of the height.
- Thinned ENTABLATURE_H 3.40 → 3.00 and PARAPET_H 2.20 → 1.80 in `Tools/hero_facade_build.py`; the colonnade grows to **7.70 m = 53.1 %** while the envelope, motto band and medallions keep their relationships (all positioned relative to those two constants).
- **Gotcha (cost a bad build):** `hero_facade_build.py` takes `--out` for the **FBX** and `--save-blend` for the `.blend`. Passing `--out <file>.blend` writes FBX bytes over the blend and Blender then reports "File format is not supported". Correct invocation: `--out /tmp/gigline/hero-facade.fbx --save-blend Art/...blend --report Art/...json`.
- Rebuilt hero → re-imported (`SM_HeroFacade`, bounds 2604 × 1257 × 1450 cm) → DemoMap rebuilt → screenshots re-rendered. Note `ue_hero_facade_import.py` saves the map, so `chmod u+w` the `.umap` first or the import silently does not persist.
## 2026-11-09 — AI detail plates from reference photos; floating windows fixed
- `Tools/facade_detail_concepts.py` generates architectural detail plates from one local reference photograph via OpenRouter image models (default `google/gemini-2.5-flash-image`; the sibling project uses the same route in `tools/generate_textures.py` — chat/completions with a text + image_url content part, image returned as `message.images[].image_url.url`). Eight studies, `--list` free, `--all` generates, `-vN` never overwrites, `.refgen.json` sidecar per image, all output under gitignored `Art/reference/facade-details/`.
- **Prompt gotcha:** the model refuses when the prompt describes features the reference photograph does not show ("the image you sent appears to be a decorative architectural detail, not an entrance with steps"). Prompts must describe the _reference subject_ and only restage light/people/surface.
- **Output is real:** 1184×864 to 1344×768 PNGs, ~2 MB each.
- **Measured defect found and fixed:** hero window panes sat at Y −1.98 while the mass front is Y −0.50 and the columns reach −1.02 — i.e. glazing floating 1.44 m in front of the wall and ahead of the colonnade. Now `WINDOW_Y = −0.56` (6 cm proud of the wall, behind the columns). Visible as dark openings in `review-hero-facade-front`.
- Reminder: `.blend` is LFS read-only on checkout — `chmod u+w` or Blender fails with "Cannot save blend file … not writable" (this bit again here).
## 2026-11-09 — UE export hygiene: smoothing groups + facade collision
- **All our FBX exporters used `mesh_smooth_type="OFF"`**, which is why every Unreal import logged "No smoothing group information was found for this mesh". The hero now exports `"FACE"` and its round parts are shaded with `shade_smooth_by_angle(35°)` before export: cylinders read as round instead of faceted, and the import warning is gone (6 → 0).
- **The hero façade had no simple collision** (complex-only), so line traces and capsule sweeps passed straight through it — the player could walk through the wall of the demo. Caught by a new `-qaselftest` **facade occlusion probe** (40 m sideways traces from the road centre at walking height).
- **Auto-generated convex collision on a 95-part 26 m façade is unusable**: it produced coarse hulls that bulged into the street and the wall probe went 0/35 → 2/35. Rejected in favour of `UseComplexAsSimple` via `UGigLineAssetTools.set_complex_collision_as_simple` (same reason the terrain uses it), saved on the asset.
- Probe expectations must mirror the staged buildings: the facade probe now carries explicit X spans per frontage (and the TCL/Tussaud's spans on the north side) so a gap between buildings is not reported as a defect.
## 2026-11-09 — one FBX export preset instead of six copies
- The `bpy.ops.export_scene.fbx(...)` argument block was duplicated in six scripts (hero, block frontage, north buildings, site, scale test, MPFB character), and all six carried `mesh_smooth_type="OFF"` — hence the "No smoothing group information was found" warning on every import.
- Now `Tools/fbx_export_preset.py` holds `ue_metric_v1` and `ue_character_v1`: `preset_kwargs()` (bpy-free, unit-tested) plus `export_static()` / `export_character()` wrappers that import `bpy` lazily. Every caller imports it and passes `selection=True/False`; the seven duplicated blocks are gone.
- Callers in `Tools/` resolve the module with `sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))`; the Blender-side `Art/blender/site/export_site_fbx.py` computes the repo root (`..`, `..`, `..`) and joins `Tools`. Getting that wrong silently inserts `Tools/Tools` — check the import path when adding a caller.
- Verified end to end: scale test, hero, frontage (6/6), site (7/7) and north buildings all re-exported through the preset, re-imported with **0 smoothing warnings**, DemoMap rebuilt and `-qaselftest` all PASS (walk, transect 0 holes, wall probe 0/35, facade probe 0/9).
- `mpfb_character_export.py` was not re-run: it needs the MPFB add-on on `BLENDER_USER_SCRIPTS` ("Add-on not loaded: bl_ext.blender_org.mpfb"), which is pre-existing and unrelated; the character is not in `0.1.x` gameplay.
## 2026-11-09 — v0.1.2 released
- Cornice finished: projecting corona over a **52-dentil course** (0.18 × 0.30 × 0.22 m at 0.45 m centres, z 12.12–12.34, 0.25 m clear of the corona). Hero is now 3 044 tris across 148 parts, still 14.50 m. Verified: packaged `-qaselftest` all PASS (transect 0 holes, wall probe 0/35, facade probe 0/9).
- `CHANGELOG.md` gets the `0.1.2` entry (added/changed/fixed/verified/limitations), the `0.1.2` section is removed from `TODO.md`, `ROADMAP.md` status updated, tag `v0.1.2` cut after the merge.
- Assumptions recorded rather than hidden: dentil spacing, base and abacus dimensions are not resolvable in our reference photography.
## 2026-11-09 — elevation cameras levelled (straight verticals)
- archviz MUST: an elevation must not lean. `hero-facade-front` looked up ~4° from 4.2 m at a 7.6 m target; `demo-spawn-view` and `demo-street-level` were pitched too. All three now put the camera at the target's height, so `FindLookAtRotation` yields pitch 0 and roll 0 and verticals are parallel by construction.
- Do not try to _measure_ verticality from the rendered PNG with a naive edge scan: banners, windows and the parapet produce different edge sets at different heights and the pairwise match is meaningless (it reported a 418 px "drift"). Verify it geometrically in the camera list instead.
## 2026-11-09 — 0.1.3 packaging/setup fixes
- **`Tools/ue_common.py`**: `make_writable()` clears the read-only bit an LFS `.umap` checks out with (otherwise `save_map()` returns False with no error and the staged actors vanish), and `require_saved()` makes that failure visible. `ue_hero_facade_import.py` and `ue_import_north_buildings.py` never did this — which is why importing the hero appeared to succeed while saving nothing. Verified by forcing `chmod u-w` on DemoMap first: the run logged `map writable: chmod u+w … (was 0o444)`, then `save_map: True`, exit 0.
- Gotcha while adding it: `ue_hero_facade_import.py` had **no** `import os`/`sys` (only `unreal`), so the sys.path line raised `NameError: name 'sys' is not defined` inside the commandlet — an exit code of 255 with no obvious cause.
- Docs: `build.md` gained the current pipeline table (7 site FBXs, frontage, north, hero, map), the `Builds/GigLineQA` archive, the packaged-log path, `-qaselftest` usage and a **traps** table; `setup.md` marks the fresh-checkout build verified and documents the two optional API keys; architecture 07 + combined doc corrected the build-output path.
## 2026-11-09 — 0.1.4 materials + architecture-camera pass
- **Material graphs**: `ensure_material()` now supports masonry coursing (`courses_m`) and roughness variation. **UE's `WorldPosition` is in CENTIMETRES**, so a course height in metres must be ×100 — the first attempt divided by 0.55 and produced coursing 100× too fine to see.
- **`connect_material_expressions` returns False instead of raising**, so an unknown pin name leaves an input unconnected and the material silently compiles with a default (a mask of 0 is invisible). All links now go through a `link()` helper that logs failures. There is also **no `MaterialExpressionLess`**; use `Frac → Divide → OneMinus → Clamp` for a soft threshold band, or `MaterialExpressionStep`/`SmoothStep` if a hard one is wanted.
- **Material assignment was slot-0-only**, which on the hero is the _banner_ — the stone, shadowed stone and glazing kept their flat Blender materials. Assignment is now per slot, keyed on the imported slot material's name (`glass`/`banner`/`stoneshadow`/`stone`/`roof`/`wall`). Hero: 4/4 slots.
- New materials: `M_FacadeStoneShadow` (same coursing, a stop darker), `M_FacadeGlass` (roughness 0.14, specular 0.62 ≈ IOR 1.5 expressed without a refraction pass), `M_FacadeBanner` (the one saturated surface).
- **Camera FOV**: levelling the elevation cameras left the building small in frame (39 % sky). Added optional per-shot `ShotFieldOfView` to `AGigLineShotActor` and a 4th element on the shot tuples; a long lens (42–55°) fills the frame from across the street while the camera stays level, so verticals stay parallel. `review-hero-facade-front` sky 39 → 20 %, `demo-spawn-view` 23 → 16 %.
## 2026-11-09 — facade texture maps (0.1.4)
- `Tools/generate_facade_textures.py` writes deterministic 512² base-colour and roughness maps (ashlar stone, stucco, asphalt, concrete) with a pure-zlib PNG encoder and hash-based value noise — no numpy/Pillow, ~14 s for all eight, and byte-identical on a re-run (checked by md5sum).
- `ue_build_demo_map.py` imports them to `/Game/Hollywood/Textures` and samples them **world-aligned**: WorldPosition (cm) → ×(1/tile_cm) → ComponentMask(R,G) → TextureSample UVs. `tile_cm` per material: stone 440, stucco 260, asphalt 900, concrete 250.
- Gotchas: `MaterialExpressionTextureSample`'s UV input pin is **"UVs"**, and the output pin for colour is **"RGB"** / roughness **"R"**. `ComponentMask` pins are R/G/B/A booleans. A textured material skips the graph coursing on purpose — overlaying both would double the course pattern.
- The script needs `ROOT` in `ue_build_demo_map.py` and `sys` imported; forgetting either fails with `NameError` and an exit code of 0 from the commandlet (always read the result file under /tmp/gigline).
## 2026-11-09 — façade module kit (0.1.4)
- `Tools/facade_kit_build.py` builds the kit at the measured **4.96 m bay** and **3.10 m storey**: pilaster, sill, lintel, window bay, shopfront bay, door bay, cornice, parapet + coping = 16 meshes, exported per-object with `fbx_export_preset` plus a JSON part report (dims + evidence per piece).
- `Tools/ue_import_facade_kit.py` → `/Game/Hollywood/Kit/*`, complex-as-simple collision (a convex hull would fence off the shopfront glazing).
- `Tools/ue_apply_facade_kit.py` lays the kit out from each building's own bbox: `bays = round(width / 496 cm)`, storeys from height minus the 310 cm ground floor. **638 pieces** over six frontage buildings + both north buildings.
- **Bug worth remembering:** the first window piece was modelled entirely at +Y (into the wall), so every window sat _inside_ the solid blockout wall and was invisible in every render while the placement maths looked right. The kit now projects the surround 3 cm proud with the glazing 6 cm behind its face — a solid-wall blockout cannot carry a real recess, so the piece has to project.
- Blender gotcha: clearing the scene _including materials_ invalidates handles already taken ("StructRNA of type Material has been removed"); clear objects and orphan meshes only, and create the material after the clear.
- The kit meshes are spawned at the world origin with their position baked into the mesh (like the frontage exports), so placement uses the bbox directly and must not add the actor location.
## 2026-11-09 — CRITICAL: blockout must be regenerated with --origin
- Regenerating the blockout in 0.1.2 (for the 3.1 m storey height) was run **without `--origin`**, so Blosm centred on the extract instead of fitting the project frame. The whole frontage shifted about **119 m in X and 2.4 m in Y**, and the hero ended up outside the 6902/6838 gap — the "verified placement" was undone silently, and every render still looked like a plausible block.
- Correct invocation (README documents it, and the affine fit prints `frame correction residual: max 0.0 cm`): `blosm_blockout.py --osm … --bounds 34.1006,-118.3422,34.1020,-118.3370 --level-height 3.1 --origin 34.101545,-118.338718 --assets-dir … --out …`
- After re-export + re-import, the frontage spans are back to the verified values: 6922 −24276..−17700, 6902 −17749..−14591, **hero −14644..−12028 between them**, theatre −12082..−8210, and the north buildings at −22889..−18442 / −27071..−22799.
- **`ue_import_north_buildings.py` also lacked `import sys`**, so its sys.path line raised NameError and the import silently did nothing (exit 255). Same trap as the hero importer: these scripts imported only `unreal`.
- Lesson: after any blockout regeneration, probe the staged buildings' world X spans and compare them with `Geo/verification/2026-11-06-el-capitan-block.md` before trusting a render. A shift of this size is invisible in a screenshot.
## 2026-11-09 — kit naming + topology validation (blender-modeler checklist)
- Kit assets now follow the Unreal convention: the Blender object keeps its readable name (`Kit_Pilaster`), the exported asset and file are `SM_Kit_Pilaster`. The kit loader strips the `SM_` prefix so the layout code still addresses pieces by short name.
- `facade_kit_build.py::validate()` runs the blender-modeler cleanup checklist on every piece before handoff — non-manifold edges, loose vertices, triangle count — and records the result in the part report. Current kit: 16 pieces, **all manifold, no loose geometry, 12 tris each**.
- `ue_import_facade_kit.py` now deletes kit assets that the current build no longer produces, so a rename cannot leave two generations of the same piece in the folder (the rename from `Kit_*` to `SM_Kit_*` did exactly that until it did).
## 2026-11-09 — visual comparison loop; screenshots are demo-only
- `Tools/visual_loop.py` closes the reference → render → feedback loop. `--verify` gates captures against `Tools/visual_targets.json` (blank, tone-less, wrong-size fail); `--compare` measures luma, RMS contrast, edge density, palette distance and a 3x3 luma grid against the reference and prints a 0–1 score; `--iteration N` writes `Art/reference/iterations/N/{metrics.json,feedback.md}`. The feedback names the skill to consult, so the next modelling pass starts from a measurement rather than an opinion.
- **Iterations are gitignored** (`/Art/reference/iterations/`); only confirmed shots are promoted into `Screenshots/`. A test asserts the ignore rule and that every target is a demo-map shot.
- `Screenshots/` now holds **demo-map captures only** — the review staging map is no longer rendered for review. `render_screenshots.sh --verify` renders the demo map, collects the shots and runs the verification gate in one step.
- **PIL gotchas:** `FIND_EDGES` leaves its own border ring at 128 (236/3600 px on a 60x60 frame = 6.5% "detail" on a perfectly flat image), so crop **after** filtering — cropping before does not help, the new border gets the same treatment. Palette shares must divide by the full-image area, not the cropped one.
- First comparison (iteration 2): spawn view 0.70, street level 0.35, crossing 0.41, north buildings 0.34. Edge density is the dominant gap everywhere (0.04–0.11 against 0.20–0.32 in the references) — the render still lacks the relief the photographs carry, which is exactly what the `0.1.4` façade work is for.
## 2026-11-09 — theatre marquee + blade sign
- `Tools/theatre_trim_build.py` builds the El Capitan Theatre's signature features in **world coordinates**: marquee canopy 12.0 × 2.60 m at a 4.60 m soffit, illuminated fascia, recessed soffit, vertical blade sign 1.30 × 5.20 m with frame and bracket. `Tools/ue_stage_theatre_trim.py` imports to `/Game/Hollywood/Theatre` and stages one actor per piece (single spawn at the origin — no placement arithmetic).
- **Two placement bugs, both invisible in a render:**
1. **Axis sign.** Blender +Y is Unreal −Y, so a face at Unreal Y +1001 is Blender Y −10.01. Using +10.01 put the whole marquee on the far side of the boulevard.
2. **Absolute Z.** Trim heights were absolute while the pavement at the theatre is 4.56 m above the project datum, so the canopy hung 5 cm above the pavement — the QA wall probe caught it as a knee-height blocker. `GROUND_Z` now offsets every height.
- Wall probe: a scan row at Y 1000 sat ~1 cm from the frontage building line and grazed the theatre wall once the kit's proud shopfronts went in. Rows now sample the street envelope only (−1400, −167, 800, −1382, 718).
## 2026-11-09 — 0.1.4 signed off; 0.1.5 created for the carried geometry
- Owner reviewed the four demo captures locally and approved the set. Signoff recorded in `Docs/reviews/0.1.4-review.md` (7/7 items) and in the per-shot `Approved` column of `Screenshots/README.md`.
- The three geometry items that `0.1.4` did not finish — retail neighbours from their own references, hero fluting/motto band, blockout cleaning — move to a new **`0.1.5` Façade finishing** patch rather than growing this one, plus a much-needed **1440p performance re-measure** (625 kit actors have never been measured; `0.1.0` measured 414 fps).
- Decision recorded because it sets a precedent: finish and sign off a version when its _stated_ scope is done, and carry leftover scope into the next patch — do not let a patch accumulate unmeasured geometry.
## 2026-11-09 — quality priority flipped: realism first, performance last
- Owner decision: realism leads; performance closes the quality list. Applied in
`Docs/Architecture/01_introduction_and_goals.md`, `10_quality_requirements.md`,
`04_solution_strategy.md`, the combined `architecture.md`, `Docs/release-checks.md`
(performance section now last, with the reason), `ROADMAP.md` (a "Quality priority"
section) and `AGENTS.md` (the tie-break rule next to the scope-cutting rule).
- Ordering used: 1 visual realism, 2 geographic fidelity, 3 walkability/usability,
4 build reproducibility/portability, 5 provenance, 6 performance.
- **Both revisions carry the same guard so the order cannot be used as an excuse:**
performance is a *floor*, not a preference — 60 FPS at 1440p without frame
generation must be met at every release, and if realism cannot fit, the realism
scope is cut deliberately and recorded in `CHANGELOG.md`, never the frame rate.
Walkability outranks realism in one direction: a block the player falls through is
not a demo.
## 2026-11-09 — 0.1.5 start: retail reference + kit layout maths
- Commons holds exactly **one** usable reference for the retail run:
`Art/reference/retail-neighbours/01-lee-drug-and-neighbor.jpg` (Gb321, CC BY-SA
4.0, 1920×953) — 6800 Lee Drug **and** its neighbour at street level, shopfronts
and parapets visible (vision pre-check: yes). Nothing for 6822 or 6806–6820, so
those two façades still have no reference and stay blocked.
- **Kit layout was wrong for tall/wide buildings**, not just ugly: bays were
`round(width / 496)`, so 6922 (65.8 m wide, 37.1 m tall) got **13 bays** — a
window every 4.96 m across the whole frontage — and the storey count came out at
11 × 310 cm = 34.1 m against a 35.7 m window zone, leaving bare wall under the
cornice. Fixed: `MAX_BAYS = 9` (module widens instead of subdividing), and the
storey spread is derived from the available height (`usable / storeys`) so the top
band always lands inside the building. Result: 6922 9 × 11, theatre 8 × 6,
6806 7 × 3, 6800 4 × 3, TCL 9 × 5.
## 2026-11-09 — hero fluting + motto band; and a silent placement divergence
- Fluting: 20 grooves per column as thin shadow-tone cylinders set into the shaft
(120 pieces) rather than boolean cuts — the eye reads them in silhouette and
grazing light, booleans would cost far more for that. Motto band is now a real
recess: surround 6 cm proud, panel 12 cm behind (was an 8 cm dark stripe).
Hero 148 parts/3 044 tris -> 271/5 492, still 14.50 m.
- **The real find:** `ue_hero_facade_import.py` had its own hero position
`(-12024.5, -1450, 0)` — the pre-`0.1.0` value, before the frontage placement fix
moved the hero to `(-13336, +1307)`. Running the importer on its own therefore
teleported the hero **13 m east and onto the north side of the boulevard**, and
nothing complained: a displaced building still renders. The only symptom was the
reference score dropping 0.69 -> 0.56 with the 3x3 composition correlation
flipping +0.54 -> -0.08.
- Fix: `Tools/ue_placement.py` is now the single source of truth (hero position,
verified frontage spans, `span_error`/`describe_span_failure`). Both
`ue_hero_facade_import.py` and `ue_build_demo_map.py` import it, the import asserts
the staged span and logs `PLACEMENT ERROR` when it does not match, and tests assert
the stale literal cannot come back. Spawn-view score back to **0.72** (best yet).
- Lesson to keep: **duplicated placement constants drift silently.** The metric that
caught it was the composition correlation, not a visual review.
## 2026-11-09 — 1440p re-measured with the kit: 363 fps, ~12% cost
- 2560×1440, Epic preset, TSR, VSync off, no frame generation: **2.76 ms average
(363 fps)**, p50 2.73, p95 3.17, p99 3.64, max 4.62 — against `0.1.0`'s 2.42 ms
(414 fps). The 625 kit actors + 8 generated textures cost about **12 %** and leave
~6× the 60 fps floor.
- **Variance trap confirmed a third time:** runs reporting ~105 fps (9.5 ms) were
two overlapping game instances — an earlier launch had not exited. `pgrep -af
Binaries/Linux/GigLine` should show one wrapper + one binary; 363 fps reproduced
twice on a clean run. Always count instances before believing a perf number.
- Series committed at `Docs/perf/0.1.5-kit-walk-frametimes.csv`; comparison table in
`Docs/release-checks.md`.
## 2026-11-09 — 604 unmaterialed actors: material rebuild was delete+recreate
- The "blockout cleaning pass" turned out to need **material and placement hygiene,
not more geometry**. `Tools/ue_audit_scene.py` (new) found **604 of 620 mesh actors
with no material**: kit pieces and theatre trim all untextured.
- **Cause:** `ensure_material` in `ue_build_demo_map.py` deleted and recreated each
`/Game/Hollywood/Site/M_*` asset every run ("so a look change cannot be masked by a
stale asset"). That orphane every reference: actors staged by other tools pointed at
deleted assets and fell back to the default material. The map looked only *slightly*
flat — no error, no warning.
- **Fix:** update the material **in place** — load it, `MaterialEditingLibrary.
delete_all_material_expressions(mat)`, rebuild the graph, recompile, save. Note
`Material.expressions` is protected from Python; use the library's deleter.
- **Ordering rule now documented and enforced:** the demo builder rebuilds materials,
so kit and theatre staging must run *after* it. `render_screenshots.sh` now runs
demo map → kit assembly → theatre trim → scene audit → shot cameras → capture, and
**fails on a non-zero audit count**.
- Second find: 53 duplicated kit labels (both shop piers of a bay shared one name).
- Scene audit after the fixes: **0 failures** (all materialed, all spans verified, no
duplicate labels). Packaged QA still PASS; spawn-view reference score 0.72.
## 2026-11-09 — v0.1.5 released
- All actionable `0.1.5` work is done: hero fluting + recessed motto band, kit layout
for wide/tall buildings, placement single-source-of-truth, 1440p re-measure
(363 fps), and the material/placement hygiene that the "blockout cleaning" task
actually needed (604 unmaterialed actors, 53 duplicate labels) with a new scene
audit gate.
- Only item carried forward is the **retail frontage (6822 / 6806–6820 / 6800)**,
blocked because Commons holds no usable reference for two of the three buildings
and substituting the theatre's photographs would misattribute the façades. It now
lives in a `0.1.6` Retail frontage patch.
- Precedent applied again: tag when the stated scope is done, carry blocked scope
into the next patch rather than letting a release wait indefinitely on missing
reference photography.
+5 -1
View File
@@ -2,9 +2,13 @@
Development roadmap for this project: milestone goals, scope, decisions, and release gates. Actionable tasks, claims, and progress marks live in [TODO.md](TODO.md).
## Quality priority
**Realism leads; performance is a floor.** Façade identity, proportion, material and light are judged against the reference photographs and drive design decisions. Performance closes the quality list: 60 FPS at 1440p without frame generation is a budget every release must meet, and if realism cannot fit inside it the realism scope is cut deliberately and recorded — never the frame rate.
## Status at a glance — 2026-11-08 15:30
Workstation, geo pipeline, and engine validation are done. The art pipeline has a first blockout, terrain, and street in metres, in one shared coordinate frame. **Project bootstrap is complete**: `Game/GigLine` builds, packages, and both content-acceptance tests (static-mesh scale, MPFB skeletal character) pass. **`v0.1.0` released 2026-11-08**: the walkable demo, hero façade, verified block frontage, first-person controls, ODbL attribution, and every release check pass (traversal, placement, collision, 60 fps at 1440p, fresh-checkout build, packaged build with controls). Next: the `0.1.1`–`0.1.4` patch series (collision detail, scale/façade corrections, packaging fixes, real building façades), then `0.2.0` (player body, third-person camera, animation).
Workstation, geo pipeline, and engine validation are done. The art pipeline has a first blockout, terrain, and street in metres, in one shared coordinate frame. **Project bootstrap is complete**: `Game/GigLine` builds, packages, and both content-acceptance tests (static-mesh scale, MPFB skeletal character) pass. **`v0.1.0` released 2026-11-08**: the walkable demo, hero façade, verified block frontage, first-person controls, ODbL attribution, and every release check pass (traversal, placement, collision, 60 fps at 1440p, fresh-checkout build, packaged build with controls). Next: the `0.1.1`–`0.1.4` patch series (collision detail, scale/façade corrections, packaging fixes, real building façades), **Next:** `0.1.6` retail frontage (6822, 6806–6820, 6800 — blocked on reference photography for two of the three), then `0.2.0` (player body, third-person camera, animation).
| Area | State |
| --- | --- |
+34 -40
View File
@@ -1,57 +1,51 @@
# Screenshots — visual review
# Screenshots — demo-map visual review
Rendered in-game captures of the current project content, generated by [`Tools/render_screenshots.sh`](../Tools/render_screenshots.sh). The PNGs are committed alongside this README so a reviewer sees exactly what the build rendered.
Rendered captures of **the demo map only** — what a player sees when the demo runs. Generated by [`Tools/render_screenshots.sh`](../Tools/render_screenshots.sh); the PNGs are committed so a reviewer sees exactly what the build rendered.
The review staging map (`/Game/Test/ReviewMap`) is not part of this set: it exists to hold the camera list and the hero detail rig, and it is not what ships.
## How to regenerate
```bash
Tools/render_screenshots.sh
Tools/render_screenshots.sh --verify
```
Rebuilds `Game/GigLine/Content/Test/ReviewMap.umap` from current content, runs the game on it (and on the DemoMap for the walkable views), walks the fixed camera list via `AGigLineShotActor`, captures each shot, copies the PNGs here and exits — about a minute. The camera list lives in `Tools/ue_build_review_map.py`; add new shots there, remove retired ones here.
Stages the demo shot cameras, runs the game on `/Game/Hollywood/DemoMap`, copies the PNGs into `Screenshots/demo/` and then runs [`Tools/visual_loop.py`](../Tools/visual_loop.py) `--verify` to check that each shot still shows the content its expectation describes. About a minute.
> Naming: shots use their stable camera name (no index prefix), so adding or reordering cameras never renumbers files or orphans the old ones. Click an image for the full-size PNG.
## The shot list
## Shot list
Each shot has an **expectation** in [`Tools/visual_targets.json`](../Tools/visual_targets.json) — the content the capture must show — and a verification state produced by `--verify`: it fails if the frame is blank, the wrong size, or has no tonal range.
All shots ☑ approved 2026-11-07, including the block frontage pass that added the neighbouring buildings and corrected the hero and north-building placement.
**2026-11-08 — regenerated set (v0.1.1 geometry).** Re-rendered again after the `0.1.1` street change (sidewalk upstand tapered to the building edge, crossing ramps graded to ≤8 %). Re-rendered via `Tools/render_screenshots.sh` after the placement release check. The `demo-north-buildings` camera was re-pointed (it previously sat 9.5 km west and framed 85 % sky); that shot now shows both north buildings, and all maps carry the re-staged shot cameras. **Approved 2026-11-08** (owner review of the regenerated set).
| # | File | Preview | Map | View | What it shows |
| # | File | View | Expected content | Verified | Approved |
| --- | --- | --- | --- | --- | --- |
| 1 | `review-overview.png` | [<img src="review-overview.png" alt="overview" width="180">](review-overview.png) | Review | 7 m up, NE | Whole review layout: character + both cubes on the floor |
| 2 | `review-character.png` | [<img src="review-character.png" alt="character" width="180">](review-character.png) | Review | eye height, south | MPFB mannequin, idle pose + warm skin; feet meet the plane |
| 3 | `review-scale-cubes.png` | [<img src="review-scale-cubes.png" alt="scale cubes" width="180">](review-scale-cubes.png) | Review | south | Floating vs grounded cube: cast vs contact shadow |
| 4 | `review-player-placement.png` | [<img src="review-player-placement.png" alt="player placement" width="180">](review-player-placement.png) | Review | spawn | The `AGigLineCharacter` pawn, capsule height and landing |
| 5 | `review-npc-prefab.png` | [<img src="review-npc-prefab.png" alt="npc prefab" width="180">](review-npc-prefab.png) | Review | mid, west | The `AGigLineNPCPerson` prefab: collision capsule + visible MPFB body, feet on Z 0 |
| 6 | `review-ground-contact.png` | [<img src="review-ground-contact.png" alt="ground contact" width="180">](review-ground-contact.png) | Review | knee height, feet | Floating/sinking character is obvious here |
| 7 | `review-shadow-compare.png` | [<img src="review-shadow-compare.png" alt="shadow compare" width="180">](review-shadow-compare.png) | Review | low angle | Detached (floating) vs touching (grounded) shadows |
| 8 | `review-hero-portals.png` | [<img src="review-hero-portals.png" alt="hero portals" width="180">](review-hero-portals.png) | Review | 8 m, eye height | Three genuine 1.2 m portal recesses with flights and plinth |
| 9 | `review-hero-facade-front.png` | [<img src="review-hero-facade-front.png" alt="hero facade front" width="180">](review-hero-facade-front.png) | Review | road side, straight-on | Hero façade elevation and column rhythm |
| 10 | `review-hero-facade-oblique.png` | [<img src="review-hero-facade-oblique.png" alt="hero facade oblique" width="180">](review-hero-facade-oblique.png) | Review | north-west | Body depth and column line standing proud of the mass |
| 11 | `demo/review-demo-spawn-view.png` | [<img src="demo/review-demo-spawn-view.png" alt="spawn view" width="180">](demo/review-demo-spawn-view.png) | Demo | north sidewalk | The whole block: hero façade between its neighbours, both sidewalks and the road |
| 12 | `demo/review-demo-street-level.png` | [<img src="demo/review-demo-street-level.png" alt="street level" width="180">](demo/review-demo-street-level.png) | Demo | eye height, north side | The hero colonnade with the adjacent frontage buildings either side |
| 13 | `demo/review-demo-crossing.png` | [<img src="demo/review-demo-crossing.png" alt="crossing" width="180">](demo/review-demo-crossing.png) | Demo | along the crosswalk | The designated mid-block crossing at s=130 m (UE X -14956): three zebra bars on the asphalt, with the south frontage and the hero colonnade behind |
| 14 | `demo/review-demo-north-buildings.png` | [<img src="demo/review-demo-north-buildings.png" alt="north buildings" width="180">](demo/review-demo-north-buildings.png) | Demo | across the Blvd | North-side buildings (TCL, Tussaud's) seen from the south side across the road. **2026-11-08:** camera re-pointed to the buildings' mass centre (was 9.5 km west → 85% sky); now frames both north buildings. Flat surfaces = `0.1.4` façade scope. |
| 1 | `demo/review-demo-spawn-view.png` | north sidewalk | The whole south block: the neoclassical hero façade between its neighbours, both sidewalks and the road in the foreground | ☑ 2026-11-09 | ☑ 2026-11-09 |
| 2 | `demo/review-demo-street-level.png` | eye height, north side | Street level: the hero colonnade with the adjacent frontage buildings either side, shopfronts at pavement level | ☑ 2026-11-09 | ☑ 2026-11-09 |
| 3 | `demo/review-demo-crossing.png` | along the crosswalk | The designated mid-block crossing (s=130 m, UE X −14956): zebra bars on the asphalt, south frontage and colonnade behind | ☑ 2026-11-09 | ☑ 2026-11-09 |
| 4 | `demo/review-demo-north-buildings.png` | across the Boulevard | The two north-side buildings (TCL Chinese Theatre, Madame Tussaud's) from the south side, full height in frame | ☑ 2026-11-09 | ☑ 2026-11-09 |
Approval is per shot, in the table above (owner review of the `0.1.4` set, recorded in [`Docs/reviews/0.1.4-review.md`](../Docs/reviews/0.1.4-review.md)). Add a row when a shot is added; remove the file in the same change when a shot is retired.
## The iteration loop (uncommitted)
`Tools/visual_loop.py` measures each capture against its reference photograph and turns the deltas into modelling actions for the Blender skill packs:
```bash
python3 Tools/visual_loop.py --compare --iteration 3 # writes Art/reference/iterations/3/
```
That directory is **gitignored**: iterations stay uncommitted until a shot is confirmed, and only confirmed shots are promoted into `Screenshots/demo/`. The feedback file names the affected skill (`blender-modeler`, `archviz`, `materials`, `lookdev`, `camera-cinematography`) so the next modelling pass has a starting point rather than an opinion.
Note the comparison is only meaningful when the shot and the reference frame the same subject — a wide block view measured against a tight façade crop will always report low edge similarity.
## Scene conventions
- **Review map** (`/Game/Test/ReviewMap`): 160 m floor, top face at Z 0; the character measures Z 0..83 cm. One warm key light plus sky fill.
- **Cubes**: `Cube_ScaleTest_A` rests on the floor, `Cube_ScaleTest_B` floats at Z 2 m; both are exactly 1 m — the scale reference.
- **Demo map** (`/Game/Hollywood/DemoMap`): fixed daytime — sun at pitch −50 / yaw 120 (intensity 4, warm), sky light 2.6, sky atmosphere, height fog, fixed exposure.
- **Materials**: every exported surface carries a look-developed material built in `Tools/ue_build_demo_map.py` (`ensure_material`) because Blender's flat diffuse colours do not survive the FBX → Unreal import. Each has an explicit roughness and a subtle world-space noise blend between two shades: `M_Asphalt` (0.92), `M_Concrete` (0.88), `M_KerbConcrete` (0.85), `M_Ground` (0.98), `M_RoadPaint` (0.62), `M_FacadeStone` (0.72) and `M_Stucco` (0.86) for the north-side buildings.
- **Lighting is dynamic-only** (`r.AllowStaticLighting=False`, all lights Movable), so no lighting build is ever required.
## What to look for
- **Ground contact**: feet meet the plane with no gap and no shadow seam.
- **Scale**: everything checks against the 1 m cubes.
- **Façade rhythm**: six columns at 4.96 m across 25.75 m should read as the photographs in silhouette.
- **Demo map** (`/Game/Hollywood/DemoMap`): fixed daytime — sun pitch −62 / yaw 120 (intensity 4, warm), sky light 2.6, sky atmosphere, height fog, fixed exposure.
- **Cameras**: elevation shots sit at their target height (pitch 0, so verticals stay parallel) and use a 42–55° lens to fill the frame from across the street.
- **Materials**: every surface carries a look-developed material from `Tools/ue_build_demo_map.py` — noise-blended base colour, masonry coursing, and a generated texture set (`Tools/generate_facade_textures.py`) sampled world-aligned. Explicit roughness and specular per surface type.
## Known cosmetic issues
- Floor speckle from the `Constant3Vector` wiring in `Tools/ue_build_review_map.py`.
- Hero façade renders pale — Blender flat colours with no normal/roughness, so it reads as untextured stone (the `0.1.4` façade patch, not a lighting bug).
- Banners and window panels float detached from the colonnade wall (next modelling pass).
- Between kerb and building line, the walkable surface is the terrain mesh, not a modelled pavement.
- Hero façade renders pale: the stone material is a graph and a texture set, not a scanned surface (the `0.1.4` façade pass is in progress).
- Banners sit on the column line rather than flush to the wall (they hang on the columns in the photographs, so this is acceptable).
- Between kerb and building line the walkable surface is the terrain mesh, not a modelled pavement.
- The El Capitan Theatre has no marquee or blade sign yet — the feature that makes it read as itself.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loaded 100 of 139 files, more files were not shown because too many files have changed in this diff. Show more