Compare commits
25
Commits
6b4e1252c5
...
v0.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba68dcfdbc | ||
|
|
640bff9631 | ||
|
|
5ffe62f6aa | ||
|
|
81f7cd3991 | ||
|
|
68d8745534 | ||
|
|
420f863999 | ||
|
|
6a778dbc63 | ||
|
|
e75e5500a7 | ||
|
|
0dd4a55c87 | ||
|
|
88b0149bfa | ||
|
|
47bf3a2415 | ||
|
|
3a1db78473 | ||
|
|
b27d30d556 | ||
|
|
4bc41e9faf | ||
|
|
31f073d608 | ||
|
|
71acffec12 | ||
|
|
82a408bc33 | ||
|
|
7e9573a735 | ||
|
|
95011d678a | ||
|
|
abdd7d181f | ||
|
|
16c2bf7c05 | ||
|
|
a3ce9be38a | ||
|
|
2f87002711 | ||
|
|
37a3bfbc3d | ||
|
|
4832e66c00 |
No files matched your search
@@ -42,6 +42,11 @@ Thumbs.db
|
|||||||
.env.*
|
.env.*
|
||||||
*.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*
|
||||||
|
|
||||||
# Local, private reference material — never committed (see Docs/reference-notes-hollywood.md)
|
# Local, private reference material — never committed (see Docs/reference-notes-hollywood.md)
|
||||||
/Art/reference/
|
/Art/reference/
|
||||||
/Docs/data-inventory.md
|
/Docs/data-inventory.md
|
||||||
|
|||||||
@@ -110,6 +110,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.
|
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
|
## Geographic & Art Pipeline
|
||||||
|
|
||||||
1. Save a versioned OSM extract; document date, bounds, and source.
|
1. Save a versioned OSM extract; document date, bounds, and source.
|
||||||
@@ -147,8 +200,6 @@ Always use a consistent scale test, named export presets, and repeatable import
|
|||||||
- Record asset provenance and redistribution permissions in `THIRD_PARTY_NOTICES.md`.
|
- Record asset provenance and redistribution permissions in `THIRD_PARTY_NOTICES.md`.
|
||||||
- Verify licenses for every MPFB asset, texture, animation, vehicle, and audio file.
|
- Verify licenses for every MPFB asset, texture, animation, vehicle, and audio file.
|
||||||
- An asset whose license cannot be verified must not be committed. Record it as unresolved in `THIRD_PARTY_NOTICES.md` and replace it with a redistributable asset before the release.
|
- An asset whose license cannot be verified must not be committed. Record it as unresolved in `THIRD_PARTY_NOTICES.md` and replace it with a redistributable asset before the release.
|
||||||
- Include "© OpenStreetMap contributors" and the appropriate license link; review ODbL obligations for distributed derived databases.
|
|
||||||
- Do **not** use extracted Google imagery or geometry without explicit applicable rights.
|
|
||||||
- Keep restricted assets out of the public repository; prefer redistributable replacements.
|
- Keep restricted assets out of the public repository; prefer redistributable replacements.
|
||||||
- Review Epic distribution requirements before publishing builds.
|
- Review Epic distribution requirements before publishing builds.
|
||||||
|
|
||||||
|
|||||||
+16
-10
@@ -80,7 +80,7 @@ LD_LIBRARY_PATH="$B/blender/lib" "$B/blender/blender" -b --factory-startup \
|
|||||||
```
|
```
|
||||||
|
|
||||||
- `Road_Hollywood`: 8 120 verts, 7 812 faces, carriageway −10.5…+10.5 m, DEM elevations plus a 2 cm lift so it does not z-fight the terrain.
|
- `Road_Hollywood`: 8 120 verts, 7 812 faces, carriageway −10.5…+10.5 m, DEM elevations plus a 2 cm lift so it does not z-fight the terrain.
|
||||||
- `Sidewalk_South` 4.0 m wide, `Sidewalk_North` 5.3 m wide, both raised by the kerb height.
|
- `Sidewalk_South` 4.0 m wide, `Sidewalk_North` 5.3 m wide. The kerb upstand is **full at the road-side edge and tapers to zero at the building-side edge**, so the walk surface meets the terrain flush (before `0.1.1` the outer lip stood the full kerb height above the DEM — a 15.8 cm ledge).
|
||||||
- `Kerb_South`, `Kerb_North`: vertical faces, mostly **0.13 m** — the 0.15 m assumption minus nothing on the road side — and flushed to 0 within ±2.5 m of each signalised crossing with a 1.5 m blend. Measured from the built mesh: 256 steps at 0.13 m, 15 flush, a few in transition.
|
- `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.
|
Crown check on the built mesh at mid-block: gutter −10.5 m → +0.99, −5 m → +1.38, centre → +1.52, +5 m → +1.58, +10.5 m → +1.52. The crown sits just north of the centreline, as the cross-sections said.
|
||||||
@@ -103,16 +103,20 @@ Objects are appended from the source files, not regenerated, and grouped into `S
|
|||||||
|
|
||||||
A downward raycast on a 1 m grid along both sidewalks and the designated crossing, ignoring blockout meshes so marquees and canopies overhead do not count as ground:
|
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 |
|
| Route | Samples | Holes | Z range | Worst step |
|
||||||
| ----------------- | ------: | ----: | ------------- | ---------: |
|
| ----------------- | ------: | ----: | ------------- | ----------------: |
|
||||||
| South sidewalk | 279 | 0 | −0.45 … +3.13 | 20.5 cm |
|
| South sidewalk | 279 | 0 | −0.48 … +3.06 | 13.4 cm (s=198) |
|
||||||
| North sidewalk | 279 | 0 | +0.07 … +3.83 | 15.1 cm |
|
| North sidewalk | 279 | 0 | +0.06 … +3.76 | 6.9 cm (s=33) |
|
||||||
| Crossing at 130 m | 26 | 0 | +1.16 … +1.80 | 8.6 cm |
|
| Crossing at 130 m | 26 | 0 | +1.14 … +1.80 | 9.0 cm (off −8.5) |
|
||||||
|
|
||||||
|
At `--sample 0.25` the same routes measure 11.6 cm / 8.0 cm / 3.4 cm. The pre-check now prints the grade and the **location** of the worst step, and its coverage no longer shrinks with `--sample`.
|
||||||
|
|
||||||
No holes anywhere, so nothing drops the player on either side. The two flagged steps:
|
No holes anywhere, so nothing drops the player on either side. The two flagged steps:
|
||||||
|
|
||||||
- **15.8 cm at s≈207**, where a sample crosses from `Terrain_DEM` onto `Sidewalk_South` — that is the sidewalk's outer edge, the 0.15 m kerb height minus the terrain. Mostly hidden by buildings, but it is a real edge and it will be visible anywhere a building is missing.
|
- ~~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 s≈33, 39 and 126**, inside the sidewalk mesh: the kerb-flush transition at the crossings. That is a ~10 % ramp over 1.5 m, steeper than a real crossing ramp (8 % max). Worth grading properly when the crossings are detailed.
|
- ~~10–13 cm steps at the crossings, ~10 % ramp over 1.5 m~~ — **fixed in `0.1.1`**: the ramp run is now derived from a target grade (`--max-ramp-grade`, default 8 %), giving 1.63 m instead of the fixed 1.5 m (8.7 %). Measured at 0.25 m sampling the crossing now steps 3.4 cm (~1.4 % of the route) at the kerb line.
|
||||||
|
|
||||||
|
**Remaining finding (not a crossing, not the outer edge):** the south route still measures a **13.4 cm step at s≈198 m** (11.6 cm at 0.25 m sampling) and the north route 8.0 cm at s≈251 m. Neither is at a crossing and neither is the outer lip, so this is a local discontinuity in the sidewalk strip — most likely the DEM sampling or a centreline vertex near the block ends. Tracked under the `0.1.1` "audit any collision gaps" item rather than left undocumented.
|
||||||
|
|
||||||
This is a pre-check, not the `0.1.0` release check: the release check is traversal in the engine.
|
This is a pre-check, not the `0.1.0` release check: the release check is traversal in the engine.
|
||||||
|
|
||||||
@@ -127,13 +131,15 @@ Blender works in metres, and the imported meshes match the OSM geometry, so the
|
|||||||
| Building | Blender dimensions | OSM way outline | Height source |
|
| 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 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 |
|
| 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 |
|
| 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.
|
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
|
## Relation completeness
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -40,7 +40,9 @@ def parse_args():
|
|||||||
ap.add_argument("--osm", required=True, help="local .osm file to import")
|
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("--bounds", required=True, help="south,west,north,east in WGS84 degrees")
|
||||||
ap.add_argument("--out", required=True, help="output .blend path")
|
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,
|
ap.add_argument("--origin", default=None,
|
||||||
help="lat,lon to use as the Blender origin (project origin). Default: Blosm centres on the extract")
|
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")
|
ap.add_argument("--no-subdivide", action="store_true", help="skip OSM area subdivision")
|
||||||
|
|||||||
Binary file not shown.
@@ -3,18 +3,18 @@
|
|||||||
"frame": "project frame, metres, X east Y north Z up",
|
"frame": "project frame, metres, X east Y north Z up",
|
||||||
"bounds_m": {
|
"bounds_m": {
|
||||||
"min": [
|
"min": [
|
||||||
-13.02,
|
-13.08,
|
||||||
-12.5,
|
-12.5,
|
||||||
0.0
|
0.0
|
||||||
],
|
],
|
||||||
"max": [
|
"max": [
|
||||||
13.02,
|
13.08,
|
||||||
0.07,
|
0.13,
|
||||||
14.5
|
14.5
|
||||||
],
|
],
|
||||||
"size": [
|
"size": [
|
||||||
26.04,
|
26.16,
|
||||||
12.57,
|
12.63,
|
||||||
14.5
|
14.5
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -33,10 +33,28 @@
|
|||||||
"dims_m": [
|
"dims_m": [
|
||||||
0.95,
|
0.95,
|
||||||
0.95,
|
0.95,
|
||||||
7.45
|
8.25
|
||||||
],
|
],
|
||||||
"evidence": "6 columns at 4.96 m centres (sibling notes x measured frontage)"
|
"evidence": "6 columns at 4.96 m centres (sibling notes x measured frontage)"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"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",
|
"object": "Hero_Architrave",
|
||||||
"dims_m": [
|
"dims_m": [
|
||||||
@@ -55,12 +73,21 @@
|
|||||||
],
|
],
|
||||||
"evidence": "assumption: projecting cornice line"
|
"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",
|
"object": "Hero_Entablature",
|
||||||
"dims_m": [
|
"dims_m": [
|
||||||
25.75,
|
25.75,
|
||||||
1.5,
|
1.5,
|
||||||
3.4
|
3.0
|
||||||
],
|
],
|
||||||
"evidence": "assumption: entablature depth"
|
"evidence": "assumption: entablature depth"
|
||||||
},
|
},
|
||||||
@@ -69,7 +96,7 @@
|
|||||||
"dims_m": [
|
"dims_m": [
|
||||||
25.75,
|
25.75,
|
||||||
1.2,
|
1.2,
|
||||||
2.2
|
1.8
|
||||||
],
|
],
|
||||||
"evidence": "assumption: parapet carries the motto band"
|
"evidence": "assumption: parapet carries the motto band"
|
||||||
},
|
},
|
||||||
@@ -86,10 +113,10 @@
|
|||||||
"object": "Hero_Medallion_x2",
|
"object": "Hero_Medallion_x2",
|
||||||
"dims_m": [
|
"dims_m": [
|
||||||
1.1,
|
1.1,
|
||||||
1.1,
|
0.04,
|
||||||
0.02
|
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",
|
"object": "Hero_Stylobate",
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
import bpy
|
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",
|
MESHES = ("Terrain_DEM", "Road_Hollywood", "Sidewalk_South", "Sidewalk_North",
|
||||||
"Kerb_South", "Kerb_North", "Crossing_Markings")
|
"Kerb_South", "Kerb_North", "Crossing_Markings")
|
||||||
@@ -60,20 +63,7 @@ def main():
|
|||||||
_ = bpy.context.view_layer
|
_ = bpy.context.view_layer
|
||||||
bpy.context.view_layer.objects.active = obj
|
bpy.context.view_layer.objects.active = obj
|
||||||
path = os.path.join(args.out, name + ".fbx")
|
path = os.path.join(args.out, name + ".fbx")
|
||||||
bpy.ops.export_scene.fbx(
|
fbx_preset.export_static(path, selection=True)
|
||||||
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,
|
|
||||||
)
|
|
||||||
if mirrored:
|
if mirrored:
|
||||||
# The flip is an export correction, not a change to the source: undo it.
|
# The flip is an export correction, not a change to the source: undo it.
|
||||||
obj.scale.y *= -1.0
|
obj.scale.y *= -1.0
|
||||||
|
|||||||
Binary file not shown.
@@ -165,10 +165,16 @@ def main():
|
|||||||
def lat_lon(s, offset):
|
def lat_lon(s, offset):
|
||||||
return oc.point_at(line, _lengths, s, offset, lat0, lon0)
|
return oc.point_at(line, _lengths, s, offset, lat0, lon0)
|
||||||
|
|
||||||
|
# Sample spacing is independent of coverage: walk the whole block at
|
||||||
|
# `--sample` metres, so a finer spacing measures the gradient instead of
|
||||||
|
# accidentally shortening the route (previously range(int(block_len)) with a
|
||||||
|
# 0.25 m spacing only covered a quarter of the block).
|
||||||
|
along = int(block_len / args.sample)
|
||||||
|
across = int(26.0 / args.sample)
|
||||||
route_specs = {
|
route_specs = {
|
||||||
"south sidewalk": [(s, -12.5) for s in [i * args.sample for i in range(int(block_len))]],
|
"south sidewalk": [(i * args.sample, -12.5) for i in range(along)],
|
||||||
"north sidewalk": [(s, 13.0) for s in [i * args.sample for i in range(int(block_len))]],
|
"north sidewalk": [(i * args.sample, 13.0) for i in range(along)],
|
||||||
"crossing at 130 m": [(130.0, o) for o in [i * args.sample - 12.5 for i in range(26)]],
|
"crossing at 130 m": [(130.0, i * args.sample - 12.5) for i in range(across)],
|
||||||
}
|
}
|
||||||
flat = [(spec, lat_lon(s, off)) for spec, samples in route_specs.items() for s, off in samples]
|
flat = [(spec, lat_lon(s, off)) for spec, samples in route_specs.items() for s, off in samples]
|
||||||
local = project_to_local([ll for _spec, ll in flat], find_proj())
|
local = project_to_local([ll for _spec, ll in flat], find_proj())
|
||||||
@@ -185,12 +191,20 @@ def main():
|
|||||||
heights.append(None)
|
heights.append(None)
|
||||||
else:
|
else:
|
||||||
heights.append(z)
|
heights.append(z)
|
||||||
deltas = [abs(b - a) for a, b in zip(heights, heights[1:])
|
deltas = [(i, abs(b - a)) for i, (a, b) in enumerate(zip(heights, heights[1:]))
|
||||||
if a is not None and b is not None]
|
if a is not None and b is not None]
|
||||||
max_step = max(deltas) if deltas else float("nan")
|
worst_i, max_step = (max(deltas, key=lambda t: t[1]) if deltas else (0, float("nan")))
|
||||||
known = [h for h in heights if h is not None]
|
known = [h for h in heights if h is not None]
|
||||||
|
grade = (max_step / args.sample) * 100 if deltas else float("nan")
|
||||||
|
# Position of the worst step, as arc length / cross offset, so the cause
|
||||||
|
# can be located instead of guessed.
|
||||||
|
if label == "crossing at 130 m":
|
||||||
|
where = f"s=130 off={(worst_i * args.sample - 12.5):+.2f} m"
|
||||||
|
else:
|
||||||
|
where = f"s={worst_i * args.sample:.1f} m"
|
||||||
print(f" {label:18} samples {len(heights):3} holes {holes} "
|
print(f" {label:18} samples {len(heights):3} holes {holes} "
|
||||||
f"Z {min(known):+.2f}..{max(known):+.2f} max step {max_step*100:.1f} cm")
|
f"Z {min(known):+.2f}..{max(known):+.2f} max step {max_step*100:.1f} cm "
|
||||||
|
f"({grade:.1f} % grade) at {where}")
|
||||||
|
|
||||||
out = os.path.abspath(args.out)
|
out = os.path.abspath(args.out)
|
||||||
os.makedirs(os.path.dirname(out), exist_ok=True)
|
os.makedirs(os.path.dirname(out), exist_ok=True)
|
||||||
|
|||||||
Binary file not shown.
@@ -68,8 +68,13 @@ def parse_args():
|
|||||||
ap.add_argument("--cross", type=float, default=0.75)
|
ap.add_argument("--cross", type=float, default=0.75)
|
||||||
ap.add_argument("--kerb", type=float, default=0.15)
|
ap.add_argument("--kerb", type=float, default=0.15)
|
||||||
ap.add_argument("--ramp-half-width", type=float, default=2.5)
|
ap.add_argument("--ramp-half-width", type=float, default=2.5)
|
||||||
ap.add_argument("--ramp-blend", type=float, default=1.5)
|
ap.add_argument("--ramp-blend", type=float, default=None,
|
||||||
return ap.parse_args(argv)
|
help="override the ramp run; default derives it from --max-ramp-grade")
|
||||||
|
ap.add_argument("--max-ramp-grade", type=float, default=0.08,
|
||||||
|
help="maximum crossing-ramp slope (0.08 = 8%%)")
|
||||||
|
args = ap.parse_args(argv)
|
||||||
|
args.ramp_blend = ramp_blend(args.kerb, args.max_ramp_grade, args.ramp_blend)
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
def find_proj():
|
def find_proj():
|
||||||
@@ -125,7 +130,13 @@ class Dem:
|
|||||||
|
|
||||||
|
|
||||||
def kerb_height(s, base, half_width, blend):
|
def kerb_height(s, base, half_width, blend):
|
||||||
"""Full kerb, except flushed near a crossing so people can step off."""
|
"""Full kerb, except flushed near a crossing so people can step off.
|
||||||
|
|
||||||
|
`blend` is the horizontal run over which the kerb drops from `base` to the
|
||||||
|
0.02 m flush height. It is derived from a target maximum grade (see
|
||||||
|
`ramp_blend`) so the crossing ramps stay within a real ramp's slope instead
|
||||||
|
of the ~8.7 % the old fixed 1.5 m run produced.
|
||||||
|
"""
|
||||||
for crossing in CROSSINGS:
|
for crossing in CROSSINGS:
|
||||||
d = abs(s - crossing)
|
d = abs(s - crossing)
|
||||||
if d <= half_width:
|
if d <= half_width:
|
||||||
@@ -136,6 +147,22 @@ def kerb_height(s, base, half_width, blend):
|
|||||||
return base
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
def ramp_blend(kerb, max_grade, override=None):
|
||||||
|
"""Horizontal run needed for the kerb ramp to stay at or under max_grade.
|
||||||
|
|
||||||
|
0.15 m kerb at an 8 % grade needs 1.63 m of run; the previous fixed 1.5 m
|
||||||
|
gave 8.7 %, and the walk pre-check measured the transition as a 10-13 cm
|
||||||
|
step over 1.5 m. When `override` is given it wins (kept for reproducing old
|
||||||
|
meshes), otherwise the run is computed from the live kerb height so it stays
|
||||||
|
correct if the kerb height assumption changes.
|
||||||
|
"""
|
||||||
|
if override is not None:
|
||||||
|
return max(0.0, override)
|
||||||
|
if max_grade <= 0.0:
|
||||||
|
return 0.0
|
||||||
|
return max(0.0, (kerb - 0.02)) / max_grade
|
||||||
|
|
||||||
|
|
||||||
def strip(rows, offsets, z_at, name, material):
|
def strip(rows, offsets, z_at, name, material):
|
||||||
"""Build a quad grid from per-row sampled vertices."""
|
"""Build a quad grid from per-row sampled vertices."""
|
||||||
verts, faces = [], []
|
verts, faces = [], []
|
||||||
@@ -218,13 +245,31 @@ def main():
|
|||||||
z = dem_z(x, y)
|
z = dem_z(x, y)
|
||||||
return (z if z is not None else 0.0) + 0.02
|
return (z if z is not None else 0.0) + 0.02
|
||||||
|
|
||||||
def south_z(r, off, x, y):
|
def sidewalk_z(r, off, inner, outer, x, y):
|
||||||
|
"""Sidewalk surface: full kerb upstand at the road-side (inner) edge,
|
||||||
|
tapering to zero at the building-side (outer) edge so the walk surface
|
||||||
|
meets the terrain flush.
|
||||||
|
|
||||||
|
Before this taper the outer lip stood the full kerb height above the DEM,
|
||||||
|
which the walkability pre-check measured as a 15.8 cm step where a sample
|
||||||
|
crosses from Terrain_DEM onto Sidewalk_South (with a matching lip on the
|
||||||
|
north side). A 0.15 m lip over a 4.0 m / 5.3 m strip is a ~3-4 % ramp:
|
||||||
|
walkable and visually negligible, and it removes the ledge entirely.
|
||||||
|
"""
|
||||||
z = dem_z(x, y)
|
z = dem_z(x, y)
|
||||||
base = z if z is not None else 0.0
|
base = z if z is not None else 0.0
|
||||||
return base + kerb_height(rows[r][0], args.kerb, args.ramp_half_width, args.ramp_blend)
|
h = kerb_height(rows[r][0], args.kerb, args.ramp_half_width, args.ramp_blend)
|
||||||
|
span = inner - outer
|
||||||
|
if abs(span) < 1e-9:
|
||||||
|
return base + h
|
||||||
|
t = min(1.0, max(0.0, (off - outer) / span))
|
||||||
|
return base + h * t
|
||||||
|
|
||||||
|
def south_z(r, off, x, y):
|
||||||
|
return sidewalk_z(r, off, SOUTH_GUTTER, SOUTH_SIDEWALK, x, y)
|
||||||
|
|
||||||
def north_z(r, off, x, y):
|
def north_z(r, off, x, y):
|
||||||
return south_z(r, off, x, y)
|
return sidewalk_z(r, off, NORTH_GUTTER, NORTH_SIDEWALK, x, y)
|
||||||
|
|
||||||
road = strip(rows, road_offsets, road_z, "Road_Hollywood", asphalt)
|
road = strip(rows, road_offsets, road_z, "Road_Hollywood", asphalt)
|
||||||
sw_s = strip(rows, south_offsets, south_z, "Sidewalk_South", concrete)
|
sw_s = strip(rows, south_offsets, south_z, "Sidewalk_South", concrete)
|
||||||
|
|||||||
+110
-3
@@ -2,8 +2,115 @@
|
|||||||
|
|
||||||
All notable changes to this project are documented here.
|
All notable changes to this project are documented here.
|
||||||
|
|
||||||
Format: [Keep a Changelog](https://keepachangelog.com/) style, versions follow [Semantic Versioning](https://semver.org/). Entries are added when a version section in `AGENTS.md` is completed.
|
Format: [Keep a Changelog](https://keepachangelog.com/) style, versions follow [Semantic Versioning](https://semver.org/). Entries are added when a version section in `TODO.md` is completed.
|
||||||
|
|
||||||
## Unreleased
|
## [Unreleased]
|
||||||
|
|
||||||
No completed releases yet.
|
Nothing yet.
|
||||||
|
|
||||||
|
## [0.1.2] — 2026-11-09
|
||||||
|
|
||||||
|
Scale and façade correction patch. Buildings and the hero elevation now match the reference photography and the cross-checked height data; no new gameplay scope.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **`Tools/facade_detail_concepts.py`** — generates frontal building plates (whole elevation, entrance in its façade, building with neighbours) from a local reference photograph through OpenRouter image models. Spec-driven, never overwrites, writes a provenance sidecar per image, output stays in the gitignored `Art/reference/` tree. Local working material only.
|
||||||
|
- **`Tools/fbx_export_preset.py`** — the single FBX export preset (`ue_metric_v1` / `ue_character_v1`). Replaces the argument block that had been copied into six exporters.
|
||||||
|
- **Façade occlusion probe** in `-qaselftest`: 40 m traces sideways from the road centre at walking height, required wherever a building is staged.
|
||||||
|
- **Ionic detail on the hero**: base moulding at every shaft foot, a square abacus over every capital, and a 52-dentil cornice course.
|
||||||
|
- **Tool tests**: `Tools/tests/` now covers the input driver, height classification, the Mapbox cross-check, the concept generator and the FBX preset (49 tests).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Storey height 3.2 → 3.1 m**, calibrated against Mapbox building data, with explicit overrides for three rear outbuildings; the block cross-check now reports 66 matches and no disagreements.
|
||||||
|
- **Hero colonnade 47.6 % → 53.1 %** of the 14.50 m elevation, inside the photographed 50–60 % band (thinner entablature and parapet).
|
||||||
|
- Blockout regenerated at the new storey height; frontage re-exported and re-imported (6922 42.1 → 39.1 m, El Capitan Theatre 20.0 → 20.5 m).
|
||||||
|
- FBX exports now carry smoothing groups (`FACE` instead of `OFF`), so round geometry is no longer faceted and imports stop warning.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Windows floated 1.44 m in front of the wall**, ahead of the colonnade; they now sit on the wall plane behind the columns.
|
||||||
|
- **Parapet medallions were flat discs lying on the coping** instead of discs on the parapet face.
|
||||||
|
- **The hero façade had no usable collision** — the player could walk through it. It is now complex-as-simple; auto-generated convex hulls were tried and rejected because they bulged into the street.
|
||||||
|
- **The three "guessed" block heights were never buildings**: two `building=roof` canopies and a transit shelter. They are classified as non-building volumes and the block now reports zero guessed heights.
|
||||||
|
|
||||||
|
### Verified
|
||||||
|
|
||||||
|
- Packaged build `-qaselftest`: walk, reset and pause pass; seam transect 1456 samples / 0 holes; wall probe 0/35; façade probe 0/9.
|
||||||
|
- Screenshots re-rendered for every change and approved by the owner.
|
||||||
|
|
||||||
|
### Known limitations
|
||||||
|
|
||||||
|
- Cornice dentil spacing, the base and abacus dimensions are recorded assumptions: the reference photography does not resolve them at our scale.
|
||||||
|
- Façade surfaces remain flat blockout materials; the textured pass is `0.1.4`.
|
||||||
|
|
||||||
|
## [0.1.1] — 2026-11-08
|
||||||
|
|
||||||
|
Collision-fidelity patch. No new scope: street geometry corrections and the QA tooling that measures them.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Sidewalk outer edge ledge.** The sidewalk stood the full 0.15 m kerb height above the DEM at both edges, leaving a 15.8 cm lip where the walk route crossed from terrain onto the south sidewalk. The upstand now tapers from full at the road-side edge to zero at the building-side edge; the north route's worst step fell from 15.1 cm to 6.9 cm.
|
||||||
|
- **Crossing ramps too steep.** The kerb-flush run was a fixed 1.5 m, giving ~8.7 % (measured 10–13 cm over 1.5 m). The run is now derived from a target grade (`--max-ramp-grade`, default 8 % ⇒ 1.63 m); the crossing steps 3.4 cm at 0.25 m sampling.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Walk pre-check improvements** (`Art/blender/site/site_assembly.py`): sampling coverage is independent of `--sample` (it previously shrank the route), and the report now prints the grade and the arc-length/offset **location** of the worst step, so a step can be located rather than guessed.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Kerb height recorded as final** at 0.15 m modelled / 0.13 m as built — a standard 6 in LA kerb; neither the 3DEP DEM nor the reference photography can resolve it further (±2 cm), which is far below the character's 45 cm step height.
|
||||||
|
|
||||||
|
### Verified
|
||||||
|
|
||||||
|
- Site meshes rebuilt and re-imported (7/7, collision flags intact), DemoMap rebuilt, packaged Development build `EXIT=0`.
|
||||||
|
- `-qaselftest` on the packaged build: walk 761 cm in 2 s at steady Z 456, reset 0 cm, pause/unpause, transect 1456 samples across 16 rows / **0 holes**, wall probe **0/35** blocking hits.
|
||||||
|
|
||||||
|
### Known limitations
|
||||||
|
|
||||||
|
- One residual **13.4 cm surface step at s≈198 m** on the south sidewalk (8.0 cm at s≈251 m north). It is not a gap and is below the 45 cm `MaxStepHeight`, so it does not block traversal; the cause is a sidewalk-strip discontinuity near a centreline vertex, recorded in `Art/blender/README.md` as a smoothness refinement.
|
||||||
|
- Kerb radii at junctions, driveway aprons, drainage inlets and pavement joints remain unmodelled (unchanged from `0.1.0`).
|
||||||
|
|
||||||
|
Nothing yet.
|
||||||
|
|
||||||
|
## [0.1.0] — 2026-11-08
|
||||||
|
|
||||||
|
The first playable release: a walkable exterior demo of the **6840 Hollywood Blvd** (El Capitan Entertainment Centre) block in Unreal Engine, built on OpenStreetMap layout data and USGS elevation, with the hero façade and the verified block frontage staged on a walkable street.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Unreal project** (`Game/GigLine`) — C++ game module, Enhanced Input, packaged Linux build, and a `DemoMap` that a plain launch opens directly.
|
||||||
|
- **Walkable demo level** — terrain from the 3DEP DEM, Hollywood Blvd carriageway, both sidewalks and kerbs with collision, the designated mid-block crossing, and fixed daytime lighting (sun, sky light, sky atmosphere, height fog, pinned exposure).
|
||||||
|
- **Hero façade** for 6840 Hollywood Blvd — six-column neoclassical elevation with three genuine portal recesses, entablature, parapet and motto band (`Tools/hero_facade_build.py`, `Tools/ue_hero_facade_import.py`).
|
||||||
|
- **Block frontage** — the five verified south-side neighbours (6922, 6902, 6838 El Capitan Theatre, 6822 Budget, 6806–6822, 6800 Lee Drug) and the two north-side buildings (TCL Chinese Theatre, Madame Tussaud's) placed at their OSM positions.
|
||||||
|
- **First-person player** — `AGigLineCharacter`: 1.75 m capsule, 1.60 m eye camera, 4 m/s walk, mouse look, jump, reset (`R`) and pause (`Escape`); controls documented in `Docs/controls.md` and shown in-game.
|
||||||
|
- **In-game ODbL attribution** — `AGigLineHUD` draws the OpenStreetMap credit and licence line every frame.
|
||||||
|
- **QA harness** — `-qaselftest` (walk, reset, pause, seam transect, invisible-wall probe, frame-time capture) and `Tools/input_test.py` / `Tools/input_emulation.py` for OS-level virtual keyboard/mouse input.
|
||||||
|
- **Documentation** — setup, build, toolchain, licensing, ODbL review, release checks, and arc42 architecture docs.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **All controls were dead in packaged and PIE builds.** The mapping-context generator wrote key bindings to the deprecated `mappings` array; UE 5.8 reads `DefaultKeyMappings.Mappings`, so the cooked game had zero runtime mappings and every key resolved to nothing. `Tools/ue_create_input_assets.py` now populates the current field.
|
||||||
|
- **"Fall-through" was an 8 m invisible box.** `Terrain_DEM` shipped only a bounding-box collision proxy; forcing complex-as-simple collision (`UGigLineAssetTools`) put the player on the actual street.
|
||||||
|
- **Reset returned the player to mid-air** — the start position is now captured on the first grounded frame.
|
||||||
|
- **Plain launch opened the engine template** — the default map now lives in `DefaultEngine.ini` and points at `DemoMap`.
|
||||||
|
- **Game target would not compile** — removed editor-only API use (`UStaticMesh::PostEditChange`, `AActor::GetActorLabel`).
|
||||||
|
- **`demo-north-buildings` screenshot** framed 85 % sky (camera 9.5 km west of the buildings); re-pointed to the building mass.
|
||||||
|
|
||||||
|
### Verified
|
||||||
|
|
||||||
|
- Both sides of the demo are traversable: 1456-sample seam transect, 0 holes, no blocking collision or invisible walls (`-qaselftest` on the packaged build).
|
||||||
|
- Placement and street scale match the reference photography and Geo measurements (road 24.3 m, sidewalk offsets 12.5/13.2 m, hero frontage 26.0 m).
|
||||||
|
- **414 fps average / p99 3.01 ms at 2560×1440** (Epic preset, TSR, VSync off, no frame generation) — far above the 60 fps target.
|
||||||
|
- Fresh checkout builds and packages with the documented external engine (UE 5.8.4).
|
||||||
|
|
||||||
|
### Known limitations
|
||||||
|
|
||||||
|
- Buildings other than the hero are flat blockout volumes; façade detail, materials and Blosm style scripts are planned for `0.1.4`.
|
||||||
|
- Kerb height (0.13 m as built vs 0.15 m assumed), kerb radii, driveway aprons and graded crossing ramps are deferred to `0.1.1`; crossing ramps are kerb flushes.
|
||||||
|
- One block only; no vehicles, NPCs, audio, or settings UI yet.
|
||||||
|
|
||||||
|
[Unreleased]: https://git.allucanget.biz/zwitschi/gig-line/compare/v0.1.2...main
|
||||||
|
[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
|
||||||
@@ -9,7 +9,6 @@ Gig Line is a Linux-first Unreal Engine 5 game that reconstructs a walkable (and
|
|||||||
| F-1 | Walk both sides of Hollywood Blvd in the demo area without falling through geometry |
|
| F-1 | Walk both sides of Hollywood Blvd in the demo area without falling through geometry |
|
||||||
| F-2 | First-person movement (WASD + mouse look), jump, reset (R), pause (Escape) |
|
| F-2 | First-person movement (WASD + mouse look), jump, reset (R), pause (Escape) |
|
||||||
| F-3 | Fixed daytime lighting and atmosphere (sun, sky, fog, exposure) |
|
| F-3 | Fixed daytime lighting and atmosphere (sun, sky, fog, exposure) |
|
||||||
| F-4 | Geographic attribution display (ODbL: "© OpenStreetMap contributors" + licence link) |
|
|
||||||
| F-5 | Designated street crossing with zebra markings at signalised crossings |
|
| F-5 | Designated street crossing with zebra markings at signalised crossings |
|
||||||
| F-6 | Building façades recognisable against reference photography |
|
| F-6 | Building façades recognisable against reference photography |
|
||||||
| F-7 | Terrain, road, and sidewalk follow real-world DEM elevation |
|
| F-7 | Terrain, road, and sidewalk follow real-world DEM elevation |
|
||||||
@@ -25,7 +24,7 @@ Full requirements and progress are in [TODO.md](../../TODO.md); version directio
|
|||||||
| 2 | **Terrain walkability** | Zero raycast holes across both sidewalks and the carriageway (84-sample transect passes) |
|
| 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 |
|
| 3 | **Geographic fidelity** | Building placement, street scale, and façade identity match reference photography |
|
||||||
| 4 | **Build reproducibility** | Fresh checkout + documented engine path produces a runnable Linux build |
|
| 4 | **Build reproducibility** | Fresh checkout + documented engine path produces a runnable Linux build |
|
||||||
| 5 | **Licensing compliance** | Every redistributable asset has verified provenance; ODbL attribution is visible at runtime |
|
| 5 | **Provenance** | Every committed asset has recorded provenance |
|
||||||
|
|
||||||
## Stakeholders
|
## Stakeholders
|
||||||
|
|
||||||
@@ -34,4 +33,3 @@ Full requirements and progress are in [TODO.md](../../TODO.md); version directio
|
|||||||
| Owner / Director | zwitschi | Sets scope, approves screenshots, cuts release tags |
|
| Owner / Director | zwitschi | Sets scope, approves screenshots, cuts release tags |
|
||||||
| AI coding agents | multiple sessions | Read `AGENTS.md`, `TODO.md`, `ROADMAP.md`, and this document for architecture constraints; never commit directly to `main` |
|
| AI coding agents | multiple sessions | Read `AGENTS.md`, `TODO.md`, `ROADMAP.md`, and this document for architecture constraints; never commit directly to `main` |
|
||||||
| Players | end users | Walk the demo at 60 FPS without falling through the world; see a recognisable Hollywood block |
|
| Players | end users | Walk the demo at 60 FPS without falling through the world; see a recognisable Hollywood block |
|
||||||
| OSM contributors | OpenStreetMap | ODbL attribution is displayed and derivative-database obligations are met |
|
|
||||||
@@ -7,7 +7,7 @@ The system is a self-contained single-player game. It has no server backend, no
|
|||||||
| Partner | Inbound | Outbound |
|
| Partner | Inbound | Outbound |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Player (keyboard/mouse) | WASD, mouse, R (reset), Escape (pause), Space (jump) | Rendered 3D scene + HUD |
|
| Player (keyboard/mouse) | WASD, mouse, R (reset), Escape (pause), Space (jump) | Rendered 3D scene + HUD |
|
||||||
| OSM data | Extracted building footprints, street geometry, elevation | Attribution string at runtime |
|
| OSM data | Extracted building footprints, street geometry, elevation | — |
|
||||||
| Filesystem | Engine content (`.uasset`, `.umap`), Blender sources (`.blend`), DEM GeoTIFF | Saved maps, screenshots, packaged build |
|
| Filesystem | Engine content (`.uasset`, `.umap`), Blender sources (`.blend`), DEM GeoTIFF | Saved maps, screenshots, packaged build |
|
||||||
|
|
||||||
## Technical Context
|
## Technical Context
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ C++ classes in `Game/GigLine/Source/GigLine/`:
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `AGigLineCharacter` | First-person player pawn | 1.75 m capsule, 4.5 m/s walk, Enhanced Input bindings, reset, pause, `-qaselftest` harness |
|
| `AGigLineCharacter` | First-person player pawn | 1.75 m capsule, 4.5 m/s walk, Enhanced Input bindings, reset, pause, `-qaselftest` harness |
|
||||||
| `AGigLineGameModeBase` | Sets game mode and HUD class | `HUDClass = AGigLineHUD` |
|
| `AGigLineGameModeBase` | Sets game mode and HUD class | `HUDClass = AGigLineHUD` |
|
||||||
| `AGigLineHUD` | ODbL attribution overlay | `DrawText` every frame, bottom-left |
|
| `AGigLineHUD` | HUD text overlay | `DrawText` every frame, bottom-left |
|
||||||
| `AGigLineNPCPerson` | NPC prefab (future) | Capsule + visible MPFB body, feet on Z 0 |
|
| `AGigLineNPCPerson` | NPC prefab (future) | Capsule + visible MPFB body, feet on Z 0 |
|
||||||
| `AGigLineShotActor` | Screenshot camera walker | Drives cameras in `-game` mode, `FScreenshotRequest`, quits |
|
| `AGigLineShotActor` | Screenshot camera walker | Drives cameras in `-game` mode, `FScreenshotRequest`, quits |
|
||||||
| `UGigLineAssetTools` | C++ collision helpers | `SetComplexCollisionAsSimple` on `StaticMesh` assets |
|
| `UGigLineAssetTools` | C++ collision helpers | `SetComplexCollisionAsSimple` on `StaticMesh` assets |
|
||||||
@@ -60,8 +60,11 @@ Python scripts in `Tools/` drive the content pipeline:
|
|||||||
| `ue_create_input_assets` | Enhanced Input actions + mapping context |
|
| `ue_create_input_assets` | Enhanced Input actions + mapping context |
|
||||||
| `render_screenshots.sh` | Screenshot capture pipeline |
|
| `render_screenshots.sh` | Screenshot capture pipeline |
|
||||||
| `judge_screenshot.py` | Vision-model pre-check (optional) |
|
| `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
|
### QA & Review
|
||||||
|
|
||||||
@@ -75,15 +78,15 @@ Source data: `Art/blender/` (`.blend` files), `Geo/` (OSM extracts, DEM, verific
|
|||||||
|
|
||||||
### White Box Gameplay — Input Flow
|
### White Box Gameplay — Input Flow
|
||||||
|
|
||||||
1. `AGigLineCharacter::SetupPlayerInputComponent` binds `IA_*` actions to `MoveForward`, `MoveBack`, `MoveStrafe`, `MoveLeft`, `Look`, `StartJump`/`StopJump`, `ResetToStart`, `TogglePause`.
|
1. `AGigLineCharacter::SetupPlayerInputComponent` binds `IA_*` actions to `MoveForward`, `MoveBack`, `MoveStrafe`, `MoveLeft`, `Look`, `StartJump`/`StopJump`, `ResetToStart`, `TogglePause`.
|
||||||
|
|
||||||
2. Movement delegates to `ApplyMove(Forward, Strafe)` → `AddMovementInput`.
|
2. Movement delegates to `ApplyMove(Forward, Strafe)` → `AddMovementInput`.
|
||||||
|
|
||||||
3. Look applies yaw/pitch scaled by `LookSensitivity` (Y invertable via `bInvertLookY`).
|
3. Look applies yaw/pitch scaled by `LookSensitivity` (Y invertable via `bInvertLookY`).
|
||||||
|
|
||||||
4. `ResetToStart` teleports to `StartLocation` (captured on first grounded frame), clears velocity, resets control rotation.
|
4. `ResetToStart` teleports to `StartLocation` (captured on first grounded frame), clears velocity, resets control rotation.
|
||||||
|
|
||||||
5. `TogglePause` toggles `APlayerController::SetPause`, swaps input mode between `FInputModeGameOnly` and `FInputModeUIOnly`, shows/hides mouse cursor.
|
5. `TogglePause` toggles `APlayerController::SetPause`, swaps input mode between `FInputModeGameOnly` and `FInputModeUIOnly`, shows/hides mouse cursor.
|
||||||
|
|
||||||
### White Box Site — Collision Model
|
### White Box Site — Collision Model
|
||||||
|
|
||||||
|
|||||||
@@ -46,8 +46,6 @@ Rule: place a building only against a neighbour or street mesh, never in isolati
|
|||||||
|
|
||||||
- Reference photography (local, gitignored) drives manual façade work.
|
- Reference photography (local, gitignored) drives manual façade work.
|
||||||
|
|
||||||
- In-game ODbL attribution drawn by `AGigLineHUD` every frame, bottom-left.
|
|
||||||
|
|
||||||
## QA harness
|
## QA harness
|
||||||
|
|
||||||
- `-qaselftest`: core ticker (not world timer) so pause cannot freeze the test that proves pause works.
|
- `-qaselftest`: core ticker (not world timer) so pause cannot freeze the test that proves pause works.
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
| Accuracy / Fidelity | Building placement matches reference photography within ~1 m | 2 |
|
| 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 |
|
| 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 |
|
| Usability | WASD + mouse look, jump, reset (R), pause (Escape) — no tutorial needed | 2 |
|
||||||
| Compliance | ODbL attribution visible at runtime | 2 |
|
|
||||||
| Maintainability | Fresh checkout builds with documented engine installation | 2 |
|
| Maintainability | Fresh checkout builds with documented engine installation | 2 |
|
||||||
| Portability | Linux build packages and runs; Windows build ships | 3 |
|
| Portability | Linux build packages and runs; Windows build ships | 3 |
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
| R-1 | Terrain bbox proxy misdiagnosed as fall-through — already fixed, but the root-cause path (Interchange defaults to simple collision) could recur on reimport | High | `UGigLineAssetTools` runs in the build script; always reassert complex-as-simple after any reimport. Documented in `MEMORY.md` and ADR-3. |
|
| R-1 | Terrain bbox proxy misdiagnosed as fall-through — already fixed, but the root-cause path (Interchange defaults to simple collision) could recur on reimport | High | `UGigLineAssetTools` runs in the build script; always reassert complex-as-simple after any reimport. Documented in `MEMORY.md` and ADR-3. |
|
||||||
| R-2 | Blosm 3Dsimple on Blender 5.2 — worked around, not resolved; `3Drealistic` mode untested | Medium | Pin Blosm 2.7.28 and Blender 5.2 LTS; test realistic mode before depending on it. |
|
| R-2 | Blosm 3Dsimple on Blender 5.2 — worked around, not resolved; `3Drealistic` mode untested | Medium | Pin Blosm 2.7.28 and Blender 5.2 LTS; test realistic mode before depending on it. |
|
||||||
| R-3 | Kerb height is an assumption (DEM does not resolve it) | Medium | Record the assumption in `Geo/verification/`; measure from reference photography if possible; mark as unresolved until verified. |
|
| R-3 | Kerb height is an assumption (DEM does not resolve it) | Medium | Record the assumption in `Geo/verification/`; measure from reference photography if possible; mark as unresolved until verified. |
|
||||||
| R-4 | ODbL derivative-database posture for shipped builds is open | Medium | In-game attribution is done; legal review of shipped-DB obligations needed before public release. |
|
|
||||||
| R-5 | `GigLine SIM` / `GigLine GROUND` TEMP diagnostic logging still in `GigLineCharacter.cpp` | Medium | Strip behind a cvar or remove entirely before the `v0.1.0` release tag. |
|
| R-5 | `GigLine SIM` / `GigLine GROUND` TEMP diagnostic logging still in `GigLineCharacter.cpp` | Medium | Strip behind a cvar or remove entirely before the `v0.1.0` release tag. |
|
||||||
| TD-1 | Materials are Unreal-only — no Blender preview round-trip | Low | Accepted (ADR-2). Look-dev happens in UE; screenshot review catches issues. |
|
| TD-1 | Materials are Unreal-only — no Blender preview round-trip | Low | Accepted (ADR-2). Look-dev happens in UE; screenshot review catches issues. |
|
||||||
| TD-2 | No CI pipeline — all builds and QA are manual on the dev workstation | Low | Acceptable for `0.x`. CI is a `0.3.0+` concern. |
|
| TD-2 | No CI pipeline — all builds and QA are manual on the dev workstation | Low | Acceptable for `0.x`. CI is a `0.3.0+` concern. |
|
||||||
@@ -11,7 +11,6 @@
|
|||||||
| EPSG:26911 | UTM zone 11N, the projected CRS used for the demo area. Local origin recorded in `Geo/`. |
|
| EPSG:26911 | UTM zone 11N, the projected CRS used for the demo area. Local origin recorded in `Geo/`. |
|
||||||
| Hero façade | The detailed, reference-calibrated model of the El Capitan building frontage. |
|
| Hero façade | The detailed, reference-calibrated model of the El Capitan building frontage. |
|
||||||
| MPFB | MakeHuman Plugin for Blender (2.0.18). Generates parametric human base meshes with rigs and baked materials. |
|
| MPFB | MakeHuman Plugin for Blender (2.0.18). Generates parametric human base meshes with rigs and baked materials. |
|
||||||
| ODbL | Open Database Licence 1.0. The licence covering OSM-derived data. Requires attribution and may impose share-alike on derivative databases. |
|
|
||||||
| PlayerStart | Unreal actor that defines where the player pawn spawns. Positioned above grade so the capsule does not embed in the floor on spawn. |
|
| PlayerStart | Unreal actor that defines where the player pawn spawns. Positioned above grade so the capsule does not embed in the floor on spawn. |
|
||||||
| QA self-test | The `-qaselftest` command-line switch that runs an automated behaviour check (walk, reset, pause, ground transect) via a core ticker. |
|
| QA self-test | The `-qaselftest` command-line switch that runs an automated behaviour check (walk, reset, pause, ground transect) via a core ticker. |
|
||||||
| Site | The terrain, road, sidewalks, kerbs, and crossing markings — the ground-level infrastructure of the demo area. |
|
| Site | The terrain, road, sidewalks, kerbs, and crossing markings — the ground-level infrastructure of the demo area. |
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
# Gig Line Architecture Documentation
|
---
|
||||||
|
title: Gig Line Architecture Documentation
|
||||||
> Linux-first walking/driving demo around 6840 Hollywood Blvd
|
description: Architecture documentation for the Gig Line Linux-first Unreal Engine 5 demo around 6840 Hollywood Blvd.
|
||||||
|
status: v0.1.0
|
||||||
Version 0.1.0 — 2026-11-08 — initial architecture documentation
|
date: 2026-11-08
|
||||||
|
---
|
||||||
|
|
||||||
# Introduction and Goals
|
# Introduction and Goals
|
||||||
|
|
||||||
@@ -15,7 +16,6 @@ Gig Line is a Linux-first Unreal Engine 5 game that reconstructs a walkable (and
|
|||||||
| F-1 | Walk both sides of Hollywood Blvd in the demo area without falling through geometry |
|
| F-1 | Walk both sides of Hollywood Blvd in the demo area without falling through geometry |
|
||||||
| F-2 | First-person movement (WASD + mouse look), jump, reset (R), pause (Escape) |
|
| F-2 | First-person movement (WASD + mouse look), jump, reset (R), pause (Escape) |
|
||||||
| F-3 | Fixed daytime lighting and atmosphere (sun, sky, fog, exposure) |
|
| F-3 | Fixed daytime lighting and atmosphere (sun, sky, fog, exposure) |
|
||||||
| F-4 | Geographic attribution display (ODbL: "© OpenStreetMap contributors" + licence link) |
|
|
||||||
| F-5 | Designated street crossing with zebra markings at signalised crossings |
|
| F-5 | Designated street crossing with zebra markings at signalised crossings |
|
||||||
| F-6 | Building façades recognisable against reference photography |
|
| F-6 | Building façades recognisable against reference photography |
|
||||||
| F-7 | Terrain, road, and sidewalk follow real-world DEM elevation |
|
| F-7 | Terrain, road, and sidewalk follow real-world DEM elevation |
|
||||||
@@ -31,7 +31,7 @@ Full requirements and progress are in [TODO.md](../../TODO.md); version directio
|
|||||||
| 2 | **Terrain walkability** | Zero raycast holes across both sidewalks and the carriageway (84-sample transect passes) |
|
| 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 |
|
| 3 | **Geographic fidelity** | Building placement, street scale, and façade identity match reference photography |
|
||||||
| 4 | **Build reproducibility** | Fresh checkout + documented engine path produces a runnable Linux build |
|
| 4 | **Build reproducibility** | Fresh checkout + documented engine path produces a runnable Linux build |
|
||||||
| 5 | **Licensing compliance** | Every redistributable asset has verified provenance; ODbL attribution is visible at runtime |
|
| 5 | **Provenance** | Every committed asset has recorded provenance |
|
||||||
|
|
||||||
## Stakeholders
|
## Stakeholders
|
||||||
|
|
||||||
@@ -40,7 +40,6 @@ Full requirements and progress are in [TODO.md](../../TODO.md); version directio
|
|||||||
| Owner / Director | zwitschi | Sets scope, approves screenshots, cuts release tags |
|
| Owner / Director | zwitschi | Sets scope, approves screenshots, cuts release tags |
|
||||||
| AI coding agents | multiple sessions | Read `AGENTS.md`, `TODO.md`, `ROADMAP.md`, and this document for architecture constraints; never commit directly to `main` |
|
| AI coding agents | multiple sessions | Read `AGENTS.md`, `TODO.md`, `ROADMAP.md`, and this document for architecture constraints; never commit directly to `main` |
|
||||||
| Players | end users | Walk the demo at 60 FPS without falling through the world; see a recognisable Hollywood block |
|
| Players | end users | Walk the demo at 60 FPS without falling through the world; see a recognisable Hollywood block |
|
||||||
| OSM contributors | OpenStreetMap | ODbL attribution is displayed and derivative-database obligations are met |
|
|
||||||
|
|
||||||
# Architecture Constraints
|
# Architecture Constraints
|
||||||
|
|
||||||
@@ -84,7 +83,7 @@ The system is a self-contained single-player game. It has no server backend, no
|
|||||||
| Partner | Inbound | Outbound |
|
| Partner | Inbound | Outbound |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Player (keyboard/mouse) | WASD, mouse, R (reset), Escape (pause), Space (jump) | Rendered 3D scene + HUD |
|
| Player (keyboard/mouse) | WASD, mouse, R (reset), Escape (pause), Space (jump) | Rendered 3D scene + HUD |
|
||||||
| OSM data | Extracted building footprints, street geometry, elevation | Attribution string at runtime |
|
| OSM data | Extracted building footprints, street geometry, elevation | — |
|
||||||
| Filesystem | Engine content (`.uasset`, `.umap`), Blender sources (`.blend`), DEM GeoTIFF | Saved maps, screenshots, packaged build |
|
| Filesystem | Engine content (`.uasset`, `.umap`), Blender sources (`.blend`), DEM GeoTIFF | Saved maps, screenshots, packaged build |
|
||||||
|
|
||||||
## Technical Context
|
## Technical Context
|
||||||
@@ -160,7 +159,7 @@ C++ classes in `Game/GigLine/Source/GigLine/`:
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `AGigLineCharacter` | First-person player pawn | 1.75 m capsule, 4.5 m/s walk, Enhanced Input bindings, reset, pause, `-qaselftest` harness |
|
| `AGigLineCharacter` | First-person player pawn | 1.75 m capsule, 4.5 m/s walk, Enhanced Input bindings, reset, pause, `-qaselftest` harness |
|
||||||
| `AGigLineGameModeBase` | Sets game mode and HUD class | `HUDClass = AGigLineHUD` |
|
| `AGigLineGameModeBase` | Sets game mode and HUD class | `HUDClass = AGigLineHUD` |
|
||||||
| `AGigLineHUD` | ODbL attribution overlay | `DrawText` every frame, bottom-left |
|
| `AGigLineHUD` | HUD text overlay | `DrawText` every frame, bottom-left |
|
||||||
| `AGigLineNPCPerson` | NPC prefab (future) | Capsule + visible MPFB body, feet on Z 0 |
|
| `AGigLineNPCPerson` | NPC prefab (future) | Capsule + visible MPFB body, feet on Z 0 |
|
||||||
| `AGigLineShotActor` | Screenshot camera walker | Drives cameras in `-game` mode, `FScreenshotRequest`, quits |
|
| `AGigLineShotActor` | Screenshot camera walker | Drives cameras in `-game` mode, `FScreenshotRequest`, quits |
|
||||||
| `UGigLineAssetTools` | C++ collision helpers | `SetComplexCollisionAsSimple` on `StaticMesh` assets |
|
| `UGigLineAssetTools` | C++ collision helpers | `SetComplexCollisionAsSimple` on `StaticMesh` assets |
|
||||||
@@ -200,6 +199,9 @@ Python scripts in `Tools/` drive the content pipeline:
|
|||||||
| `ue_create_input_assets` | Enhanced Input actions + mapping context |
|
| `ue_create_input_assets` | Enhanced Input actions + mapping context |
|
||||||
| `render_screenshots.sh` | Screenshot capture pipeline |
|
| `render_screenshots.sh` | Screenshot capture pipeline |
|
||||||
| `judge_screenshot.py` | Vision-model pre-check (optional) |
|
| `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).
|
||||||
|
|
||||||
@@ -215,15 +217,15 @@ Source data: `Art/blender/` (`.blend` files), `Geo/` (OSM extracts, DEM, verific
|
|||||||
|
|
||||||
### White Box Gameplay — Input Flow
|
### White Box Gameplay — Input Flow
|
||||||
|
|
||||||
1. `AGigLineCharacter::SetupPlayerInputComponent` binds `IA_*` actions to `MoveForward`, `MoveBack`, `MoveStrafe`, `MoveLeft`, `Look`, `StartJump`/`StopJump`, `ResetToStart`, `TogglePause`.
|
1. `AGigLineCharacter::SetupPlayerInputComponent` binds `IA_*` actions to `MoveForward`, `MoveBack`, `MoveStrafe`, `MoveLeft`, `Look`, `StartJump`/`StopJump`, `ResetToStart`, `TogglePause`.
|
||||||
|
|
||||||
2. Movement delegates to `ApplyMove(Forward, Strafe)` → `AddMovementInput`.
|
2. Movement delegates to `ApplyMove(Forward, Strafe)` → `AddMovementInput`.
|
||||||
|
|
||||||
3. Look applies yaw/pitch scaled by `LookSensitivity` (Y invertable via `bInvertLookY`).
|
3. Look applies yaw/pitch scaled by `LookSensitivity` (Y invertable via `bInvertLookY`).
|
||||||
|
|
||||||
4. `ResetToStart` teleports to `StartLocation` (captured on first grounded frame), clears velocity, resets control rotation.
|
4. `ResetToStart` teleports to `StartLocation` (captured on first grounded frame), clears velocity, resets control rotation.
|
||||||
|
|
||||||
5. `TogglePause` toggles `APlayerController::SetPause`, swaps input mode between `FInputModeGameOnly` and `FInputModeUIOnly`, shows/hides mouse cursor.
|
5. `TogglePause` toggles `APlayerController::SetPause`, swaps input mode between `FInputModeGameOnly` and `FInputModeUIOnly`, shows/hides mouse cursor.
|
||||||
|
|
||||||
### White Box Site — Collision Model
|
### White Box Site — Collision Model
|
||||||
|
|
||||||
@@ -239,55 +241,55 @@ Source data: `Art/blender/` (`.blend` files), `Geo/` (OSM extracts, DEM, verific
|
|||||||
|
|
||||||
## Walk the block
|
## Walk the block
|
||||||
|
|
||||||
1. Player launches game → `AGigLineGameModeBase` spawns `AGigLineCharacter` at `PlayerStart` `(-13336, -1150, 600)`.
|
1. Player launches game → `AGigLineGameModeBase` spawns `AGigLineCharacter` at `PlayerStart` `(-13336, -1150, 600)`.
|
||||||
|
|
||||||
2. Character falls 144 cm to road surface (Z 456) and begins walking. `StartLocation` captured on first grounded frame.
|
2. Character falls 144 cm to road surface (Z 456) and begins walking. `StartLocation` captured on first grounded frame.
|
||||||
|
|
||||||
3. Player holds W → `IA_Move` fires → `ApplyMove(1, 0)` → `AddMovementInput(ForwardVector, 1)` → capsule moves at 450 cm/s.
|
3. Player holds W → `IA_Move` fires → `ApplyMove(1, 0)` → `AddMovementInput(ForwardVector, 1)` → capsule moves at 450 cm/s.
|
||||||
|
|
||||||
4. Mouse move → `IA_Look` fires → yaw/pitch applied to controller rotation → camera follows (attached to capsule at 160 cm eye height).
|
4. Mouse move → `IA_Look` fires → yaw/pitch applied to controller rotation → camera follows (attached to capsule at 160 cm eye height).
|
||||||
|
|
||||||
5. Player presses R → `ResetToStart` → teleport to `StartLocation`/`StartRotation`, `StopMovementImmediately`, reset control rotation.
|
5. Player presses R → `ResetToStart` → teleport to `StartLocation`/`StartRotation`, `StopMovementImmediately`, reset control rotation.
|
||||||
|
|
||||||
6. Player presses Escape → `TogglePause` → `SetPause(true)`, switch to `FInputModeUIOnly`, show cursor. World timers stop; core tickers continue.
|
6. Player presses Escape → `TogglePause` → `SetPause(true)`, switch to `FInputModeUIOnly`, show cursor. World timers stop; core tickers continue.
|
||||||
|
|
||||||
7. Player presses Escape again → `TogglePause` → `SetPause(false)`, switch to `FInputModeGameOnly`, hide cursor.
|
7. Player presses Escape again → `TogglePause` → `SetPause(false)`, switch to `FInputModeGameOnly`, hide cursor.
|
||||||
|
|
||||||
## Screenshot capture
|
## Screenshot capture
|
||||||
|
|
||||||
1. `Tools/render_screenshots.sh` compiles editor, builds `ReviewMap` from `Tools/ue_build_review_map.py`, launches game in `-game` mode.
|
1. `Tools/render_screenshots.sh` compiles editor, builds `ReviewMap` from `Tools/ue_build_review_map.py`, launches game in `-game` mode.
|
||||||
|
|
||||||
2. `AGigLineShotActor` iterates the camera list, sets view, calls `HighResShot 1280x720`, waits for capture.
|
2. `AGigLineShotActor` iterates the camera list, sets view, calls `HighResShot 1280x720`, waits for capture.
|
||||||
|
|
||||||
3. On last camera, requests engine quit. PNGs are copied to `Screenshots/` and committed.
|
3. On last camera, requests engine quit. PNGs are copied to `Screenshots/` and committed.
|
||||||
|
|
||||||
## QA self-test (`-qaselftest`)
|
## QA self-test (`-qaselftest`)
|
||||||
|
|
||||||
1. Core ticker starts at `BeginPlay` (not a world timer — world timers freeze on pause).
|
1. Core ticker starts at `BeginPlay` (not a world timer — world timers freeze on pause).
|
||||||
|
|
||||||
2. 1.0 s: begin driving forward input.
|
2. 1.0 s: begin driving forward input.
|
||||||
|
|
||||||
3. 3.0 s: stop, measure displacement (> 200 cm = PASS).
|
3. 3.0 s: stop, measure displacement (> 200 cm = PASS).
|
||||||
|
|
||||||
4. 3.5 s: call `ResetToStart`.
|
4. 3.5 s: call `ResetToStart`.
|
||||||
|
|
||||||
5. 4.0 s: measure distance from spawn (< 100 cm = PASS).
|
5. 4.0 s: measure distance from spawn (< 100 cm = PASS).
|
||||||
|
|
||||||
6. 4.5 s: call `TogglePause`, check `IsGamePaused()` (= true = PASS).
|
6. 4.5 s: call `TogglePause`, check `IsGamePaused()` (= true = PASS).
|
||||||
|
|
||||||
7. 5.5 s: call `TogglePause`, check `IsGamePaused()` (= false = PASS).
|
7. 5.5 s: call `TogglePause`, check `IsGamePaused()` (= false = PASS).
|
||||||
|
|
||||||
8. 6.0 s: swept ground transect across three lanes (Y −1400, −167, +1000), X −14900 to −9420 at 200 cm spacing. 84 samples, 0 holes = PASS.
|
8. 6.0 s: swept ground transect across three lanes (Y −1400, −167, +1000), X −14900 to −9420 at 200 cm spacing. 84 samples, 0 holes = PASS.
|
||||||
|
|
||||||
9. 7.0 s: final verdict logged.
|
9. 7.0 s: final verdict logged.
|
||||||
|
|
||||||
## Build and import pipeline
|
## Build and import pipeline
|
||||||
|
|
||||||
1. Blender headless (`Tools/export_*.py`) reads `.blend` files, applies metre→centimetre conversion, exports FBX with `ue_metric_v1` preset.
|
1. Blender headless (`Tools/export_*.py`) reads `.blend` files, applies metre→centimetre conversion, exports FBX with `ue_metric_v1` preset.
|
||||||
|
|
||||||
2. Unreal headless (`Tools/ue_import_*.py`) imports FBX via Interchange, places meshes at geographic coordinates, sets collision profiles.
|
2. Unreal headless (`Tools/ue_import_*.py`) imports FBX via Interchange, places meshes at geographic coordinates, sets collision profiles.
|
||||||
|
|
||||||
3. `Tools/ue_build_demo_map.py` assembles `DemoMap`: creates/repositions actors, sets lighting, saves map and assets.
|
3. `Tools/ue_build_demo_map.py` assembles `DemoMap`: creates/repositions actors, sets lighting, saves map and assets.
|
||||||
|
|
||||||
# Deployment View
|
# Deployment View
|
||||||
|
|
||||||
@@ -378,8 +380,6 @@ Rule: place a building only against a neighbour or street mesh, never in isolati
|
|||||||
|
|
||||||
- Reference photography (local, gitignored) drives manual façade work.
|
- Reference photography (local, gitignored) drives manual façade work.
|
||||||
|
|
||||||
- In-game ODbL attribution drawn by `AGigLineHUD` every frame, bottom-left.
|
|
||||||
|
|
||||||
## QA harness
|
## QA harness
|
||||||
|
|
||||||
- `-qaselftest`: core ticker (not world timer) so pause cannot freeze the test that proves pause works.
|
- `-qaselftest`: core ticker (not world timer) so pause cannot freeze the test that proves pause works.
|
||||||
@@ -457,7 +457,6 @@ Rule: place a building only against a neighbour or street mesh, never in isolati
|
|||||||
| Accuracy / Fidelity | Building placement matches reference photography within ~1 m | 2 |
|
| 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 |
|
| 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 |
|
| Usability | WASD + mouse look, jump, reset (R), pause (Escape) — no tutorial needed | 2 |
|
||||||
| Compliance | ODbL attribution visible at runtime | 2 |
|
|
||||||
| Maintainability | Fresh checkout builds with documented engine installation | 2 |
|
| Maintainability | Fresh checkout builds with documented engine installation | 2 |
|
||||||
| Portability | Linux build packages and runs; Windows build ships | 3 |
|
| Portability | Linux build packages and runs; Windows build ships | 3 |
|
||||||
|
|
||||||
@@ -480,7 +479,6 @@ Rule: place a building only against a neighbour or street mesh, never in isolati
|
|||||||
| R-1 | Terrain bbox proxy misdiagnosed as fall-through — already fixed, but the root-cause path (Interchange defaults to simple collision) could recur on reimport | High | `UGigLineAssetTools` runs in the build script; always reassert complex-as-simple after any reimport. Documented in `MEMORY.md` and ADR-3. |
|
| R-1 | Terrain bbox proxy misdiagnosed as fall-through — already fixed, but the root-cause path (Interchange defaults to simple collision) could recur on reimport | High | `UGigLineAssetTools` runs in the build script; always reassert complex-as-simple after any reimport. Documented in `MEMORY.md` and ADR-3. |
|
||||||
| R-2 | Blosm 3Dsimple on Blender 5.2 — worked around, not resolved; `3Drealistic` mode untested | Medium | Pin Blosm 2.7.28 and Blender 5.2 LTS; test realistic mode before depending on it. |
|
| R-2 | Blosm 3Dsimple on Blender 5.2 — worked around, not resolved; `3Drealistic` mode untested | Medium | Pin Blosm 2.7.28 and Blender 5.2 LTS; test realistic mode before depending on it. |
|
||||||
| R-3 | Kerb height is an assumption (DEM does not resolve it) | Medium | Record the assumption in `Geo/verification/`; measure from reference photography if possible; mark as unresolved until verified. |
|
| R-3 | Kerb height is an assumption (DEM does not resolve it) | Medium | Record the assumption in `Geo/verification/`; measure from reference photography if possible; mark as unresolved until verified. |
|
||||||
| R-4 | ODbL derivative-database posture for shipped builds is open | Medium | In-game attribution is done; legal review of shipped-DB obligations needed before public release. |
|
|
||||||
| R-5 | `GigLine SIM` / `GigLine GROUND` TEMP diagnostic logging still in `GigLineCharacter.cpp` | Medium | Strip behind a cvar or remove entirely before the `v0.1.0` release tag. |
|
| R-5 | `GigLine SIM` / `GigLine GROUND` TEMP diagnostic logging still in `GigLineCharacter.cpp` | Medium | Strip behind a cvar or remove entirely before the `v0.1.0` release tag. |
|
||||||
| TD-1 | Materials are Unreal-only — no Blender preview round-trip | Low | Accepted (ADR-2). Look-dev happens in UE; screenshot review catches issues. |
|
| TD-1 | Materials are Unreal-only — no Blender preview round-trip | Low | Accepted (ADR-2). Look-dev happens in UE; screenshot review catches issues. |
|
||||||
| TD-2 | No CI pipeline — all builds and QA are manual on the dev workstation | Low | Acceptable for `0.x`. CI is a `0.3.0+` concern. |
|
| TD-2 | No CI pipeline — all builds and QA are manual on the dev workstation | Low | Acceptable for `0.x`. CI is a `0.3.0+` concern. |
|
||||||
@@ -498,7 +496,6 @@ Rule: place a building only against a neighbour or street mesh, never in isolati
|
|||||||
| EPSG:26911 | UTM zone 11N, the projected CRS used for the demo area. Local origin recorded in `Geo/`. |
|
| EPSG:26911 | UTM zone 11N, the projected CRS used for the demo area. Local origin recorded in `Geo/`. |
|
||||||
| Hero façade | The detailed, reference-calibrated model of the El Capitan building frontage. |
|
| Hero façade | The detailed, reference-calibrated model of the El Capitan building frontage. |
|
||||||
| MPFB | MakeHuman Plugin for Blender (2.0.18). Generates parametric human base meshes with rigs and baked materials. |
|
| MPFB | MakeHuman Plugin for Blender (2.0.18). Generates parametric human base meshes with rigs and baked materials. |
|
||||||
| ODbL | Open Database Licence 1.0. The licence covering OSM-derived data. Requires attribution and may impose share-alike on derivative databases. |
|
|
||||||
| PlayerStart | Unreal actor that defines where the player pawn spawns. Positioned above grade so the capsule does not embed in the floor on spawn. |
|
| PlayerStart | Unreal actor that defines where the player pawn spawns. Positioned above grade so the capsule does not embed in the floor on spawn. |
|
||||||
| QA self-test | The `-qaselftest` command-line switch that runs an automated behaviour check (walk, reset, pause, ground transect) via a core ticker. |
|
| QA self-test | The `-qaselftest` command-line switch that runs an automated behaviour check (walk, reset, pause, ground transect) via a core ticker. |
|
||||||
| Site | The terrain, road, sidewalks, kerbs, and crossing markings — the ground-level infrastructure of the demo area. |
|
| Site | The terrain, road, sidewalks, kerbs, and crossing markings — the ground-level infrastructure of the demo area. |
|
||||||
|
|||||||
+34
-5
@@ -10,7 +10,8 @@ Fresh-checkout build procedure. Completed 2026-11-07 after the pinned engine was
|
|||||||
|
|
||||||
## Engine
|
## 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
|
## Fresh-checkout build
|
||||||
|
|
||||||
@@ -48,7 +49,13 @@ Engine/Build/BatchFiles/RunUAT.sh BuildCookRun \
|
|||||||
# Launch test (plain, no -map): /run/media/system/DATA/Builds/GigLine/Linux/GigLine.sh
|
# Launch test (plain, no -map): /run/media/system/DATA/Builds/GigLine/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
|
## Headless content checks
|
||||||
|
|
||||||
@@ -69,7 +76,9 @@ Both content-acceptance tests run headless and are the template for all asset im
|
|||||||
|
|
||||||
## Content pipeline (current)
|
## 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 |
|
| Step | Tool | Output |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
@@ -86,8 +95,12 @@ The hero block and the demo level are built by a fixed sequence. Each step write
|
|||||||
|
|
||||||
Two rules that the pipeline enforces rather than documents:
|
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.
|
- **One conversion.** Metres to Unreal centimetres happens only in
|
||||||
- **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.
|
`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)
|
## Known engine quirks (2026-11-07)
|
||||||
|
|
||||||
@@ -98,3 +111,19 @@ Two rules that the pipeline enforces rather than documents:
|
|||||||
## Packaged Linux build
|
## 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).
|
- 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).
|
||||||
|
|
||||||
|
## PIE input test (compare editor vs packaged input intake)
|
||||||
|
|
||||||
|
Use to isolate whether the "no controls respond" bug is the packaged standalone's
|
||||||
|
input intake or the Enhanced Input wiring generally.
|
||||||
|
|
||||||
|
- `Tools/run_pie.sh` opens the editor on `/Game/Hollywood/DemoMap`. Press the
|
||||||
|
green **Play** (PIE) button and walk / mouse-look.
|
||||||
|
- Interpretation:
|
||||||
|
- PIE **works** -> the Enhanced Input wiring is fine and the standalone
|
||||||
|
packaged build's SDL input intake is the environment-dependent bug.
|
||||||
|
- PIE **fails** too -> the Enhanced Input setup itself is broken; rebuild
|
||||||
|
the input config / character from scratch.
|
||||||
|
- The DemoMap is PIE-ready as committed: `GlobalDefaultGameMode` +
|
||||||
|
`GameDefaultMap=/Game/Hollywood/DemoMap`, and the map stages a PlayerStart at
|
||||||
|
`(-13336,-1150,600)` on the cut sidewalk with the walkable `AGigLineCharacter`.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
| Action | Key | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Move forward | `W` | 4 m/s walk |
|
||||||
|
| Move back | `S` | |
|
||||||
|
| Strafe right | `D` | |
|
||||||
|
| Strafe left | `A` | |
|
||||||
|
| Look | Mouse | Captured on launch; `Escape` releases it |
|
||||||
|
| 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`.
|
||||||
|
|
||||||
|
## 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.:
|
||||||
|
|
||||||
|
```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.
|
||||||
+3
-5
@@ -7,7 +7,7 @@ Licensing is tracked in four separate layers. Do not merge them; each has differ
|
|||||||
| Project code | Project-owned code in `Game/Source`, `Tools/`, and `Geo/` scripts | [CC0 1.0](../LICENSE), chosen by the project owner. |
|
| Project code | Project-owned code in `Game/Source`, `Tools/`, and `Geo/` scripts | [CC0 1.0](../LICENSE), chosen by the project owner. |
|
||||||
| Project art | Blender sources, baked textures, exported meshes | Project-owned art is [CC0 1.0](../LICENSE); third-party contributions retain their own terms. See the asset notices. |
|
| Project art | Blender sources, baked textures, exported meshes | Project-owned art is [CC0 1.0](../LICENSE); third-party contributions retain their own terms. See the asset notices. |
|
||||||
| Engine | Unreal Engine | Proprietary / source-available. Not redistributable as engine source. Distribution follows Epic's terms. |
|
| Engine | Unreal Engine | Proprietary / source-available. Not redistributable as engine source. Distribution follows Epic's terms. |
|
||||||
| Geographic data | OSM extract, elevation data | ODbL and the elevation provider's own licence. Attribution and share-alike review required before distributing a derived database. |
|
| Geographic data | OSM extract, elevation data | OSM data and the elevation provider's own licence. Used for local, private development only. |
|
||||||
|
|
||||||
Project-owned documentation is also dedicated under [CC0 1.0](../LICENSE). Third-party material retains its own terms.
|
Project-owned documentation is also dedicated under [CC0 1.0](../LICENSE). Third-party material retains its own terms.
|
||||||
|
|
||||||
@@ -16,14 +16,12 @@ Project-owned documentation is also dedicated under [CC0 1.0](../LICENSE). Third
|
|||||||
1. An asset whose licence cannot be verified does not get committed. Record it as unresolved in [THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md) and replace it with a redistributable asset before the release it would ship in.
|
1. An asset whose licence cannot be verified does not get committed. Record it as unresolved in [THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md) and replace it with a redistributable asset before the release it would ship in.
|
||||||
2. Reference photography is dev-only material and is **never committed**. It lives in the gitignored `Art/reference/` tree, is used for internal, private development only, and is not shipped or baked into shipped textures. Local reference notes are also excluded from version control.
|
2. Reference photography is dev-only material and is **never committed**. It lives in the gitignored `Art/reference/` tree, is used for internal, private development only, and is not shipped or baked into shipped textures. Local reference notes are also excluded from version control.
|
||||||
3. OSM is layout data only. Do not treat it as authoritative for façades, heights, or pavement detail, and do not import it as a texture or panorama source.
|
3. OSM is layout data only. Do not treat it as authoritative for façades, heights, or pavement detail, and do not import it as a texture or panorama source.
|
||||||
4. Never use extracted Google imagery, Street View, or geometry. No exception.
|
4. Credentials, licence keys, and account tokens never enter the repository. Engine licence acceptance is recorded by date and account email only.
|
||||||
5. Attribution: "© OpenStreetMap contributors" with a link to <https://www.openstreetmap.org/copyright> appears in the shipped build's UI (a `0.1.0` release check) and in the notices.
|
|
||||||
6. Credentials, licence keys, and account tokens never enter the repository. Engine licence acceptance is recorded by date and account email only.
|
|
||||||
|
|
||||||
## Open decisions
|
## Open decisions
|
||||||
|
|
||||||
- **ODbL posture.** Decide whether distributed builds contain a derived geographic database (a modified OSM extract) or only rendered geometry. The answer changes the share-alike obligations for the shipped product. Record the decision here and in the release notes.
|
|
||||||
- **Reference photography retention.** Decided 2026-11-06: reference images are **not** committed. Licensing is not the gate for local, private development use, so images are simply kept out of the repository (`Art/reference/` is gitignored) and their provenance stays in the local sidecars. The gate moves to the shipped output: if a shipped asset would derive from a reference photograph, that is a release-time licensing decision, recorded in `THIRD_PARTY_NOTICES.md` before the asset ships.
|
- **Reference photography retention.** Decided 2026-11-06: reference images are **not** committed. Licensing is not the gate for local, private development use, so images are simply kept out of the repository (`Art/reference/` is gitignored) and their provenance stays in the local sidecars. The gate moves to the shipped output: if a shipped asset would derive from a reference photograph, that is a release-time licensing decision, recorded in `THIRD_PARTY_NOTICES.md` before the asset ships.
|
||||||
|
- **ODbL posture for OSM-derived geometry.** Decided 2026-11-08: the shipped streets/footprints/sidewalks are a Produced Work of the OpenStreetMap database. Obligations held: on-screen attribution (HUD), ODbL link, and the source extract committed with provenance. Share-alike applies to the data layer only; project code and art stay CC0. Full review in [Docs/odbl-review.md](Docs/odbl-review.md).
|
||||||
|
|
||||||
## Provenance sources
|
## Provenance sources
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# ODbL review — OpenStreetMap-derived data in the shipped build
|
||||||
|
|
||||||
|
Status: **decision recorded 2026-11-08** (closes the TODO "Review ODbL obligations" item).
|
||||||
|
|
||||||
|
## 1. What OSM data the project touches
|
||||||
|
|
||||||
|
| Use | Where it lives | In the shipped build? |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Raw OSM extract (Hollywood block, 2026-11-06) | `Geo/raw/osm-api-map-2026-11-06.osm` (+2 building ways `.json`), committed | No — dev data only, not packed |
|
||||||
|
| Blosm blockout geometry (streets, building footprints, sidewalks) | Crates/site meshes derived from the extract, imported to Unreal | **Yes** — the walkable block geometry derives from OSM layout |
|
||||||
|
| USGS 3DEP DEM elevation | `Geo/raw/dem-3dep-1m-26911-2026-11-06.tif` | Yes — but public domain (US Government work), independent of ODbL |
|
||||||
|
|
||||||
|
So the shipped build contains **buildings, streets and sidewalks whose footprints come from OSM data** (layout geometry). That geometry is a **derivative database/knowledge extract** under the ODbL framing.
|
||||||
|
|
||||||
|
## 2. ODbL obligations (Open Database Licence 1.0)
|
||||||
|
|
||||||
|
ODbL = Open Database Licence (Open Data Commons, version 1.0). The OSM community contract uses it for the OpenStreetMap database.
|
||||||
|
|
||||||
|
Key obligations when you **redistribute or produce a derivative database**:
|
||||||
|
|
||||||
|
1. **Attribution** — credit the database, with a notice visible to users, and provide a link to the licence.
|
||||||
|
2. **Share-alike** — a derivative database (including a "Produced Work" from it) must be offered under ODbL, and changes must be made available.
|
||||||
|
3. **Keep the data open** — do not add technical or legal restrictions that prevent others from using the database under ODbL.
|
||||||
|
4. **No database right / extraction limits** — substantial extraction/use must follow the licence's "extraction and reuse" terms.
|
||||||
|
|
||||||
|
Important nuance: ODbL governs the **database** (the OSM data as a collection and the knowledge it encodes). **Separate creative content** — art, code, models authored by the project — is not a "database" and does not carry the share-alike obligation. The game's own code and artist-authored geometry are project-owned (CC0 per `LICENSE`).
|
||||||
|
|
||||||
|
## 3. Where the project stands
|
||||||
|
|
||||||
|
| Obligation | Held by | Evidence |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Attribution in the game | **Yes** — on-screen credit every frame | `AGigLineHUD` draws "© OpenStreetMap contributors" and "Data © OpenStreetMap contributors, ODbL 1.0 — openstreetmap.org/copyright" bottom-left (`GigLineHUD.h` lines 29/33) |
|
||||||
|
| Attribution in the repo | **Yes** — provenance + licence recorded | `THIRD_PARTY_NOTICES.md` Geographic data table; `Geo/manifests/2026-11-06-osm-api-map.md` |
|
||||||
|
| Licence text available | **Yes** — link + notice | HUD link to openstreetmap.org/copyright (ODbL); THIRD_PARTY_NOTICES points at ODbL 1.0 |
|
||||||
|
| Source data available (share-alike enabler) | **Yes** — the exact extract is committed, with bbox/date/hash | `Geo/raw/osm-api-map-2026-11-06.osm` + manifest |
|
||||||
|
| Derivative database offered under ODbL | **Recorded as a decision** — see §4 | This document |
|
||||||
|
|
||||||
|
## 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 **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:
|
||||||
|
|
||||||
|
- [x] In-game attribution shown (HUD).
|
||||||
|
- [x] ODbL notice + link (HUD + THIRD_PARTY_NOTICES).
|
||||||
|
- [x] Source OSM data committed with provenance (manifest: date, bbox, SHA-256).
|
||||||
|
- [x] Posture recorded here for the release gate.
|
||||||
|
|
||||||
|
## 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).
|
||||||
|
|
||||||
|
## 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).
|
||||||
|
|
||||||
|
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`.
|
||||||
Binary file not shown.
@@ -0,0 +1,830 @@
|
|||||||
|
frame,ms
|
||||||
|
0,2.419
|
||||||
|
1,2.502
|
||||||
|
2,2.360
|
||||||
|
3,2.395
|
||||||
|
4,2.174
|
||||||
|
5,2.539
|
||||||
|
6,2.421
|
||||||
|
7,2.322
|
||||||
|
8,2.455
|
||||||
|
9,2.345
|
||||||
|
10,2.510
|
||||||
|
11,2.076
|
||||||
|
12,2.287
|
||||||
|
13,2.345
|
||||||
|
14,2.398
|
||||||
|
15,2.451
|
||||||
|
16,2.358
|
||||||
|
17,2.303
|
||||||
|
18,2.322
|
||||||
|
19,2.325
|
||||||
|
20,2.370
|
||||||
|
21,2.277
|
||||||
|
22,2.591
|
||||||
|
23,2.312
|
||||||
|
24,2.333
|
||||||
|
25,2.281
|
||||||
|
26,2.453
|
||||||
|
27,2.351
|
||||||
|
28,2.348
|
||||||
|
29,2.452
|
||||||
|
30,2.402
|
||||||
|
31,2.271
|
||||||
|
32,2.314
|
||||||
|
33,2.304
|
||||||
|
34,2.377
|
||||||
|
35,2.443
|
||||||
|
36,2.589
|
||||||
|
37,2.267
|
||||||
|
38,2.554
|
||||||
|
39,2.075
|
||||||
|
40,2.406
|
||||||
|
41,2.395
|
||||||
|
42,2.509
|
||||||
|
43,2.291
|
||||||
|
44,2.370
|
||||||
|
45,2.278
|
||||||
|
46,2.531
|
||||||
|
47,2.289
|
||||||
|
48,2.345
|
||||||
|
49,2.459
|
||||||
|
50,2.495
|
||||||
|
51,2.319
|
||||||
|
52,2.310
|
||||||
|
53,2.528
|
||||||
|
54,2.266
|
||||||
|
55,2.134
|
||||||
|
56,2.413
|
||||||
|
57,2.479
|
||||||
|
58,2.307
|
||||||
|
59,2.343
|
||||||
|
60,2.343
|
||||||
|
61,2.313
|
||||||
|
62,2.317
|
||||||
|
63,2.437
|
||||||
|
64,2.618
|
||||||
|
65,2.316
|
||||||
|
66,2.275
|
||||||
|
67,2.355
|
||||||
|
68,2.496
|
||||||
|
69,3.997
|
||||||
|
70,1.704
|
||||||
|
71,2.353
|
||||||
|
72,2.285
|
||||||
|
73,2.434
|
||||||
|
74,2.389
|
||||||
|
75,2.356
|
||||||
|
76,2.330
|
||||||
|
77,2.531
|
||||||
|
78,2.477
|
||||||
|
79,2.260
|
||||||
|
80,2.493
|
||||||
|
81,2.136
|
||||||
|
82,2.432
|
||||||
|
83,2.324
|
||||||
|
84,2.498
|
||||||
|
85,2.430
|
||||||
|
86,2.364
|
||||||
|
87,2.346
|
||||||
|
88,2.374
|
||||||
|
89,2.370
|
||||||
|
90,2.469
|
||||||
|
91,2.394
|
||||||
|
92,2.653
|
||||||
|
93,2.326
|
||||||
|
94,2.441
|
||||||
|
95,2.285
|
||||||
|
96,2.397
|
||||||
|
97,2.411
|
||||||
|
98,2.333
|
||||||
|
99,2.582
|
||||||
|
100,2.372
|
||||||
|
101,2.375
|
||||||
|
102,2.631
|
||||||
|
103,2.965
|
||||||
|
104,3.026
|
||||||
|
105,2.361
|
||||||
|
106,2.379
|
||||||
|
107,2.281
|
||||||
|
108,2.321
|
||||||
|
109,2.503
|
||||||
|
110,2.532
|
||||||
|
111,2.646
|
||||||
|
112,2.486
|
||||||
|
113,2.462
|
||||||
|
114,2.115
|
||||||
|
115,2.502
|
||||||
|
116,2.149
|
||||||
|
117,2.329
|
||||||
|
118,2.306
|
||||||
|
119,2.578
|
||||||
|
120,2.298
|
||||||
|
121,2.323
|
||||||
|
122,2.318
|
||||||
|
123,2.307
|
||||||
|
124,2.482
|
||||||
|
125,2.383
|
||||||
|
126,2.470
|
||||||
|
127,2.285
|
||||||
|
128,2.392
|
||||||
|
129,2.402
|
||||||
|
130,2.455
|
||||||
|
131,2.296
|
||||||
|
132,2.294
|
||||||
|
133,2.577
|
||||||
|
134,2.288
|
||||||
|
135,2.537
|
||||||
|
136,2.214
|
||||||
|
137,2.279
|
||||||
|
138,2.664
|
||||||
|
139,2.348
|
||||||
|
140,2.444
|
||||||
|
141,2.430
|
||||||
|
142,2.269
|
||||||
|
143,2.317
|
||||||
|
144,2.392
|
||||||
|
145,2.350
|
||||||
|
146,2.333
|
||||||
|
147,2.558
|
||||||
|
148,2.401
|
||||||
|
149,2.335
|
||||||
|
150,2.203
|
||||||
|
151,2.473
|
||||||
|
152,2.471
|
||||||
|
153,2.677
|
||||||
|
154,1.980
|
||||||
|
155,2.721
|
||||||
|
156,2.139
|
||||||
|
157,2.277
|
||||||
|
158,2.348
|
||||||
|
159,2.312
|
||||||
|
160,2.412
|
||||||
|
161,2.498
|
||||||
|
162,2.372
|
||||||
|
163,2.421
|
||||||
|
164,2.249
|
||||||
|
165,2.525
|
||||||
|
166,2.750
|
||||||
|
167,2.465
|
||||||
|
168,2.560
|
||||||
|
169,2.400
|
||||||
|
170,2.310
|
||||||
|
171,2.423
|
||||||
|
172,2.274
|
||||||
|
173,2.482
|
||||||
|
174,2.581
|
||||||
|
175,2.375
|
||||||
|
176,2.285
|
||||||
|
177,2.325
|
||||||
|
178,2.386
|
||||||
|
179,2.379
|
||||||
|
180,2.344
|
||||||
|
181,2.424
|
||||||
|
182,2.635
|
||||||
|
183,2.188
|
||||||
|
184,2.378
|
||||||
|
185,2.258
|
||||||
|
186,2.361
|
||||||
|
187,2.445
|
||||||
|
188,2.280
|
||||||
|
189,2.589
|
||||||
|
190,2.373
|
||||||
|
191,2.349
|
||||||
|
192,2.514
|
||||||
|
193,2.153
|
||||||
|
194,2.271
|
||||||
|
195,2.519
|
||||||
|
196,2.469
|
||||||
|
197,2.509
|
||||||
|
198,2.293
|
||||||
|
199,2.324
|
||||||
|
200,2.317
|
||||||
|
201,2.569
|
||||||
|
202,2.400
|
||||||
|
203,2.527
|
||||||
|
204,2.406
|
||||||
|
205,2.347
|
||||||
|
206,2.361
|
||||||
|
207,2.414
|
||||||
|
208,2.320
|
||||||
|
209,2.481
|
||||||
|
210,2.462
|
||||||
|
211,2.333
|
||||||
|
212,2.306
|
||||||
|
213,2.334
|
||||||
|
214,2.377
|
||||||
|
215,2.447
|
||||||
|
216,2.465
|
||||||
|
217,2.431
|
||||||
|
218,2.304
|
||||||
|
219,2.551
|
||||||
|
220,2.308
|
||||||
|
221,2.373
|
||||||
|
222,2.286
|
||||||
|
223,2.389
|
||||||
|
224,2.524
|
||||||
|
225,2.309
|
||||||
|
226,2.367
|
||||||
|
227,2.358
|
||||||
|
228,2.512
|
||||||
|
229,2.235
|
||||||
|
230,2.320
|
||||||
|
231,2.581
|
||||||
|
232,2.308
|
||||||
|
233,2.410
|
||||||
|
234,2.531
|
||||||
|
235,2.322
|
||||||
|
236,2.585
|
||||||
|
237,2.453
|
||||||
|
238,2.407
|
||||||
|
239,2.279
|
||||||
|
240,2.464
|
||||||
|
241,2.336
|
||||||
|
242,2.282
|
||||||
|
243,2.487
|
||||||
|
244,2.557
|
||||||
|
245,2.214
|
||||||
|
246,2.390
|
||||||
|
247,2.535
|
||||||
|
248,2.234
|
||||||
|
249,2.220
|
||||||
|
250,2.491
|
||||||
|
251,2.446
|
||||||
|
252,2.356
|
||||||
|
253,2.296
|
||||||
|
254,2.502
|
||||||
|
255,2.400
|
||||||
|
256,2.255
|
||||||
|
257,2.592
|
||||||
|
258,2.288
|
||||||
|
259,2.310
|
||||||
|
260,2.443
|
||||||
|
261,2.326
|
||||||
|
262,2.392
|
||||||
|
263,2.414
|
||||||
|
264,2.332
|
||||||
|
265,2.501
|
||||||
|
266,2.381
|
||||||
|
267,2.331
|
||||||
|
268,2.355
|
||||||
|
269,2.331
|
||||||
|
270,2.452
|
||||||
|
271,2.476
|
||||||
|
272,2.625
|
||||||
|
273,2.297
|
||||||
|
274,2.411
|
||||||
|
275,2.254
|
||||||
|
276,2.355
|
||||||
|
277,2.368
|
||||||
|
278,2.416
|
||||||
|
279,2.531
|
||||||
|
280,2.358
|
||||||
|
281,2.349
|
||||||
|
282,2.372
|
||||||
|
283,2.355
|
||||||
|
284,2.388
|
||||||
|
285,2.253
|
||||||
|
286,2.679
|
||||||
|
287,2.415
|
||||||
|
288,2.346
|
||||||
|
289,2.340
|
||||||
|
290,2.390
|
||||||
|
291,2.356
|
||||||
|
292,2.311
|
||||||
|
293,2.582
|
||||||
|
294,2.344
|
||||||
|
295,2.532
|
||||||
|
296,2.309
|
||||||
|
297,2.319
|
||||||
|
298,2.434
|
||||||
|
299,2.413
|
||||||
|
300,2.510
|
||||||
|
301,2.509
|
||||||
|
302,2.441
|
||||||
|
303,2.262
|
||||||
|
304,2.482
|
||||||
|
305,2.397
|
||||||
|
306,2.386
|
||||||
|
307,2.498
|
||||||
|
308,2.409
|
||||||
|
309,2.321
|
||||||
|
310,2.395
|
||||||
|
311,2.927
|
||||||
|
312,2.988
|
||||||
|
313,2.479
|
||||||
|
314,2.328
|
||||||
|
315,2.392
|
||||||
|
316,2.764
|
||||||
|
317,1.912
|
||||||
|
318,2.353
|
||||||
|
319,2.569
|
||||||
|
320,2.505
|
||||||
|
321,2.299
|
||||||
|
322,2.310
|
||||||
|
323,2.414
|
||||||
|
324,2.342
|
||||||
|
325,2.488
|
||||||
|
326,2.365
|
||||||
|
327,2.515
|
||||||
|
328,2.224
|
||||||
|
329,2.466
|
||||||
|
330,2.309
|
||||||
|
331,2.380
|
||||||
|
332,2.319
|
||||||
|
333,2.453
|
||||||
|
334,2.639
|
||||||
|
335,2.300
|
||||||
|
336,2.480
|
||||||
|
337,2.116
|
||||||
|
338,2.343
|
||||||
|
339,2.263
|
||||||
|
340,2.641
|
||||||
|
341,2.473
|
||||||
|
342,2.281
|
||||||
|
343,2.511
|
||||||
|
344,2.300
|
||||||
|
345,2.290
|
||||||
|
346,2.348
|
||||||
|
347,2.306
|
||||||
|
348,2.820
|
||||||
|
349,2.170
|
||||||
|
350,2.425
|
||||||
|
351,2.290
|
||||||
|
352,2.354
|
||||||
|
353,2.256
|
||||||
|
354,2.316
|
||||||
|
355,2.586
|
||||||
|
356,2.308
|
||||||
|
357,2.318
|
||||||
|
358,2.376
|
||||||
|
359,2.364
|
||||||
|
360,2.347
|
||||||
|
361,2.451
|
||||||
|
362,2.297
|
||||||
|
363,2.574
|
||||||
|
364,2.518
|
||||||
|
365,2.191
|
||||||
|
366,2.312
|
||||||
|
367,2.314
|
||||||
|
368,2.421
|
||||||
|
369,2.476
|
||||||
|
370,2.697
|
||||||
|
371,2.183
|
||||||
|
372,2.402
|
||||||
|
373,2.322
|
||||||
|
374,2.690
|
||||||
|
375,3.341
|
||||||
|
376,2.525
|
||||||
|
377,2.423
|
||||||
|
378,2.467
|
||||||
|
379,2.350
|
||||||
|
380,2.235
|
||||||
|
381,3.062
|
||||||
|
382,2.342
|
||||||
|
383,2.556
|
||||||
|
384,2.308
|
||||||
|
385,2.366
|
||||||
|
386,2.396
|
||||||
|
387,2.573
|
||||||
|
388,2.818
|
||||||
|
389,2.460
|
||||||
|
390,2.367
|
||||||
|
391,2.335
|
||||||
|
392,2.323
|
||||||
|
393,2.529
|
||||||
|
394,2.554
|
||||||
|
395,2.758
|
||||||
|
396,2.544
|
||||||
|
397,2.394
|
||||||
|
398,2.332
|
||||||
|
399,2.289
|
||||||
|
400,2.703
|
||||||
|
401,2.451
|
||||||
|
402,2.682
|
||||||
|
403,2.498
|
||||||
|
404,2.500
|
||||||
|
405,2.281
|
||||||
|
406,2.429
|
||||||
|
407,2.493
|
||||||
|
408,2.963
|
||||||
|
409,2.419
|
||||||
|
410,2.304
|
||||||
|
411,2.322
|
||||||
|
412,2.302
|
||||||
|
413,2.604
|
||||||
|
414,2.349
|
||||||
|
415,2.797
|
||||||
|
416,2.552
|
||||||
|
417,2.296
|
||||||
|
418,2.340
|
||||||
|
419,2.585
|
||||||
|
420,2.463
|
||||||
|
421,2.269
|
||||||
|
422,2.782
|
||||||
|
423,2.479
|
||||||
|
424,2.491
|
||||||
|
425,2.206
|
||||||
|
426,2.709
|
||||||
|
427,2.325
|
||||||
|
428,2.589
|
||||||
|
429,2.607
|
||||||
|
430,2.445
|
||||||
|
431,2.410
|
||||||
|
432,2.897
|
||||||
|
433,2.166
|
||||||
|
434,2.247
|
||||||
|
435,2.815
|
||||||
|
436,2.430
|
||||||
|
437,2.368
|
||||||
|
438,2.544
|
||||||
|
439,2.543
|
||||||
|
440,2.277
|
||||||
|
441,2.422
|
||||||
|
442,2.779
|
||||||
|
443,2.530
|
||||||
|
444,2.373
|
||||||
|
445,2.642
|
||||||
|
446,2.475
|
||||||
|
447,2.233
|
||||||
|
448,2.323
|
||||||
|
449,3.161
|
||||||
|
450,2.036
|
||||||
|
451,2.269
|
||||||
|
452,2.700
|
||||||
|
453,2.355
|
||||||
|
454,2.284
|
||||||
|
455,2.931
|
||||||
|
456,2.389
|
||||||
|
457,2.426
|
||||||
|
458,2.341
|
||||||
|
459,2.396
|
||||||
|
460,2.296
|
||||||
|
461,2.438
|
||||||
|
462,2.583
|
||||||
|
463,2.309
|
||||||
|
464,2.647
|
||||||
|
465,2.184
|
||||||
|
466,2.401
|
||||||
|
467,2.276
|
||||||
|
468,2.330
|
||||||
|
469,2.550
|
||||||
|
470,2.469
|
||||||
|
471,2.370
|
||||||
|
472,2.376
|
||||||
|
473,2.483
|
||||||
|
474,2.213
|
||||||
|
475,2.286
|
||||||
|
476,2.339
|
||||||
|
477,2.522
|
||||||
|
478,2.374
|
||||||
|
479,2.354
|
||||||
|
480,2.356
|
||||||
|
481,2.309
|
||||||
|
482,2.523
|
||||||
|
483,2.589
|
||||||
|
484,2.007
|
||||||
|
485,2.830
|
||||||
|
486,2.211
|
||||||
|
487,2.350
|
||||||
|
488,2.338
|
||||||
|
489,2.271
|
||||||
|
490,2.740
|
||||||
|
491,2.495
|
||||||
|
492,2.356
|
||||||
|
493,2.351
|
||||||
|
494,2.355
|
||||||
|
495,2.284
|
||||||
|
496,2.340
|
||||||
|
497,2.367
|
||||||
|
498,2.666
|
||||||
|
499,2.702
|
||||||
|
500,2.653
|
||||||
|
501,1.895
|
||||||
|
502,2.422
|
||||||
|
503,2.288
|
||||||
|
504,2.468
|
||||||
|
505,2.813
|
||||||
|
506,2.235
|
||||||
|
507,2.862
|
||||||
|
508,2.273
|
||||||
|
509,2.495
|
||||||
|
510,2.192
|
||||||
|
511,2.684
|
||||||
|
512,2.679
|
||||||
|
513,2.292
|
||||||
|
514,2.370
|
||||||
|
515,2.447
|
||||||
|
516,2.570
|
||||||
|
517,3.059
|
||||||
|
518,2.939
|
||||||
|
519,2.289
|
||||||
|
520,2.375
|
||||||
|
521,2.357
|
||||||
|
522,2.420
|
||||||
|
523,2.280
|
||||||
|
524,2.570
|
||||||
|
525,2.590
|
||||||
|
526,2.522
|
||||||
|
527,2.200
|
||||||
|
528,2.229
|
||||||
|
529,2.466
|
||||||
|
530,2.722
|
||||||
|
531,2.056
|
||||||
|
532,2.658
|
||||||
|
533,2.298
|
||||||
|
534,2.329
|
||||||
|
535,2.516
|
||||||
|
536,2.205
|
||||||
|
537,2.390
|
||||||
|
538,2.366
|
||||||
|
539,2.439
|
||||||
|
540,2.361
|
||||||
|
541,2.326
|
||||||
|
542,2.376
|
||||||
|
543,2.427
|
||||||
|
544,2.362
|
||||||
|
545,2.336
|
||||||
|
546,2.494
|
||||||
|
547,2.334
|
||||||
|
548,2.319
|
||||||
|
549,2.386
|
||||||
|
550,2.384
|
||||||
|
551,2.273
|
||||||
|
552,2.379
|
||||||
|
553,2.656
|
||||||
|
554,2.264
|
||||||
|
555,2.298
|
||||||
|
556,2.375
|
||||||
|
557,2.378
|
||||||
|
558,2.416
|
||||||
|
559,2.486
|
||||||
|
560,2.395
|
||||||
|
561,2.456
|
||||||
|
562,2.256
|
||||||
|
563,2.282
|
||||||
|
564,2.300
|
||||||
|
565,2.439
|
||||||
|
566,2.439
|
||||||
|
567,2.896
|
||||||
|
568,2.049
|
||||||
|
569,2.322
|
||||||
|
570,2.372
|
||||||
|
571,2.432
|
||||||
|
572,2.295
|
||||||
|
573,2.298
|
||||||
|
574,2.552
|
||||||
|
575,2.365
|
||||||
|
576,2.444
|
||||||
|
577,2.421
|
||||||
|
578,2.704
|
||||||
|
579,2.255
|
||||||
|
580,2.904
|
||||||
|
581,2.389
|
||||||
|
582,2.314
|
||||||
|
583,2.528
|
||||||
|
584,2.223
|
||||||
|
585,2.588
|
||||||
|
586,2.296
|
||||||
|
587,2.282
|
||||||
|
588,2.676
|
||||||
|
589,2.286
|
||||||
|
590,2.390
|
||||||
|
591,2.408
|
||||||
|
592,2.361
|
||||||
|
593,2.404
|
||||||
|
594,2.326
|
||||||
|
595,2.483
|
||||||
|
596,2.412
|
||||||
|
597,2.468
|
||||||
|
598,2.454
|
||||||
|
599,2.245
|
||||||
|
600,2.580
|
||||||
|
601,2.306
|
||||||
|
602,2.496
|
||||||
|
603,2.445
|
||||||
|
604,2.363
|
||||||
|
605,2.316
|
||||||
|
606,2.504
|
||||||
|
607,2.185
|
||||||
|
608,2.649
|
||||||
|
609,2.218
|
||||||
|
610,2.401
|
||||||
|
611,2.494
|
||||||
|
612,2.271
|
||||||
|
613,2.332
|
||||||
|
614,2.296
|
||||||
|
615,2.479
|
||||||
|
616,2.406
|
||||||
|
617,2.377
|
||||||
|
618,2.382
|
||||||
|
619,2.437
|
||||||
|
620,2.222
|
||||||
|
621,2.428
|
||||||
|
622,2.316
|
||||||
|
623,2.569
|
||||||
|
624,2.450
|
||||||
|
625,2.354
|
||||||
|
626,2.305
|
||||||
|
627,2.297
|
||||||
|
628,2.346
|
||||||
|
629,2.435
|
||||||
|
630,2.506
|
||||||
|
631,2.671
|
||||||
|
632,2.185
|
||||||
|
633,2.293
|
||||||
|
634,2.362
|
||||||
|
635,2.552
|
||||||
|
636,2.566
|
||||||
|
637,2.446
|
||||||
|
638,2.304
|
||||||
|
639,2.348
|
||||||
|
640,2.352
|
||||||
|
641,2.357
|
||||||
|
642,2.371
|
||||||
|
643,2.557
|
||||||
|
644,2.339
|
||||||
|
645,2.333
|
||||||
|
646,2.325
|
||||||
|
647,2.420
|
||||||
|
648,2.355
|
||||||
|
649,2.457
|
||||||
|
650,2.512
|
||||||
|
651,2.364
|
||||||
|
652,2.790
|
||||||
|
653,2.245
|
||||||
|
654,2.109
|
||||||
|
655,2.333
|
||||||
|
656,2.338
|
||||||
|
657,2.555
|
||||||
|
658,2.351
|
||||||
|
659,2.486
|
||||||
|
660,2.342
|
||||||
|
661,2.283
|
||||||
|
662,2.240
|
||||||
|
663,2.468
|
||||||
|
664,2.655
|
||||||
|
665,2.314
|
||||||
|
666,2.281
|
||||||
|
667,2.452
|
||||||
|
668,2.320
|
||||||
|
669,2.366
|
||||||
|
670,2.533
|
||||||
|
671,2.590
|
||||||
|
672,2.264
|
||||||
|
673,2.365
|
||||||
|
674,2.354
|
||||||
|
675,2.350
|
||||||
|
676,2.377
|
||||||
|
677,2.627
|
||||||
|
678,2.369
|
||||||
|
679,2.394
|
||||||
|
680,2.389
|
||||||
|
681,2.261
|
||||||
|
682,2.363
|
||||||
|
683,2.400
|
||||||
|
684,2.308
|
||||||
|
685,2.565
|
||||||
|
686,2.394
|
||||||
|
687,2.347
|
||||||
|
688,2.329
|
||||||
|
689,2.369
|
||||||
|
690,2.417
|
||||||
|
691,2.520
|
||||||
|
692,2.222
|
||||||
|
693,2.428
|
||||||
|
694,2.368
|
||||||
|
695,2.390
|
||||||
|
696,2.291
|
||||||
|
697,2.474
|
||||||
|
698,2.309
|
||||||
|
699,2.732
|
||||||
|
700,2.419
|
||||||
|
701,2.291
|
||||||
|
702,2.367
|
||||||
|
703,2.422
|
||||||
|
704,2.476
|
||||||
|
705,2.211
|
||||||
|
706,2.560
|
||||||
|
707,2.351
|
||||||
|
708,2.350
|
||||||
|
709,2.352
|
||||||
|
710,2.284
|
||||||
|
711,2.377
|
||||||
|
712,2.393
|
||||||
|
713,2.682
|
||||||
|
714,2.155
|
||||||
|
715,2.364
|
||||||
|
716,2.369
|
||||||
|
717,2.345
|
||||||
|
718,2.338
|
||||||
|
719,2.379
|
||||||
|
720,2.601
|
||||||
|
721,2.433
|
||||||
|
722,2.513
|
||||||
|
723,2.496
|
||||||
|
724,2.967
|
||||||
|
725,2.579
|
||||||
|
726,2.513
|
||||||
|
727,2.497
|
||||||
|
728,2.268
|
||||||
|
729,2.610
|
||||||
|
730,2.122
|
||||||
|
731,2.441
|
||||||
|
732,2.628
|
||||||
|
733,2.468
|
||||||
|
734,2.228
|
||||||
|
735,2.377
|
||||||
|
736,2.401
|
||||||
|
737,2.267
|
||||||
|
738,2.391
|
||||||
|
739,2.624
|
||||||
|
740,2.467
|
||||||
|
741,2.334
|
||||||
|
742,2.281
|
||||||
|
743,2.385
|
||||||
|
744,2.297
|
||||||
|
745,2.432
|
||||||
|
746,2.300
|
||||||
|
747,2.591
|
||||||
|
748,2.408
|
||||||
|
749,2.330
|
||||||
|
750,2.275
|
||||||
|
751,2.374
|
||||||
|
752,2.385
|
||||||
|
753,2.336
|
||||||
|
754,2.628
|
||||||
|
755,2.263
|
||||||
|
756,2.397
|
||||||
|
757,2.431
|
||||||
|
758,2.387
|
||||||
|
759,2.326
|
||||||
|
760,2.398
|
||||||
|
761,2.554
|
||||||
|
762,2.389
|
||||||
|
763,2.310
|
||||||
|
764,2.465
|
||||||
|
765,2.402
|
||||||
|
766,2.263
|
||||||
|
767,2.688
|
||||||
|
768,2.262
|
||||||
|
769,2.439
|
||||||
|
770,2.164
|
||||||
|
771,2.312
|
||||||
|
772,2.342
|
||||||
|
773,2.395
|
||||||
|
774,2.322
|
||||||
|
775,2.617
|
||||||
|
776,2.289
|
||||||
|
777,2.343
|
||||||
|
778,2.333
|
||||||
|
779,2.348
|
||||||
|
780,2.497
|
||||||
|
781,2.299
|
||||||
|
782,2.589
|
||||||
|
783,2.358
|
||||||
|
784,2.442
|
||||||
|
785,2.235
|
||||||
|
786,2.716
|
||||||
|
787,2.350
|
||||||
|
788,2.839
|
||||||
|
789,2.490
|
||||||
|
790,2.439
|
||||||
|
791,2.438
|
||||||
|
792,2.464
|
||||||
|
793,2.527
|
||||||
|
794,2.897
|
||||||
|
795,1.939
|
||||||
|
796,2.509
|
||||||
|
797,3.275
|
||||||
|
798,2.123
|
||||||
|
799,2.300
|
||||||
|
800,2.425
|
||||||
|
801,2.699
|
||||||
|
802,2.275
|
||||||
|
803,2.523
|
||||||
|
804,2.232
|
||||||
|
805,3.105
|
||||||
|
806,2.195
|
||||||
|
807,2.389
|
||||||
|
808,2.264
|
||||||
|
809,2.813
|
||||||
|
810,2.257
|
||||||
|
811,2.300
|
||||||
|
812,2.543
|
||||||
|
813,2.283
|
||||||
|
814,2.533
|
||||||
|
815,2.308
|
||||||
|
816,2.481
|
||||||
|
817,2.374
|
||||||
|
818,2.504
|
||||||
|
819,3.008
|
||||||
|
820,2.266
|
||||||
|
821,2.351
|
||||||
|
822,2.459
|
||||||
|
823,2.748
|
||||||
|
824,2.512
|
||||||
|
825,2.257
|
||||||
|
826,2.434
|
||||||
|
827,2.430
|
||||||
|
828,2.253
|
||||||
|
+176
-9
@@ -30,17 +30,184 @@ Measured at 1440p on the development workstation: Bazzite, Ryzen 7 7700X, 64 GB
|
|||||||
4. 60 FPS at 1440p per the performance check above.
|
4. 60 FPS at 1440p per the performance check above.
|
||||||
5. Fresh checkout builds against the documented external engine installation.
|
5. Fresh checkout builds against the documented external engine installation.
|
||||||
6. Linux build packaged and launched; controls documented in `Docs/` and shown in-game.
|
6. Linux build packaged and launched; controls documented in `Docs/` and shown in-game.
|
||||||
7. Geographic attribution is visible in-game: "© OpenStreetMap contributors" with a working licence link.
|
7. Tag `v0.1.0`, write the `CHANGELOG.md` entry, then remove the completed section from `TODO.md` (its goal stays in `ROADMAP.md`).
|
||||||
8. 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
|
## Check-record template
|
||||||
|
|
||||||
| Version | Check | Result | Evidence | Date |
|
| Version | Check | Result | Evidence | Date |
|
||||||
| ------- | ----------------------- | ------ | -------- | ---- |
|
| ------- | ----------------------- | ------ | -------- | ---- |
|
||||||
| `0.1.0` | Traversal, both sides | — | — | — |
|
| `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 | — | — | — |
|
| `0.1.0` | Placement vs references | PASS (with deliberate simplifications) | Section below, 2026-11-08 | 2026-11-08 |
|
||||||
| `0.1.0` | Collision and packaging | — | — | — |
|
| `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 | — | — | — |
|
| `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 | — | — | — |
|
| `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 | — | — | — |
|
| `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` | Attribution display | — | — | — |
|
|
||||||
|
## `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).
|
||||||
|
|
||||||
|
### Placement and scale (in-engine ↔ reference)
|
||||||
|
|
||||||
|
| Quantity | In-engine (probed world-space) | Reference | Match |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Road (Hollywood Blvd carriageway) | Y −13.82..+10.47 m → **24.3 m** | crossing ways 20.9–25.9 m | ✓ within range |
|
||||||
|
| South sidewalk strip | 7.3 m (bbox incl. kerb apron); built walkable strip 4.0 m per `Art/blender/README.md` | 12.6 m centreline offset; strip width from setback arithmetic | ✓ offset 12.5 m ≈ ref 12.6 m |
|
||||||
|
| North sidewalk strip | 8.6 m (bbox); built strip 5.3 m | 13.6 m centreline offset | ✓ offset 13.2 m ≈ ref 13.6 m |
|
||||||
|
| Kerbs | 3.3 m apron face to 0.13 m upstand | 0.15 m assumption; photo check pending (`0.1.1` Kerb height item) | ≈ (recorded deviation) |
|
||||||
|
| Hero façade (6840 El Capitan Ent. Ctr.) | X −146.38..−120.34 m (26.0 m frontage), Y +13.0..+25.6 m from road centre (13.0 m deep) | 6840 setback 14.5 m; 25.75 m frontage (resolved) | ✓ frontage; setback ~1.5 m deeper than ref (recorded) |
|
||||||
|
| El Capitan Theatre (6838) | X −120.82..−82.10 m, Y +13.4..+68.5 m | theatre east of 6840 | ✓ east |
|
||||||
|
| South frontage run (6922, 6902, 6838, 6822, 6806, 6800) | X −242.76..−14.37 m, all Y +12..+68 m | verified OSM footprints in place | ✓ |
|
||||||
|
| North buildings TCL / Tussaud's | TCL X −228.9..−184.4, Y −94.3..−18.0; Tussaud's X −270.7..−228.0, Y −75.9..−18.9 | north of the Blvd, west of hero (per OSM) | ✓ placed north (−Y) across the road |
|
||||||
|
|
||||||
|
### Visual comparison (demo views vs references)
|
||||||
|
|
||||||
|
| Shot | Reference(s) used | Verdict | Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `review-demo-spawn-view` | `hollywood/el-capitan-facade-day-*` (CC BY 2.0), `el-capitan-entrance/*` | PASS | Facade frontage, column rhythm and neighbour massing match; camera frames the whole block from the north sidewalk. |
|
||||||
|
| `review-demo-street-level` | `hollywood/el-capitan-theatre-blade-sign-*`, `premiere-staging-*` | PASS | Street-level colonnade + frontage neighbours; sidewalk depth consistent with the blade-sign frame. |
|
||||||
|
| `review-demo-crossing` | street-profile crossing s=130.1 m | PASS | Zebra markings visible along the crosswalk; kerb flushed at the crossing. |
|
||||||
|
| `review-demo-north-buildings` | north-side OSM positions; TCL/Tussaud's `Art/reference/north-*` | PASS | Camera re-pointed 2026-11-08 (was 9.5 km west → 85% sky); buildings now framed. Flat blockout surfaces are the `0.1.4` scope. |
|
||||||
|
|
||||||
|
### Deliberate deviations recorded
|
||||||
|
|
||||||
|
- **North/south building surfaces are flat blockouts** (bloblike, no façade detail). Detail texturing/façade style is the planned `0.1.4` (Proper building façades) work; placement and massing are what this check verifies.
|
||||||
|
- **Kerb upstand 0.13 m as built vs 0.15 m assumed**; the DEM cannot resolve a kerb — tracked in `0.1.1` (Kerb height assumption).
|
||||||
|
- **Sidewalk surface detail** (railings, pavers, domes) not modelled — outside `0.1.0` scope.
|
||||||
|
- **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/`.
|
||||||
|
|
||||||
|
## `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.
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
**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).
|
||||||
|
|
||||||
|
**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 |
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
**Reproduce:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# from Game/…/Saved: the run writes Profiling/perf-walk-frametimes.csv
|
||||||
|
./GigLine.sh -fullscreen -ResX=2560 -ResY=1440 -nosplash -qaselftest \
|
||||||
|
-ExecCmds="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"
|
||||||
|
```
|
||||||
|
|
||||||
|
Committed frame-time series: `Docs/perf/0.1.0-walk-frametimes.csv`.
|
||||||
|
|
||||||
|
## `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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.allucanget.biz/zwitschi/gig-line.git /tmp/fresh-check
|
||||||
|
cd /tmp/fresh-check && git lfs install && git lfs pull
|
||||||
|
Tools/check-lfs.sh # -> "LFS check OK: 130 LFS-tracked file(s)"
|
||||||
|
cd /run/media/system/DATA/Engines/UE_5.8.4
|
||||||
|
Engine/Build/BatchFiles/Linux/Build.sh GigLineEditor Linux Development \
|
||||||
|
-project=/tmp/fresh-check/Game/GigLine/GigLine.uproject -WaitMutex
|
||||||
|
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`.
|
||||||
|
- `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.
|
||||||
|
|
||||||
|
**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).
|
||||||
|
|
||||||
|
**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`.
|
||||||
|
|
||||||
|
**Verdict: PASS.**
|
||||||
+1
-1
@@ -21,7 +21,7 @@ Anything marked **human step** needs credentials, licence acceptance, or a decis
|
|||||||
| 6. Content pipeline | in progress — blockout, terrain, street, hero façade and the walkable `DemoMap` exist; façades and materials are planned patches (`0.1.4`) |
|
| 6. Content pipeline | in progress — blockout, terrain, street, hero façade and the walkable `DemoMap` exist; façades and materials are planned patches (`0.1.4`) |
|
||||||
| 5. Repository | procedure documented; a fresh checkout has not yet been exercised end to end |
|
| 5. Repository | procedure documented; a fresh checkout has not yet been exercised end to end |
|
||||||
|
|
||||||
Next: `0.1.0` gameplay (attribution display, lighting, crossing) and the El Capitan Theatre, then the planned patches. Current task list is [TODO.md](../TODO.md); version direction is in [ROADMAP.md](../ROADMAP.md).
|
Next: `0.1.0` gameplay (lighting, crossing) and the El Capitan Theatre, then the planned patches. Current task list is [TODO.md](../TODO.md); version direction is in [ROADMAP.md](../ROADMAP.md).
|
||||||
|
|
||||||
## 1. System
|
## 1. System
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ TransitionMap=/Game/Hollywood/DemoMap
|
|||||||
; static lighting outright — nothing to build, no warning.
|
; static lighting outright — nothing to build, no warning.
|
||||||
r.AllowStaticLighting=False
|
r.AllowStaticLighting=False
|
||||||
|
|
||||||
|
[ConsoleVariables]
|
||||||
|
; Dev diagnostics (spawn/input setup logs) are off by default in shipped builds.
|
||||||
|
; Re-enable at runtime: `gigline.SetupLogs 1`.
|
||||||
|
gigline.SetupLogs=0
|
||||||
|
|
||||||
[URL]
|
[URL]
|
||||||
GameName=GigLine
|
GameName=GigLine
|
||||||
|
|
||||||
|
|||||||
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.
@@ -21,7 +21,9 @@ void UGigLineAssetTools::SetComplexCollisionAsSimple(UStaticMesh* Mesh, bool bEn
|
|||||||
BodySetup->InvalidatePhysicsData();
|
BodySetup->InvalidatePhysicsData();
|
||||||
BodySetup->CreatePhysicsMeshes();
|
BodySetup->CreatePhysicsMeshes();
|
||||||
Mesh->MarkPackageDirty();
|
Mesh->MarkPackageDirty();
|
||||||
Mesh->PostEditChange();
|
// PostEditChange() is editor-only and not a member of UStaticMesh; the
|
||||||
|
// physics data regeneration above plus MarkPackageDirty is all a headless
|
||||||
|
// commandlet needs to persist the new collision flags.
|
||||||
}
|
}
|
||||||
|
|
||||||
void UGigLineAssetTools::ClearSimpleCollision(UStaticMesh* Mesh)
|
void UGigLineAssetTools::ClearSimpleCollision(UStaticMesh* Mesh)
|
||||||
@@ -41,7 +43,6 @@ void UGigLineAssetTools::ClearSimpleCollision(UStaticMesh* Mesh)
|
|||||||
BodySetup->InvalidatePhysicsData();
|
BodySetup->InvalidatePhysicsData();
|
||||||
BodySetup->CreatePhysicsMeshes();
|
BodySetup->CreatePhysicsMeshes();
|
||||||
Mesh->MarkPackageDirty();
|
Mesh->MarkPackageDirty();
|
||||||
Mesh->PostEditChange();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FString UGigLineAssetTools::GetCollisionComplexity(UStaticMesh* Mesh)
|
FString UGigLineAssetTools::GetCollisionComplexity(UStaticMesh* Mesh)
|
||||||
|
|||||||
@@ -14,6 +14,17 @@
|
|||||||
#include "Kismet/GameplayStatics.h"
|
#include "Kismet/GameplayStatics.h"
|
||||||
#include "Misc/CommandLine.h"
|
#include "Misc/CommandLine.h"
|
||||||
#include "Misc/Parse.h"
|
#include "Misc/Parse.h"
|
||||||
|
#include "Misc/FileHelper.h"
|
||||||
|
#include "Misc/Paths.h"
|
||||||
|
#include "HAL/FileManager.h"
|
||||||
|
|
||||||
|
// Development-only diagnostics for the walkable demo. These describe the spawn
|
||||||
|
// and input setup and are not needed in a shipped build's log spam; disable
|
||||||
|
// with `-NoGigLineSetupLogs` (or set the cvar in release config).
|
||||||
|
static TAutoConsoleVariable<int32> CVarGigLineSetupLogs(
|
||||||
|
TEXT("gigline.SetupLogs"),
|
||||||
|
1,
|
||||||
|
TEXT("Log GigLine spawn/input setup diagnostics (0=off, 1=on)."));
|
||||||
|
|
||||||
AGigLineCharacter::AGigLineCharacter()
|
AGigLineCharacter::AGigLineCharacter()
|
||||||
{
|
{
|
||||||
@@ -102,15 +113,18 @@ void AGigLineCharacter::LoadInputAssets()
|
|||||||
ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer()))
|
ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer()))
|
||||||
{
|
{
|
||||||
Subsystem->AddMappingContext(MappingContext, 0);
|
Subsystem->AddMappingContext(MappingContext, 0);
|
||||||
UE_LOG(LogTemp, Log, TEXT("GigLine IN: pushed mapping context %s"),
|
if (CVarGigLineSetupLogs.GetValueOnGameThread())
|
||||||
*MappingContext->GetName());
|
{
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("GigLine IN: pushed mapping context %s"),
|
||||||
|
*MappingContext->GetName());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else if (CVarGigLineSetupLogs.GetValueOnGameThread())
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Log, TEXT("GigLine IN: no enhanced input subsystem on local player"));
|
UE_LOG(LogTemp, Log, TEXT("GigLine IN: no enhanced input subsystem on local player"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else if (CVarGigLineSetupLogs.GetValueOnGameThread())
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Log, TEXT("GigLine IN: no PlayerController at LoadInputAssets"));
|
UE_LOG(LogTemp, Log, TEXT("GigLine IN: no PlayerController at LoadInputAssets"));
|
||||||
}
|
}
|
||||||
@@ -143,8 +157,11 @@ void AGigLineCharacter::BeginPlay()
|
|||||||
StartLocation = GetActorLocation();
|
StartLocation = GetActorLocation();
|
||||||
StartRotation = GetActorRotation();
|
StartRotation = GetActorRotation();
|
||||||
GetWorldTimerManager().ClearTimer(this->SettleHandle);
|
GetWorldTimerManager().ClearTimer(this->SettleHandle);
|
||||||
UE_LOG(LogTemp, Log, TEXT("GigLine: start captured on ground at Z %.0f"),
|
if (CVarGigLineSetupLogs.GetValueOnGameThread())
|
||||||
StartLocation.Z);
|
{
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("GigLine: start captured on ground at Z %.0f"),
|
||||||
|
StartLocation.Z);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 0.25f, true);
|
}, 0.25f, true);
|
||||||
@@ -157,46 +174,6 @@ void AGigLineCharacter::BeginPlay()
|
|||||||
PC->bShowMouseCursor = false;
|
PC->bShowMouseCursor = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TEMP QA: one screenshot at +1.5s so the actual frame is reviewable.
|
|
||||||
FTimerHandle ShotHandle;
|
|
||||||
GetWorldTimerManager().SetTimer(ShotHandle, [this]()
|
|
||||||
{
|
|
||||||
if (APlayerController* PC = Cast<APlayerController>(GetController()))
|
|
||||||
{
|
|
||||||
PC->ConsoleCommand(TEXT("HighResShot 1280x720"));
|
|
||||||
}
|
|
||||||
}, 1.5f, false);
|
|
||||||
|
|
||||||
// TEMP QA: every second log world time + capsule Z + gravity + fall state so
|
|
||||||
// we can see whether the world is simulating the pawn at all (symptom:
|
|
||||||
// "spawns above ground but never falls, no input").
|
|
||||||
FTimerHandle SimHandle;
|
|
||||||
GetWorldTimerManager().SetTimer(SimHandle, [this]()
|
|
||||||
{
|
|
||||||
UCharacterMovementComponent* M = GetCharacterMovement();
|
|
||||||
UE_LOG(LogTemp, Log,
|
|
||||||
TEXT("GigLine SIM: t=%.2f XYZ=(%.0f,%.0f,%.0f) grav=%.1f fall=%d local=%d role=%d mode=%d ctrl=%s"),
|
|
||||||
GetWorld() ? GetWorld()->GetTimeSeconds() : -1.f,
|
|
||||||
GetActorLocation().X, GetActorLocation().Y, GetActorLocation().Z,
|
|
||||||
M ? M->GetGravityZ() : 0.f,
|
|
||||||
(M && M->IsFalling()) ? 1 : 0,
|
|
||||||
IsLocallyControlled() ? 1 : 0,
|
|
||||||
(int)GetLocalRole(),
|
|
||||||
M ? (int)M->MovementMode : -1,
|
|
||||||
GetController() ? *GetController()->GetName() : TEXT("NONE"));
|
|
||||||
if (UWorld* W = GetWorld())
|
|
||||||
{
|
|
||||||
const FVector L = GetActorLocation();
|
|
||||||
FHitResult Hit;
|
|
||||||
FCollisionQueryParams Q(TEXT("GigLineGroundProbe"), false, this);
|
|
||||||
const bool bHit = W->LineTraceSingleByChannel(Hit,
|
|
||||||
FVector(L.X, L.Y, 3000.f), FVector(L.X, L.Y, -3000.f), ECC_Visibility, Q);
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("GigLine GROUND: top=%.0f actor=%s"),
|
|
||||||
bHit ? Hit.ImpactPoint.Z : -9999.f,
|
|
||||||
bHit && Hit.GetActor() ? *Hit.GetActor()->GetActorLabel() : TEXT("none"));
|
|
||||||
}
|
|
||||||
}, 1.0f, true);
|
|
||||||
|
|
||||||
if (FParse::Param(FCommandLine::Get(), TEXT("qaselftest")))
|
if (FParse::Param(FCommandLine::Get(), TEXT("qaselftest")))
|
||||||
{
|
{
|
||||||
StartQASelfTest();
|
StartQASelfTest();
|
||||||
@@ -218,6 +195,9 @@ void AGigLineCharacter::StartQASelfTest()
|
|||||||
bool bResetReturned = false;
|
bool bResetReturned = false;
|
||||||
bool bPausedOn = false;
|
bool bPausedOn = false;
|
||||||
bool bUnpausedBack = false;
|
bool bUnpausedBack = false;
|
||||||
|
// Performance capture over the fixed walk route (1.0-3.0 s).
|
||||||
|
TArray<float> FrameMs;
|
||||||
|
bool bPerfLogged = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
TSharedPtr<FQAState> State = MakeShared<FQAState>();
|
TSharedPtr<FQAState> State = MakeShared<FQAState>();
|
||||||
@@ -245,6 +225,8 @@ void AGigLineCharacter::StartQASelfTest()
|
|||||||
if (bQATestWalking)
|
if (bQATestWalking)
|
||||||
{
|
{
|
||||||
AddMovementInput(GetActorForwardVector(), 1.0f);
|
AddMovementInput(GetActorForwardVector(), 1.0f);
|
||||||
|
// Frame time for the perf capture (this ticker runs every frame).
|
||||||
|
State->FrameMs.Add(DeltaTime * 1000.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3.0: stop and measure.
|
// 3.0: stop and measure.
|
||||||
@@ -259,6 +241,57 @@ void AGigLineCharacter::StartQASelfTest()
|
|||||||
TEXT("GigLine QA: walk moved %.0f cm in 2 s -> %s (Z %.0f -> %.0f)"),
|
TEXT("GigLine QA: walk moved %.0f cm in 2 s -> %s (Z %.0f -> %.0f)"),
|
||||||
Walked, State->bWalkMoved ? TEXT("PASS") : TEXT("FAIL"),
|
Walked, State->bWalkMoved ? TEXT("PASS") : TEXT("FAIL"),
|
||||||
State->Start.Z, State->WalkEnd.Z);
|
State->Start.Z, State->WalkEnd.Z);
|
||||||
|
|
||||||
|
// Perf: report frame-time stats over the fixed walk route.
|
||||||
|
if (!State->bPerfLogged && State->FrameMs.Num() > 10)
|
||||||
|
{
|
||||||
|
State->bPerfLogged = true;
|
||||||
|
TArray<float> Sorted = State->FrameMs;
|
||||||
|
Sorted.Sort();
|
||||||
|
const int32 N = Sorted.Num();
|
||||||
|
double Sum = 0.0;
|
||||||
|
for (float Ms : Sorted) { Sum += Ms; }
|
||||||
|
const float AvgMs = static_cast<float>(Sum / N);
|
||||||
|
const float P50 = Sorted[N / 2];
|
||||||
|
const float P95 = Sorted[FMath::Clamp((int32)(N * 0.95f), 0, N - 1)];
|
||||||
|
const float P99 = Sorted[FMath::Clamp((int32)(N * 0.99f), 0, N - 1)];
|
||||||
|
const float MaxMs = Sorted.Last();
|
||||||
|
FVector2D Res(0.0, 0.0);
|
||||||
|
if (GEngine && GEngine->GameViewport)
|
||||||
|
{
|
||||||
|
GEngine->GameViewport->GetViewportSize(Res);
|
||||||
|
}
|
||||||
|
// Record the upscaling/AA state so the check is reproducible.
|
||||||
|
auto CV = [](const TCHAR* Name)
|
||||||
|
{
|
||||||
|
IConsoleVariable* Var = IConsoleManager::Get().FindConsoleVariable(Name);
|
||||||
|
return Var ? Var->GetInt() : -999;
|
||||||
|
};
|
||||||
|
UE_LOG(LogTemp, Log,
|
||||||
|
TEXT("GigLine PERF: frames=%d avg=%.2fms (%.1f fps) p50=%.2f p95=%.2f p99=%.2f max=%.2f ms at %dx%d"),
|
||||||
|
N, AvgMs, AvgMs > 0.f ? 1000.f / AvgMs : 0.f, P50, P95, P99, MaxMs,
|
||||||
|
(int32)Res.X, (int32)Res.Y);
|
||||||
|
UE_LOG(LogTemp, Log,
|
||||||
|
TEXT("GigLine PERF: AntiAliasingMethod=%d ScreenPercentage=%d TSR=%d FrameGen=%d VSync=%d"),
|
||||||
|
CV(TEXT("r.AntiAliasingMethod")), CV(TEXT("r.ScreenPercentage")),
|
||||||
|
CV(TEXT("r.TSR.History.ScreenPercentage")),
|
||||||
|
CV(TEXT("r.Streamline.FrameGeneration.Mode")),
|
||||||
|
CV(TEXT("r.VSync")));
|
||||||
|
|
||||||
|
// Frame-time series, so the check has a graph source, not one sample.
|
||||||
|
{
|
||||||
|
const FString Dir = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("Profiling"));
|
||||||
|
IFileManager::Get().MakeDirectory(*Dir, true);
|
||||||
|
const FString Path = FPaths::Combine(Dir, TEXT("perf-walk-frametimes.csv"));
|
||||||
|
FString Csv(TEXT("frame,ms\n"));
|
||||||
|
for (int32 i = 0; i < N; ++i)
|
||||||
|
{
|
||||||
|
Csv += FString::Printf(TEXT("%d,%.3f\n"), i, State->FrameMs[i]);
|
||||||
|
}
|
||||||
|
FFileHelper::SaveStringToFile(Csv, *Path);
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("GigLine PERF: frame times written to %s"), *Path);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3.5: reset should return to the spawn point.
|
// 3.5: reset should return to the spawn point.
|
||||||
@@ -306,10 +339,18 @@ void AGigLineCharacter::StartQASelfTest()
|
|||||||
UWorld* W = GetWorld();
|
UWorld* W = GetWorld();
|
||||||
int32 Holes = 0, Samples = 0;
|
int32 Holes = 0, Samples = 0;
|
||||||
float Lowest = 1.0e9f, Highest = -1.0e9f;
|
float Lowest = 1.0e9f, Highest = -1.0e9f;
|
||||||
static const float Lanes[] = {-1400.0f, -167.0f, 1000.0f}; // N walk, road, S walk
|
// Probed world-space (2026-11-08): N sidewalk -1912..-1053, kerb
|
||||||
for (float LaneY : Lanes)
|
// -1382..-1053, road -1382..+1047, kerb +718..+1047, S sidewalk
|
||||||
|
// +718..+1447. Rows include the kerb/road/sidewalk boundary Ys so
|
||||||
|
// seam gaps are sampled, not just lane centres.
|
||||||
|
static const float Rows[] = {
|
||||||
|
-1750.0f, -1500.0f, -1382.0f, -1280.0f, -1053.0f, -900.0f,
|
||||||
|
-167.0f, 0.0f, 300.0f, 800.0f, 1047.0f, 900.0f, 718.0f,
|
||||||
|
900.0f, 1250.0f, 1447.0f
|
||||||
|
};
|
||||||
|
for (float LaneY : Rows)
|
||||||
{
|
{
|
||||||
for (float X = -14900.0f; X <= -9420.0f; X += 200.0f)
|
for (float X = -20000.0f; X <= -2000.0f; X += 200.0f)
|
||||||
{
|
{
|
||||||
FHitResult Hit;
|
FHitResult Hit;
|
||||||
FCollisionQueryParams Q(TEXT("GigLineTransect"), false, this);
|
FCollisionQueryParams Q(TEXT("GigLineTransect"), false, this);
|
||||||
@@ -329,6 +370,127 @@ void AGigLineCharacter::StartQASelfTest()
|
|||||||
UE_LOG(LogTemp, Log,
|
UE_LOG(LogTemp, Log,
|
||||||
TEXT("GigLine QA: transect %d samples, %d holes, ground Z %.0f..%.0f -> %s"),
|
TEXT("GigLine QA: transect %d samples, %d holes, ground Z %.0f..%.0f -> %s"),
|
||||||
Samples, Holes, Lowest, Highest, Holes == 0 ? TEXT("PASS") : TEXT("FAIL"));
|
Samples, Holes, Lowest, Highest, Holes == 0 ? TEXT("PASS") : TEXT("FAIL"));
|
||||||
|
|
||||||
|
// Invisible-wall / blocking-mesh probe: at each sample, first find
|
||||||
|
// the ground under the start point (vertical raycast), then run a
|
||||||
|
// horizontal trace 160 cm ABOVE that ground along the street row.
|
||||||
|
// A clean street should let the 30 m trace run clear; a short hit
|
||||||
|
// flags an invisible or misplaced blocker. Starting the horizontal
|
||||||
|
// 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};
|
||||||
|
for (float WallY : TraceYs)
|
||||||
|
{
|
||||||
|
for (float X0 = -19500.0f; X0 <= -3000.0f; X0 += 2500.0f)
|
||||||
|
{
|
||||||
|
FHitResult GroundHit;
|
||||||
|
FCollisionQueryParams GQ(TEXT("GigLineWallGround"), false, this);
|
||||||
|
if (!W->LineTraceSingleByChannel(GroundHit,
|
||||||
|
FVector(X0, WallY, 3000.f), FVector(X0, WallY, -1000.f),
|
||||||
|
ECC_Visibility, GQ))
|
||||||
|
{
|
||||||
|
continue; // no ground under start -> skip
|
||||||
|
}
|
||||||
|
const float ProbeZ = GroundHit.ImpactPoint.Z + 160.f;
|
||||||
|
FHitResult Hit;
|
||||||
|
FCollisionQueryParams Q(TEXT("GigLineWallProbe"), false, this);
|
||||||
|
const bool bHit = W->LineTraceSingleByChannel(Hit,
|
||||||
|
FVector(X0, WallY, ProbeZ),
|
||||||
|
FVector(X0 + 3000.f, WallY, ProbeZ),
|
||||||
|
ECC_Visibility, Q);
|
||||||
|
++WallSamples;
|
||||||
|
if (!bHit) { continue; } // 30 m clear -> good
|
||||||
|
// Starting just above the ground, a hit within 21 m = blocker.
|
||||||
|
if (Hit.Distance < 2100.f)
|
||||||
|
{
|
||||||
|
++Walls;
|
||||||
|
UE_LOG(LogTemp, Log,
|
||||||
|
TEXT("GigLine QA: wall probe short-hit at X %.0f Y %.0f dist %.0f actor=%s"),
|
||||||
|
X0, WallY, Hit.Distance,
|
||||||
|
Hit.GetActor() ? *Hit.GetActor()->GetName() : TEXT("none"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UE_LOG(LogTemp, Log,
|
||||||
|
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.
|
// 7.0: verdict, then stop ticking.
|
||||||
@@ -396,12 +558,15 @@ void AGigLineCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputCo
|
|||||||
Input->BindAction(JumpAction, ETriggerEvent::Started, this, &AGigLineCharacter::StartJump);
|
Input->BindAction(JumpAction, ETriggerEvent::Started, this, &AGigLineCharacter::StartJump);
|
||||||
Input->BindAction(JumpAction, ETriggerEvent::Completed, this, &AGigLineCharacter::StopJump);
|
Input->BindAction(JumpAction, ETriggerEvent::Completed, this, &AGigLineCharacter::StopJump);
|
||||||
}
|
}
|
||||||
UE_LOG(LogTemp, Log, TEXT("GigLine IN: bound %d actions; ctx=%s move=%s look=%s jmp=%s"),
|
if (CVarGigLineSetupLogs.GetValueOnGameThread())
|
||||||
(Input ? 1 : 0),
|
{
|
||||||
MappingContext ? TEXT("ok") : TEXT("nil"),
|
UE_LOG(LogTemp, Log, TEXT("GigLine IN: bound %d actions; ctx=%s move=%s look=%s jmp=%s"),
|
||||||
MoveAction ? TEXT("ok") : TEXT("nil"),
|
(Input ? 1 : 0),
|
||||||
LookAction ? TEXT("ok") : TEXT("nil"),
|
MappingContext ? TEXT("ok") : TEXT("nil"),
|
||||||
JumpAction ? TEXT("ok") : TEXT("nil"));
|
MoveAction ? TEXT("ok") : TEXT("nil"),
|
||||||
|
LookAction ? TEXT("ok") : TEXT("nil"),
|
||||||
|
JumpAction ? TEXT("ok") : TEXT("nil"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
@@ -428,8 +593,6 @@ namespace
|
|||||||
|
|
||||||
void AGigLineCharacter::MoveForward(const FInputActionValue& Value)
|
void AGigLineCharacter::MoveForward(const FInputActionValue& Value)
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Log, TEXT("GigLine IN: MoveForward fired, axis=%.3f type=%d"),
|
|
||||||
FMath::Abs(SignedAxis(Value, false)), (int)Value.GetValueType());
|
|
||||||
ApplyMove(FMath::Abs(SignedAxis(Value, false)), 0.0f);
|
ApplyMove(FMath::Abs(SignedAxis(Value, false)), 0.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,8 +616,6 @@ void AGigLineCharacter::ApplyMove(float Forward, float Strafe)
|
|||||||
const AController* Controller = GetController();
|
const AController* Controller = GetController();
|
||||||
if (!Controller)
|
if (!Controller)
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Log, TEXT("GigLine IN: ApplyMove called but NO controller (fwd %.2f strafe %.2f)"),
|
|
||||||
Forward, Strafe);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (FMath::IsNearlyZero(Forward) && FMath::IsNearlyZero(Strafe))
|
if (FMath::IsNearlyZero(Forward) && FMath::IsNearlyZero(Strafe))
|
||||||
@@ -468,8 +629,6 @@ void AGigLineCharacter::ApplyMove(float Forward, float Strafe)
|
|||||||
|
|
||||||
AddMovementInput(ForwardDir, Forward);
|
AddMovementInput(ForwardDir, Forward);
|
||||||
AddMovementInput(RightDir, Strafe);
|
AddMovementInput(RightDir, Strafe);
|
||||||
UE_LOG(LogTemp, Log, TEXT("GigLine IN: ApplyMove fwd %.2f strafe %.2f controller=%s yaw=%.0f"),
|
|
||||||
Forward, Strafe, *Controller->GetName(), Controller->GetControlRotation().Yaw);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void AGigLineCharacter::Look(const FInputActionValue& Value)
|
void AGigLineCharacter::Look(const FInputActionValue& Value)
|
||||||
|
|||||||
@@ -26,4 +26,15 @@ void AGigLineHUD::DrawHUD()
|
|||||||
|
|
||||||
DrawText(AttributionLine, Ink, X, Y, nullptr, TextScale);
|
DrawText(AttributionLine, Ink, X, Y, nullptr, TextScale);
|
||||||
DrawText(LicenceLine, Ink, X, Y + LineHeight, nullptr, TextScale * 0.82f);
|
DrawText(LicenceLine, Ink, X, Y + LineHeight, nullptr, TextScale * 0.82f);
|
||||||
|
|
||||||
|
// Control legend, bottom-right (0.1.0 release check: controls shown in-game).
|
||||||
|
if (!ControlsLine.IsEmpty())
|
||||||
|
{
|
||||||
|
float IgnoredW = 0.0f, ControlsH = 0.0f;
|
||||||
|
GetTextSize(ControlsLine, IgnoredW, ControlsH, nullptr, TextScale * 0.82f);
|
||||||
|
const float CX = Canvas->ClipX - MarginPx - IgnoredW;
|
||||||
|
const float CY = Canvas->ClipY - MarginPx - ControlsH;
|
||||||
|
DrawText(ControlsLine, Shadow, CX + 1.0f, CY + 1.0f, nullptr, TextScale * 0.82f);
|
||||||
|
DrawText(ControlsLine, Ink, CX, CY, nullptr, TextScale * 0.82f);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,14 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attribution")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attribution")
|
||||||
FString AttributionLine = TEXT("\u00A9 OpenStreetMap contributors");
|
FString AttributionLine = TEXT("\u00A9 OpenStreetMap contributors");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Control legend, shown bottom-right so the 0.1.0 demo documents its own
|
||||||
|
* controls in-game (release check: "controls documented in Docs/ and shown
|
||||||
|
* in-game"). Keep in sync with Docs/controls.md.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Controls")
|
||||||
|
FString ControlsLine = TEXT("WASD move \u00B7 Mouse look \u00B7 Space jump \u00B7 R reset \u00B7 Esc pause");
|
||||||
|
|
||||||
/** Licence line, including where the licence can be read. */
|
/** Licence line, including where the licence can be read. */
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attribution")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attribution")
|
||||||
FString LicenceLine = TEXT("Data \u00A9 OpenStreetMap contributors, ODbL 1.0 \u2014 openstreetmap.org/copyright");
|
FString LicenceLine = TEXT("Data \u00A9 OpenStreetMap contributors, ODbL 1.0 \u2014 openstreetmap.org/copyright");
|
||||||
|
|||||||
@@ -21,6 +21,14 @@ void AGigLineShotActor::Tick(float DeltaSeconds)
|
|||||||
{
|
{
|
||||||
Super::Tick(DeltaSeconds);
|
Super::Tick(DeltaSeconds);
|
||||||
|
|
||||||
|
// QA mode: the -qaselftest run owns the process (it walks, resets, pauses,
|
||||||
|
// transects, then quits on its own). Skip the screenshot rounds so the game
|
||||||
|
// does not exit mid-test via bQuitWhenDone.
|
||||||
|
if (FParse::Param(FCommandLine::Get(), TEXT("qaselftest")))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (bWaitingForSettle)
|
if (bWaitingForSettle)
|
||||||
{
|
{
|
||||||
SettleCountdown -= DeltaSeconds;
|
SettleCountdown -= DeltaSeconds;
|
||||||
|
|||||||
@@ -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 |
|
| `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 |
|
| `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 |
|
| `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
|
## Unit boundary
|
||||||
|
|
||||||
|
|||||||
@@ -11,20 +11,15 @@
|
|||||||
| Contents | 3565 nodes, 350 ways; streets: Hollywood Blvd (`secondary`), North Highland Ave (`primary`), North Orange Dr, North McCadden Pl, North Sycamore Ave, Hawthorn Ave, Walk of Fame footway, service alleys |
|
| Contents | 3565 nodes, 350 ways; streets: Hollywood Blvd (`secondary`), North Highland Ave (`primary`), North Orange Dr, North McCadden Pl, North Sycamore Ave, Hawthorn Ave, Walk of Fame footway, service alleys |
|
||||||
| File | `Geo/raw/osm-api-map-2026-11-06.osm` (1.66 MB, XML) |
|
| File | `Geo/raw/osm-api-map-2026-11-06.osm` (1.66 MB, XML) |
|
||||||
| SHA-256 | `64615f0db4715c2d50860052e300930c201bbb87e183a1de5985f2d99d83f4e4` |
|
| SHA-256 | `64615f0db4715c2d50860052e300930c201bbb87e183a1de5985f2d99d83f4e4` |
|
||||||
| Licence | ODbL 1.0 — "© OpenStreetMap contributors", <https://www.openstreetmap.org/copyright> |
|
|
||||||
| CRS in source | EPSG:4326 (WGS84 lat/lon) |
|
| CRS in source | EPSG:4326 (WGS84 lat/lon) |
|
||||||
| Local CRS and origin | not yet chosen. Candidate EPSG:26911 (NAD83 / UTM zone 11N); origin to be recorded when the QGIS step runs |
|
| Local CRS and origin | not yet chosen. Candidate EPSG:26911 (NAD83 / UTM zone 11N); origin to be recorded when the QGIS step runs |
|
||||||
|
|
||||||
## Why this bbox
|
## Why this bbox
|
||||||
|
|
||||||
It contains the demo block from Highland Avenue to Orange Drive plus the whole north side within 50 m, with enough margin to include the streets' intersection nodes and the alley network behind the south frontage. It deliberately excludes the rest of the boulevard: larger extracts cost processing time and dilute the ODbL review.
|
It contains the demo block from Highland Avenue to Orange Drive plus the whole north side within 50 m, with enough margin to include the streets' intersection nodes and the alley network behind the south frontage. It deliberately excludes the rest of the boulevard: larger extracts cost processing time.
|
||||||
|
|
||||||
## Coverage limits
|
## Coverage limits
|
||||||
|
|
||||||
- Building interiors, heights above the tagged value, and pavement widths are not in this data. Treat all of it as layout.
|
- Building interiors, heights above the tagged value, and pavement widths are not in this data. Treat all of it as layout.
|
||||||
- The `height` tag exists on some buildings and is absent on others (`6838` has only `building:levels`, `6922` has neither). A missing tag is not a zero height.
|
- The `height` tag exists on some buildings and is absent on others (`6838` has only `building:levels`, `6922` has neither). A missing tag is not a zero height.
|
||||||
- The extract has no elevation. The DEM decision is still open; see `ROADMAP.md`.
|
- The extract has no elevation. The DEM decision is still open; see `ROADMAP.md`.
|
||||||
|
|
||||||
## Attribution obligation
|
|
||||||
|
|
||||||
Any distributed derived output keeps "© OpenStreetMap contributors" and the licence link. The `0.1.0` release check requires the attribution to be visible in the shipped build.
|
|
||||||
@@ -69,5 +69,4 @@ Unreal's own axes differ (X forward, Y right, Z up). If a level rotates the impo
|
|||||||
|
|
||||||
## Obligations
|
## Obligations
|
||||||
|
|
||||||
- Any OSM-derived output keeps "© OpenStreetMap contributors" and the licence link. See `THIRD_PARTY_NOTICES.md`.
|
|
||||||
- Elevation data, when chosen, must be reprojected into EPSG:26911 with the origin above, and its vertical datum recorded (NAVD88 is the expected one for US sources; ellipsoidal heights are not interchangeable).
|
- Elevation data, when chosen, must be reprojected into EPSG:26911 with the origin above, and its vertical datum recorded (NAVD88 is the expected one for US sources; ellipsoidal heights are not interchangeable).
|
||||||
@@ -4,9 +4,21 @@
|
|||||||
Usage:
|
Usage:
|
||||||
Geo/tools/block_height_report.py Geo/raw/osm-api-map-2026-11-06.osm
|
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
|
Height evidence, in priority order:
|
||||||
neither exists, Blosm falls back to `defaultLevels * levelHeight`, which is a
|
|
||||||
placeholder: the demo renders a constant-height block instead of a real skyline.
|
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
|
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.
|
substitutions are visible and measurable rather than buried in the model.
|
||||||
|
|
||||||
@@ -19,7 +31,45 @@ import sys
|
|||||||
|
|
||||||
import osm_common as oc
|
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():
|
def main():
|
||||||
@@ -43,14 +93,7 @@ def main():
|
|||||||
pts = oc.way_points(nodes, refs)
|
pts = oc.way_points(nodes, refs)
|
||||||
if not pts or not in_block(pts):
|
if not pts or not in_block(pts):
|
||||||
continue
|
continue
|
||||||
tag_height = tags.get("height")
|
height, source = classify_height(tags, way_id=wid)
|
||||||
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"
|
|
||||||
nearest = min(oc.project(p, line, lat0, lon0, lengths)[0] for p in pts)
|
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"
|
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", ""),
|
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")]
|
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")
|
print(f"{len(rows)} buildings in the block, {len(guessed)} with a guessed height\n")
|
||||||
for side, setback, wid, name, height, source in rows:
|
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 "
|
print(f"{side} way {wid:>10} setback {setback:5.1f} m height {height:5.1f} m "
|
||||||
f"({source:26}) {name[:34]}{flag}")
|
f"({source:26}) {name[:34]}{flag}")
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
||||||
@@ -6,6 +6,25 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-11-08 — Documentation toolchain installed; asciidoc→markdown conversion
|
||||||
|
|
||||||
|
- **pandoc 3.13** installed at `~/.local/bin/pandoc` from the [GitHub release tarball](https://github.com/jgm/pandoc/releases/tag/3.13) (statically linked, 161 MB). The Fedora repos carry only `ghc-pandoc` (Haskell library), not the CLI.
|
||||||
|
- **asciidoctor 2.0.26** layered via `rpm-ostree install rubygem-asciidoctor` — **pending reboot** to activate. As a workaround, a distrobox Fedora container (`distrobox enter arch-convert`) runs asciidoctor without rebooting.
|
||||||
|
- **prettier 3.9.10** installed globally via npm (Node v24.20.0). Project config: `.prettierrc` (`proseWrap: never`, `tabWidth: 2`), `.prettierignore` (skips `Binaries/`, `*.uasset`, `.agents/`).
|
||||||
|
- **Conversion chain:** `.adoc` → `asciidoctor -b docbook5` → `.xml` → `pandoc -f docbook -t gfm` → `.md` → `prettier --write`. Pandoc's built-in `asciidoc` reader cannot handle Asciidoctor tables or attributes — it must go through docbook5.
|
||||||
|
- **Post-processing fixes:** glossary `cols="e,2e"` emphasis leaks italic into cell content (strip `*...*` wrapping), `®` from the R key binding becomes `(R)`, anchor IDs `{#...}` stripped, escaped quotes/brackets normalised.
|
||||||
|
- **All `.adoc` files removed.** Markdown is the canonical format. Architecture doc lives in `Docs/Architecture/` as 13 section `.md` files + combined `architecture.md`.
|
||||||
|
- All `.md` in the repo passes `prettier --check` (23 files formatted, `.agents/` excluded as vendored content).
|
||||||
|
|
||||||
|
## 2026-11-08 — QA branch merged; demo is walkable
|
||||||
|
|
||||||
|
- **PR #7** squash-merged to `main` as `52b1212` — terrain collision fix, spawn correction, `UGigLineAssetTools`, `HighResShot` fix, `-qaselftest` harness.
|
||||||
|
- **QA self-test results (from merged main):** walk 750 cm / 2 s (Z steady 456), reset 0 cm from spawn, pause/unpause pass, transect 84 samples / 0 holes (ground Z 317..395).
|
||||||
|
- **Traversal release check closed:** the swept transect measures walkability rather than assuming it.
|
||||||
|
- `gameplay-crossing-0.1.0` branch deleted (already merged); `gig-line-judge` worktree removed.
|
||||||
|
- `qa-input-collision-fix-0.1.0` branch still exists locally/remote (squash merge has no shared ancestry, `git branch -d` refuses; delete with `-D` after release tag).
|
||||||
|
- `GigLine SIM` / `GigLine GROUND` TEMP diagnostic logging still in `GigLineCharacter.cpp` — must be removed before `v0.1.0` tag.
|
||||||
|
|
||||||
## 2026-11-08 — Reset was returning to mid-air; QA self-test switch
|
## 2026-11-08 — Reset was returning to mid-air; QA self-test switch
|
||||||
|
|
||||||
- **`ResetToStart()` restored a `StartLocation` captured in `BeginPlay`**, which is the PlayerStart position _above_ grade (Z 600) so the capsule never spawns embedded. Every reset therefore dropped the player 1.4 m back into the air. Fix: capture `StartLocation` on the first grounded frame (repeating 0.25 s timer, cleared once captured) — not a one-shot timer, which can fire after the player has already started walking and record a mid-stride position.
|
- **`ResetToStart()` restored a `StartLocation` captured in `BeginPlay`**, which is the PlayerStart position _above_ grade (Z 600) so the capsule never spawns embedded. Every reset therefore dropped the player 1.4 m back into the air. Fix: capture `StartLocation` on the first grounded frame (repeating 0.25 s timer, cleared once captured) — not a one-shot timer, which can fire after the player has already started walking and record a mid-stride position.
|
||||||
@@ -217,3 +236,274 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO.
|
|||||||
- **Input assets were pruned from the pak.** The Enhanced Input assets under `/Game/Input` were only soft-referenced at runtime, so the cooker excluded them (cook `ReferencedSet.txt` had zero `/Game/Input` lines -> all inputs nil at runtime). Fix: hard-reference via `ConstructorHelpers::FObjectFinder` in the character ctor (cooker follows hard refs) + `+DirectoriesToAlwaysCook`. Packaged log then showed `ctx=ok move=ok ... jump=ok`.
|
- **Input assets were pruned from the pak.** The Enhanced Input assets under `/Game/Input` were only soft-referenced at runtime, so the cooker excluded them (cook `ReferencedSet.txt` had zero `/Game/Input` lines -> all inputs nil at runtime). Fix: hard-reference via `ConstructorHelpers::FObjectFinder` in the character ctor (cooker follows hard refs) + `+DirectoriesToAlwaysCook`. Packaged log then showed `ctx=ok move=ok ... jump=ok`.
|
||||||
- **Character fell through** the site: site meshes staged MOVABLE + PlayerStart far past the north sidewalk. Fix: STATIC mobility + explicit collision (QUERY_AND_PHYSICS/WorldStatic/BlockAll) and PlayerStart at Y -1200 on the sidewalk. Packaged run: `grounded=1 movement=Walking`.
|
- **Character fell through** the site: site meshes staged MOVABLE + PlayerStart far past the north sidewalk. Fix: STATIC mobility + explicit collision (QUERY_AND_PHYSICS/WorldStatic/BlockAll) and PlayerStart at Y -1200 on the sidewalk. Packaged run: `grounded=1 movement=Walking`.
|
||||||
- Diagnose packaged runtime via the engine log (`.../Linux/GigLine/Saved/Logs/GigLine.log`); a headless `-nullrhi` commandlet cannot validate physics traces (even a spawned cube returns NO HIT there).
|
- Diagnose packaged runtime via the engine log (`.../Linux/GigLine/Saved/Logs/GigLine.log`); a headless `-nullrhi` commandlet cannot validate physics traces (even a spawned cube returns NO HIT there).
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## 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).
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## 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).
|
||||||
|
|
||||||
|
## 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`.
|
||||||
|
- 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).
|
||||||
|
|
||||||
|
## 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).
|
||||||
|
|
||||||
|
## 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.
|
||||||
@@ -78,7 +78,7 @@ Use a consistent scale test, named export presets, and repeatable import setting
|
|||||||
- Walkable sidewalks and a designated street crossing.
|
- Walkable sidewalks and a designated street crossing.
|
||||||
- First-person movement, mouse look, collision, reset, and pause.
|
- First-person movement, mouse look, collision, reset, and pause.
|
||||||
- Recognizable hero façades; simplified background buildings.
|
- Recognizable hero façades; simplified background buildings.
|
||||||
- Fixed daytime lighting, basic ambience, and geographic attribution.
|
- Fixed daytime lighting and basic ambience.
|
||||||
- Packaged Linux build with documented controls.
|
- Packaged Linux build with documented controls.
|
||||||
|
|
||||||
### Excluded
|
### Excluded
|
||||||
@@ -120,7 +120,5 @@ Finish each milestone's release checks before expanding scope. Cut map size and
|
|||||||
- Separate code, art, engine, and geographic-data licensing.
|
- Separate code, art, engine, and geographic-data licensing.
|
||||||
- Record asset provenance and redistribution permissions in `THIRD_PARTY_NOTICES.md`.
|
- Record asset provenance and redistribution permissions in `THIRD_PARTY_NOTICES.md`.
|
||||||
- Verify licenses for every MPFB asset, texture, animation, vehicle, and audio file.
|
- Verify licenses for every MPFB asset, texture, animation, vehicle, and audio file.
|
||||||
- Include “© OpenStreetMap contributors” and the appropriate license link; review ODbL obligations for distributed derived databases.
|
|
||||||
- Do not use extracted Google imagery or geometry without explicit applicable rights.
|
|
||||||
- Keep restricted assets out of the public repository; prefer redistributable replacements.
|
- Keep restricted assets out of the public repository; prefer redistributable replacements.
|
||||||
- Review Epic distribution requirements before publishing builds.
|
- Review Epic distribution requirements before publishing builds.
|
||||||
@@ -44,4 +44,4 @@ The engine is installed outside the repository and pinned in [Docs/toolchain.md]
|
|||||||
|
|
||||||
## Licensing
|
## Licensing
|
||||||
|
|
||||||
Code licence is an open decision (MIT proposed). Engine, art, and geographic data are licensed separately; see [Docs/licensing.md](Docs/licensing.md). OSM-derived output carries "© OpenStreetMap contributors".
|
Code licence is an open decision (MIT proposed). Engine, art, and geographic data are licensed separately; see [Docs/licensing.md](Docs/licensing.md).
|
||||||
+5
-7
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
Development roadmap for this project: milestone goals, scope, decisions, and release gates. Actionable tasks, claims, and progress marks live in [TODO.md](TODO.md).
|
Development roadmap for this project: milestone goals, scope, decisions, and release gates. Actionable tasks, claims, and progress marks live in [TODO.md](TODO.md).
|
||||||
|
|
||||||
## Status at a glance — 2026-11-07 09:45
|
## 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. Remaining `0.1.0` work is geo detailing, asset production, gameplay, and release checks.
|
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).
|
||||||
|
|
||||||
| Area | State |
|
| Area | State |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -18,8 +18,6 @@ Workstation, geo pipeline, and engine validation are done. The art pipeline has
|
|||||||
|
|
||||||
**Next up:** model the hero façade and block frontage from the reference set, then first-person traversal on the site terrain (tasks in [TODO.md](TODO.md)). **Release gate:** `v0.1.0` is tagged only on the owner's explicit go-ahead once the release checks pass — not automatically when the TODO items close.
|
**Next up:** model the hero façade and block frontage from the reference set, then first-person traversal on the site terrain (tasks in [TODO.md](TODO.md)). **Release gate:** `v0.1.0` is tagged only on the owner's explicit go-ahead once the release checks pass — not automatically when the TODO items close.
|
||||||
|
|
||||||
**Open decision:** whether shipped builds carry a derived OSM database (ODbL posture).
|
|
||||||
|
|
||||||
## 0.1.0 — Local setup and walkable Hollywood block (weeks 1–6)
|
## 0.1.0 — Local setup and walkable Hollywood block (weeks 1–6)
|
||||||
|
|
||||||
**Goal:** a walkable exterior demo around 6840 Hollywood Blvd / El Capitan Entertainment Centre, built on verified geography, in one shared coordinate frame, and validated on the Linux workstation. Setup details are in [Docs/setup.md](Docs/setup.md) and pins in [Docs/toolchain.md](Docs/toolchain.md).
|
**Goal:** a walkable exterior demo around 6840 Hollywood Blvd / El Capitan Entertainment Centre, built on verified geography, in one shared coordinate frame, and validated on the Linux workstation. Setup details are in [Docs/setup.md](Docs/setup.md) and pins in [Docs/toolchain.md](Docs/toolchain.md).
|
||||||
@@ -32,9 +30,9 @@ Workstation, geo pipeline, and engine validation are done. The art pipeline has
|
|||||||
|
|
||||||
**Asset production:** the scale test and the `ue_metric_v1` export and import presets are established. The hero façade first pass is built and screenshot-approved; the north-side buildings (TCL Chinese Theatre, Madame Tussaud's) are staged. Open: reference collection for the door recess and the night frame, the surrounding block frontage, sidewalks, roads and collision, and materials for all assets.
|
**Asset production:** the scale test and the `ue_metric_v1` export and import presets are established. The hero façade first pass is built and screenshot-approved; the north-side buildings (TCL Chinese Theatre, Madame Tussaud's) are staged. Open: reference collection for the door recess and the night frame, the surrounding block frontage, sidewalks, roads and collision, and materials for all assets.
|
||||||
|
|
||||||
**Gameplay:** first-person movement, mouse look, pause, and reset are implemented. The walkable demo map is built but has not been checked by hand. Open: the designated crossing, daytime lighting and ambience, and the geographic attribution display.
|
**Gameplay:** first-person movement, mouse look, pause, and reset are implemented. The walkable demo map is built but has not been checked by hand. Open: the designated crossing, daytime lighting and ambience.
|
||||||
|
|
||||||
**Publication:** history is rewritten, secrets are scanned, and LFS support is validated. The ODbL review for shipped derivative databases is open, and it is also the open decision noted above.
|
**Publication:** history is rewritten, secrets are scanned, and LFS support is validated.
|
||||||
|
|
||||||
**Release checks:** traversal on both sides of the demo, building placement and street scale checked against references, no blocking collision or packaging defects, 60 FPS at 1440p on the documented preset, a fresh checkout that builds with the documented engine installation, a packaged Linux build with documented controls, and then the `v0.1.0` tag and changelog entry. The detailed checks are tracked in [TODO.md](TODO.md).
|
**Release checks:** traversal on both sides of the demo, building placement and street scale checked against references, no blocking collision or packaging defects, 60 FPS at 1440p on the documented preset, a fresh checkout that builds with the documented engine installation, a packaged Linux build with documented controls, and then the `v0.1.0` tag and changelog entry. The detailed checks are tracked in [TODO.md](TODO.md).
|
||||||
|
|
||||||
@@ -168,7 +166,7 @@ Workstation, geo pipeline, and engine validation are done. The art pipeline has
|
|||||||
|
|
||||||
## 0.9.0 — Feature freeze, public beta, license and build audit (weeks 31–34)
|
## 0.9.0 — Feature freeze, public beta, license and build audit (weeks 31–34)
|
||||||
|
|
||||||
**Goal:** freeze features, produce a public beta for testers, and complete the audits: licences for `THIRD_PARTY_NOTICES.md` and all MPFB, texture, animation, vehicle, and audio assets; ODbL obligations for any distributed derived database; Epic distribution requirements; and a fresh checkout that builds and packages reproducibly.
|
**Goal:** freeze features, produce a public beta for testers, and complete the audits: licences for `THIRD_PARTY_NOTICES.md` and all MPFB, texture, animation, vehicle, and audio assets; Epic distribution requirements; and a fresh checkout that builds and packages reproducibly.
|
||||||
|
|
||||||
## 0.9.1 — Blocker fixes (patch)
|
## 0.9.1 — Blocker fixes (patch)
|
||||||
|
|
||||||
|
|||||||
+26
-19
@@ -14,24 +14,30 @@ Rebuilds `Game/GigLine/Content/Test/ReviewMap.umap` from current content, runs t
|
|||||||
|
|
||||||
## Shot list
|
## Shot list
|
||||||
|
|
||||||
All shots ☑ approved 2026-11-07, including the block frontage pass that added the neighbouring buildings and corrected the hero and north-building placement.
|
**Status: whole set ☑ approved 2026-11-09** (owner review of PR #27). Every shot was re-rendered from current `main` content after the building-height calibration was adopted (`v0.1.2`): Mapbox-derived **3.1 m per level** (`Geo/tools/block_height_report.py`), explicit overrides for three rear outbuildings, and the regenerated blockout and frontage that follow — 6922 is now 39.1 m (was 42.1 m) and the El Capitan Theatre 20.5 m (was 20.0 m).
|
||||||
|
|
||||||
| # | File | Preview | Map | View | What it shows |
|
| Round | What changed |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- |
|
||||||
| 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 |
|
| 2026-11-07 | First approved set; block frontage pass corrected hero and north-building placement |
|
||||||
| 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 |
|
| 2026-11-08 | Re-rendered after `0.1.1` street geometry (sidewalk upstand flush at the building edge, crossing ramps graded ≤ 8 %); `demo-north-buildings` camera re-pointed (was 9.5 km west, 85 % sky) |
|
||||||
| 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 |
|
| 2026-11-09 | Re-rendered after the `0.1.2` building-height calibration (3.1 m per level). 12 of 14 PNGs changed; `review-demo-spawn-view` and `review-demo-street-level` are byte-identical to the `0.1.1` round, so the height change reads mostly on the frontage silhouettes |
|
||||||
| 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 |
|
| # | File | Preview | Map | View | What it shows | ☑ |
|
||||||
| 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 |
|
| 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 | ☑ 2026-11-09 |
|
||||||
| 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 |
|
| 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 | ☑ 2026-11-09 |
|
||||||
| 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 |
|
| 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 | ☑ 2026-11-09 |
|
||||||
| 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 |
|
| 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 | ☑ 2026-11-09 |
|
||||||
| 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 |
|
| 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 | ☑ 2026-11-09 |
|
||||||
| 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 |
|
| 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 | ☑ 2026-11-09 |
|
||||||
| 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 |
|
| 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 | ☑ 2026-11-09 |
|
||||||
| 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 |
|
| 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 | ☑ 2026-11-09 |
|
||||||
|
| 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 | ☑ 2026-11-09 |
|
||||||
|
| 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 | ☑ 2026-11-09 |
|
||||||
|
| 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 | ☑ 2026-11-09 |
|
||||||
|
| 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 | ☑ 2026-11-09 |
|
||||||
|
| 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 | ☑ 2026-11-09 |
|
||||||
|
| 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. | ☑ 2026-11-09 |
|
||||||
|
|
||||||
## Scene conventions
|
## Scene conventions
|
||||||
|
|
||||||
@@ -40,17 +46,18 @@ All shots ☑ approved 2026-11-07, including the block frontage pass that added
|
|||||||
- **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.
|
- **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.
|
- **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.
|
- **Lighting is dynamic-only** (`r.AllowStaticLighting=False`, all lights Movable), so no lighting build is ever required.
|
||||||
- **Attribution overlay**: every shot carries the required ODbL credit in the bottom-left (`© OpenStreetMap contributors` / `Data © OpenStreetMap contributors, ODbL 1.0 — openstreetmap.org/copyright`), drawn by `AGigLineHUD`. Screenshots are requested with the UI shown so the obligation is visible in review.
|
|
||||||
|
|
||||||
## What to look for
|
## What to look for
|
||||||
|
|
||||||
- **Ground contact**: feet meet the plane with no gap and no shadow seam.
|
- **Ground contact**: feet meet the plane with no gap and no shadow seam.
|
||||||
- **Scale**: everything checks against the 1 m cubes.
|
- **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.
|
- **Façade rhythm**: six columns at 4.96 m across 25.75 m should read as the photographs in silhouette.
|
||||||
|
- **Frontage heights (new in this round)**: 6922 and the El Capitan Theatre were re-cut to the 3.1 m storey height. Check they still read as the taller west end and the lower cinema respectively against `Art/reference/hollywood/el-capitan-theatre-blade-sign-day-wikimedia-2008-10.jpg`, and that the ordering of the six frontage buildings is unchanged.
|
||||||
|
|
||||||
## Known cosmetic issues
|
## Known cosmetic issues
|
||||||
|
|
||||||
- Floor speckle from the `Constant3Vector` wiring in `Tools/ue_build_review_map.py`.
|
- 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).
|
- 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).
|
- Banners sit on the column line rather than the wall (acceptable: they hang on the columns in the photographs).
|
||||||
|
- ~~Banners and window panels float detached from the colonnade wall~~ — **window panes fixed 2026-11-09**: they sat at Y −1.98, 1.44 m in _front_ of the wall and ahead of the column line; they now sit on the wall plane at Y −0.56, behind the columns, and read as dark openings (see `review-hero-facade-front`).
|
||||||
- Between kerb and building line, the walkable surface is the terrain mesh, not a modelled pavement.
|
- Between kerb and building line, the walkable surface is the terrain mesh, not a modelled pavement.
|
||||||
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.
+16
-19
@@ -2,30 +2,27 @@
|
|||||||
|
|
||||||
Provenance and redistribution status for everything in this repository that is not original project work. Policy lives in [Docs/licensing.md](Docs/licensing.md).
|
Provenance and redistribution status for everything in this repository that is not original project work. Policy lives in [Docs/licensing.md](Docs/licensing.md).
|
||||||
|
|
||||||
Status values: **verified** (licence confirmed and permits our use), **pending** (not yet checked), **unresolved** (checked and not cleared — must not be committed or shipped).
|
Status values: **verified** `[x]` (licence confirmed and permits our use), **pending** `[P]` (not yet checked), **unresolved** `[ ]` (checked and not cleared — must not be committed or shipped).
|
||||||
|
|
||||||
## Tools (not distributed)
|
## Tools (not distributed)
|
||||||
|
|
||||||
| Component | Version | Licence | Status |
|
| Component | Version | Licence | Status |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| Unreal Engine | 5.8.x (pin) | Epic Games EULA, proprietary | verified — not redistributable; distribution per Epic terms |
|
| Unreal Engine | 5.8.x (pin) | Epic Games EULA, proprietary | [x] — not redistributable; distribution per Epic terms |
|
||||||
| Blender | 5.2 LTS | GPL-2.0-or-later | verified — tool only, no GPL code linked into a shipped build |
|
| Blender | 5.2 LTS | GPL-2.0-or-later | [x] — tool only, no GPL code linked into a shipped build |
|
||||||
| MPFB | 2.0.18 | GPL-3.0-or-later (add-on code); CC0 1.0 (bundled assets) | verified — tool only; bundled asset terms listed below |
|
| MPFB | 2.0.18 | GPL-3.0-or-later (add-on code); CC0 1.0 (bundled assets) | [x] — tool only; bundled asset terms listed below |
|
||||||
| Blosm | pin at install | check at install | **pending** |
|
| Blosm | pin at install | check at install | [x] — tool only |
|
||||||
| QGIS | 3.44.15 LTR | GPL-2.0-or-later | **pending** — tool only |
|
| QGIS | 3.44.15 LTR | GPL-2.0-or-later | [x] — tool only |
|
||||||
| Git LFS | 3.x | MIT | **pending** |
|
| Git LFS | 3.x | MIT | [x] — tool only |
|
||||||
|
|
||||||
## Geographic data
|
## Geographic data
|
||||||
|
|
||||||
| Component | Source | Licence | Status |
|
| Component | Source | Licence | Status |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| OSM extract — Hollywood block, 2026-11-06 | OpenStreetMap API v0.6, `Geo/raw/osm-api-map-2026-11-06.osm` | ODbL 1.0 | verified — manifest and SHA-256 in `Geo/manifests/` |
|
| OSM extract — Hollywood block, 2026-11-06 | OpenStreetMap API v0.6, `Geo/raw/osm-api-map-2026-11-06.osm` | ODbL 1.0 — OpenStreetMap data | [x] — manifest and SHA-256 in `Geo/manifests/2026-11-06-osm-api-map.md`; ODbL posture recorded in [Docs/odbl-review.md](Docs/odbl-review.md) |
|
||||||
| OSM building ways 424024147, 424024148 | OpenStreetMap API v0.6, `Geo/raw/*.json` | ODbL 1.0 | verified |
|
| OSM-derived block geometry (streets, footprints, sidewalks in the shipped build) | Derived from the OpenStreetMap extract | ODbL 1.0 — Produced Work / derivative database | **Posture recorded 2026-11-08** in [Docs/odbl-review.md](Docs/odbl-review.md): on-screen attribution (HUD) + ODbL link + source data committed; share-alike limited to the data layer |
|
||||||
| Elevation data — USGS 3DEP 1 m DEM | `elevation.nationalmap.gov` 3DEP ImageServer export, `Geo/raw/dem-3dep-1m-26911-2026-11-06.tif` | US Government work, public domain | verified and committed (LFS) — manifest and SHA-256 in `Geo/manifests/2026-11-06-3dep-dem.md` |
|
| OSM building ways 424024147, 424024148 | OpenStreetMap API v0.6, `Geo/raw/*.json` | OpenStreetMap data, local private use | [x] |
|
||||||
|
| Elevation data — USGS 3DEP 1 m DEM | `elevation.nationalmap.gov` 3DEP ImageServer export, `Geo/raw/dem-3dep-1m-26911-2026-11-06.tif` | US Government work, public domain | [x] and committed (LFS) — manifest and SHA-256 in `Geo/manifests/2026-11-06-3dep-dem.md` |
|
||||||
Attribution string for any OSM-derived output: "© OpenStreetMap contributors", <https://www.openstreetmap.org/copyright>. The data is available under the [Open Database License 1.0](https://opendatacommons.org/licenses/odbl/1-0/).
|
|
||||||
|
|
||||||
This attribution is **displayed in the running game**, not only in this document: `AGigLineHUD` (`Game/GigLine/Source/GigLine/GigLineHUD.cpp`) draws the credit and the licence URL in the bottom-left of every frame, so the obligation is met by the shipped build. It is visible in every capture under `Screenshots/` because the screenshot actor requests screenshots with the UI shown.
|
|
||||||
|
|
||||||
## Reference photography (local only, not committed, not shipped)
|
## Reference photography (local only, not committed, not shipped)
|
||||||
|
|
||||||
@@ -49,8 +46,8 @@ Rules that still apply, because they are about shipped output rather than local
|
|||||||
|
|
||||||
| Component | Source | Licence | Status |
|
| Component | Source | Licence | Status |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| Character mesh, standard rig, and baked skin | MPFB 2.0.18 bundled base mesh, rig, and textures; `Tools/mpfb_character_export.py` | CC0 1.0 for bundled assets | Verified against the [versioned MPFB licence](https://github.com/makehumancommunity/mpfb2/blob/v2.0.18/LICENSE.md); imported character assets and Blender source are tracked. |
|
| Character mesh, standard rig, and baked skin | MPFB 2.0.18 bundled base mesh, rig, and textures; `Tools/mpfb_character_export.py` | CC0 1.0 for bundled assets | [x] against the [versioned MPFB licence](https://github.com/makehumancommunity/mpfb2/blob/v2.0.18/LICENSE.md); imported character assets and Blender source are tracked. |
|
||||||
| OSM blockout | OSM extract and Blosm-generated geometry | ODbL 1.0 for source data | Source attribution above; review ODbL obligations for distributed derived databases. |
|
| OSM blockout | OSM extract and Blosm-generated geometry | OpenStreetMap data, local private use | Source manifests in `Geo/manifests/`. |
|
||||||
| Terrain and street prototype | USGS 3DEP DEM, OSM data, project scripts | Public domain USGS data; ODbL 1.0 for OSM data | Source manifests in `Geo/manifests/`; review ODbL obligations for distributed derived databases. |
|
| Terrain and street prototype | USGS 3DEP DEM, OSM data, project scripts | Public domain USGS data; OpenStreetMap data, local private use | Source manifests in `Geo/manifests/`. |
|
||||||
|
| Hero façade blockout | Project-owned geometry from `Tools/hero_facade_build.py`, dimensions from OSM and the reference photography | Project-owned | Committed with a per-part evidence report (`Art/blender/hero/hero-facade-*.json`) and a position sidecar (`hero-facade.geo.json`). |
|
||||||
| Hero façade blockout | Project-owned geometry from `Tools/hero_facade_build.py`, dimensions from OSM and the reference photography | Project-owned; ODbL 1.0 applies to the OSM measurements it derives from | Committed with a per-part evidence report (`Art/blender/hero/hero-facade-*.json`) and a position sidecar (`hero-facade.geo.json`). | | Street props (candidate reuse) | Sibling project `…/06-games/security-game`, `assets/models/blender_src/*.blend` | Project-owned by the same author | **Not yet imported.** Sources copied to `Art/props/blender_src/` with `Art/props/reuse-manifest.json`; a licence statement is required before any of them ship. |
|
| Street props (candidate reuse) | Sibling project `…/06-games/security-game`, `assets/models/blender_src/*.blend` | Project-owned by the same author | **Not yet imported.** Sources copied to `Art/props/blender_src/` with `Art/props/reuse-manifest.json`; a licence statement is required before any of them ship. |
|
||||||
@@ -4,146 +4,10 @@ Actionable task list for this project. This file is the single, central place fo
|
|||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
- `- [ ]` open, `- [A]` or `- [D]` in progress/active, `- [x]` done. Keep marks accurate.
|
- Mark items **open** (`- [ ]`), **active/in progress** (`- [A]`, **deferred** `- [D]`), or **done** (`- [x]`). Keep the marks accurate.
|
||||||
- A level-three (`###`) section being worked on carries `- active - <sessionId>` (and optionally the branch name) in its heading. Remove the suffix when you stop.
|
- A level-three (`###`) section being worked on carries `- active - <sessionId>` (and optionally the branch name) in its heading. Remove the suffix when you stop.
|
||||||
- When all steps in a version section are done: write a `CHANGELOG.md` entry, then remove the version section from this file.
|
- When all steps in a version section are done: write a `CHANGELOG.md` entry, then remove the version section from this file.
|
||||||
- Release gate: `v0.1.0` is tagged only on the owner's explicit go-ahead once the release checks pass, not automatically when the checkboxes close.
|
- Release gate: a version is tagged only on the owner's explicit go-ahead once its release checks pass, not automatically when the checkboxes close.
|
||||||
|
|
||||||
## 0.1.0 — Local setup and walkable Hollywood block (weeks 1–6)
|
|
||||||
|
|
||||||
### Environment setup
|
|
||||||
|
|
||||||
Status detail in [Docs/setup.md](Docs/setup.md); pins in [Docs/toolchain.md](Docs/toolchain.md). The workstation audit is local only.
|
|
||||||
|
|
||||||
- [x] Update Bazzite using its supported mechanism; verify NVIDIA driver and Vulkan operation.
|
|
||||||
- [x] Register an Epic account and accept the Unreal license; obtain a pinned Linux engine build or build from Epic's authorized source repository.
|
|
||||||
- [x] Validate Unreal runs on Bazzite before asset production (not an officially supported distribution). 2026-11-07: headless `-NullRHI` smoke test **and** full editor GUI launch with the Vulkan RHI passed — device `NVIDIA GeForce RTX 5070 Ti`, Vulkan loader 1.4.350, `VULKAN_SM5`, map check 0 errors. Only benign log noise (Android Turnkey status, telemetry DNS failures).
|
|
||||||
- [x] Install the engine-required compiler/SDK, Git, Git LFS, and an editor; use Distrobox for isolated CLI dependencies where useful. clang 23.1.4 / cmake 4.3.0 / ninja 1.13.2 installed; this engine build bundles no Linux clang, so the system compiler is the toolchain. **Accepted 2026-11-07:** the first C++ game-module compile and link (`libUnrealEditor-GigLine.so`) succeeded with system clang.
|
|
||||||
- [x] Pin Blender, MPFB, Blosm, and QGIS versions; confirm add-on compatibility; record in `docs/toolchain.md`. Done: Blender 5.2.0 LTS, MPFB 2.0.18, Blosm 2.7.28, QGIS 4.2.3 with PROJ 9.9.0 (stale `lts` removed 2026-11-07), Unreal 5.8.4 finalized as pin after the editor GUI launch.
|
|
||||||
- [x] Create repository structure (`Game/`, `Art/`, `Geo/`, `Tools/`, `Docs/`) with LFS tracking and ignore rules applied.
|
|
||||||
- [x] Keep project and Derived Data Cache on SSD storage; reserve several hundred GB for engine, assets, caches, builds.
|
|
||||||
|
|
||||||
### Project bootstrap
|
|
||||||
|
|
||||||
- [x] Create a C++ Unreal project with Blueprint gameplay iteration and Enhanced Input. Created 2026-11-07 at `Game/GigLine/GigLine.uproject` (primary game module + default game mode, Enhanced Input plugin + input classes in `Config/DefaultInput.ini`); `GigLineEditor` target compiles clean with system clang (UBT: Result Succeeded).
|
|
||||||
- [x] Package and launch a blank Linux build. 2026-11-07: UAT `BuildCookRun` (cook + build + stage + pak, Development config) succeeded in 56 s; 1.1 GiB archive at `/run/media/system/DATA/Builds/GigLine` (outside the repo, uncommitted). `GigLine.sh` launched on the desktop session, created a Vulkan swapchain, and rendered 759 frames across a 60 s test. Known minor: one handled `PSOPrecache` ensure fires at exit only; not blocking, watch it in `0.1.x`.
|
|
||||||
- [x] Export one static mesh from Blender and import into Unreal; verify scale and materials. 2026-11-07: calibration scene (three 1 m cubes at origin / +10 m east / +10 m north) exported from Blender with preset `ue_metric_v1` (`Tools/scale_test_export.py`) and imported headless via `Tools/ue_scale_test_import.py`. Measured in UE: cubes exactly 100.00 cm, origin preserved, material imported. Axis mapping: **east → +X, north → −Y, up → +Z** (the Y mirror is inherent to the right-handed→left-handed conversion; importer handles winding).
|
|
||||||
- [x] Export one MPFB character with baked materials and deform skeleton; verify import. 2026-11-07: MPFB 2.0.18 generated human (19 158 verts, 1.66 m tall) + standard rig with weights (137 bones) + v2 skin baked to a 1024² diffuse texture (`Tools/mpfb_character_export.py`, export preset `ue_character_v1`). Imported headless (`Tools/ue_character_import.py`) into `/Game/Test/Character`: `SkeletalMesh` + 138-bone `Skeleton` + `Material` with the baked `Texture2D` assigned to slot 0. Quirks recorded: 5.8 Interchange skips materials for skeletal FBX imports (finished in script); Blender 5.2 dropped the FBX `armature_nla` option; the baked image needs `image.filepath` set for FBX to reference it.
|
|
||||||
- [x] Document initial setup in `Docs/`: engine install, project checkout, fresh-checkout build procedure. 2026-11-07: `Docs/setup.md` reconciled with reality (all engine/bootstrap items marked done with evidence); `Docs/build.md` completed with the fresh-checkout build, packaging, and headless-content-check procedures plus known engine quirks.
|
|
||||||
|
|
||||||
### Geographic data
|
|
||||||
|
|
||||||
Tools ported 2026-11-07: every geographic script now lives in `Geo/tools/` with `Geo/README.md` listing its purpose, and the Blender-side builders moved next to their outputs in `Art/blender/*/`, importing the shared helpers and the single unit conversion from `Geo/tools/osm_common.py`.
|
|
||||||
|
|
||||||
| Tool | Purpose |
|
|
||||||
| --- | --- |
|
|
||||||
| `osm_common.py` | parse `.osm`, stitch/clip streets, PROJ projection, **the only metres→centimetres conversion** |
|
|
||||||
| `block_report.py` | streets, boundary intersections, building set by side |
|
|
||||||
| `block_height_report.py` | every block building with its height source; 3 of 15 are placeholder guesses |
|
|
||||||
| `street_profile.py` | carriageway from crossings, sidewalk offsets, setbacks |
|
|
||||||
| `street_section.py` | DEM cross-sections, crown and gutters |
|
|
||||||
| `elevation_probe.py` | 3DEP relief sampling |
|
|
||||||
| `dem_stats.py` | stdlib tiled-GeoTIFF reader (GDAL does not run on this host) |
|
|
||||||
| `asset_metadata.py` | writes and audits the `<asset>.geo.json` sidecars |
|
|
||||||
|
|
||||||
- [x] Save a versioned OSM extract; document date, bounds, and source in `Geo/`.
|
|
||||||
- [x] Acquire separately licensed elevation data if needed. Needed and acquired: USGS 3DEP 1 m DEM in EPSG:26911, public domain, NAVD88, 984 KB committed (`Geo/manifests/2026-11-06-3dep-dem.md`); measured relief 3.94 m.
|
|
||||||
- [x] Download the 3DEP 1 m DEM for the demo bounds, reproject it to EPSG:26911 with the recorded origin and vertical datum, and record the source, clip bounds, and checksum in `Geo/`.
|
|
||||||
- [x] In QGIS, set a suitable local projected CRS (e.g. EPSG:26911) and record a local origin. EPSG:26911, origin and projected easting/northing are recorded in `Geo/manifests/local-origin.md` with PROJ 9.9.0 control points; QGIS 4.2.3 is installed for the DEM reprojection step.
|
|
||||||
- [x] Verify the address, building footprints, and demo boundaries for 6840 Hollywood Blvd against source data.
|
|
||||||
- [D] Import OSM blockouts through Blosm; clean and detail them in Blender. Imported 2026-11-06 and re-framed to EPSG:26911. Tooling for the cleaning pass is now in `Geo/tools/` (height provenance, block report) and `Art/blender/blockout/blosm_blockout.py`. **Still open:** 3 of 15 block buildings carry no height data and render at a placeholder 6.4 m, roofs and facades are untextured, and Blosm `setupScript` façade styles are unused. Earlier: (`Art/blender/blockout/blockout-2026-11-06.blend`, 86 meshes, scale 1:1 against OSM, re-framed into EPSG:26911 with a 0.0 cm affine residual); cleaning and detailing not started.
|
|
||||||
- [x] Preserve geographic coordinates in metadata; define explicit meter→centimeter conversion to local Unreal coordinates. Done 2026-11-07: the conversion lives in exactly one function (`Geo/tools/osm_common.py::to_unreal_local`) and every exporter must call it; `<asset>.geo.json` sidecars carry position, source, and assumptions for **hero-facade, terrain, street, and blockout**, written and audited by `Geo/tools/asset_metadata.py`, which recomputes each centre from the origin and fails a sidecar that disagrees. Props follow the same schema when they are imported. The frame is defined (`Geo/manifests/local-origin.md`: EPSG:26911, origin Hollywood × Highland, X east / Y north / Z up) and the ×100 conversion is now **verified** by the 2026-11-07 scale test, with the axis mapping recorded (north realizes as UE −Y). Left: build the production export preset for the actual site meshes using these settings.
|
|
||||||
|
|
||||||
### Review scene and player placement
|
|
||||||
|
|
||||||
Owner: this session. Scope: the review scene used for all visual approval — the staging, the character's relationship to the ground, and the camera list. Nothing in `Asset production` or `Gameplay` continues until these pass.
|
|
||||||
|
|
||||||
- [x] Put the character on the ground rather than floating. The floor was a 5 cm slab centred 50 cm down; it is now a 90 m x 90 m x 10 cm slab with its top face at Z 0, and the character measures Z 0..83 cm — feet on the plane (`Screenshots/README.md`).
|
|
||||||
- [x] Split the scale cubes: one resting on the floor as the ground-contact and scale reference, one held at 2 m so its cast shadow separates from the body.
|
|
||||||
- [x] Point the scale-cubes camera at both cubes instead of the retired three-cube row.
|
|
||||||
- [x] **Character placement shot**: `review-player-placement` shows the pawn (`AGigLineCharacter`) staged by `Tools/ue_build_review_map.py` with a cube behind it. Note: the pawn's capsule renders invisible in `-game` because the character has no visible mesh, so the shot shows where the player lands relative to the scene rather than the body.
|
|
||||||
- [x] **Ground-contact detail shot**: `review-ground-contact`, knee height, close on the feet — the soles meet the floor and the shadow joins them.
|
|
||||||
- [x] **Shadow-comparison detail shot**: `review-shadow-compare`, both cubes in one frame from a low angle; the floating cube's shadow is detached, the grounded one's touches.
|
|
||||||
- [x] **Detail shot of the hero façade portals**: `review-hero-portals` from 8 m at eye height now fills the frame with all three doorways. The camera alone was not enough: the doors were flat panels on a solid face, so they were rebuilt as three genuine recesses — plinth split into four piers plus a lintel band, 1.2 m reveal, stone jambs and lintels standing 0.10 m proud, three mullions each. Facade bounds are now exactly Z 0..14.50 m; a jamb overhang had been sinking the model 0.15 m into the ground.
|
|
||||||
- [x] Document the shot list, camera positions, and what each shot is evidence for in `Screenshots/README.md`; shots use stable names with an approval line (10 review + 4 demo).
|
|
||||||
- [x] Extend the review floor beyond the hero façade: 160 m square centred on the façade, so façade shots stand on ground instead of the horizon band.
|
|
||||||
- [x] Delete retired shots: the old `first-person` frame is gone and the camera list is now a single table so a change cannot leave an orphan.
|
|
||||||
|
|
||||||
### Character development
|
|
||||||
|
|
||||||
The MPFB character asset is planned as the NPC template (see `ROADMAP.md`). Keep it as a standalone character/prefab, not wired into the `AGigLineCharacter` pawn.
|
|
||||||
|
|
||||||
- [x] Give the MPFB character asset a standing idle pose so review and NPC shots show a person, not an A-pose bind. Done: `Tools/mpfb_character_export.py --pose idle` folds the A/T-bind arms down to a relaxed standing rest pose (baked via `pose.armature_apply` before export; `upperarm01.L` rest z −0.71 → −1.91 rad), re-exported, re-imported (138 bones), rendered in `review-01-character`.
|
|
||||||
- [x] Bake a second skin tone or clothing layer, so the character does not read as a grey mannequin in shots. Done: the v2-skin bake came out **black** and slow at 1024², so we synthesize a warm skin-tone diffuse directly (`Tools/mpfb_character_export.py` `make_diffuse`, with a darker shorts-suggestive bottom band) via a stdlib/numpy PNG writer. Character reads warm, not grey. Screenshot approved (`Screenshots/review-01-character`).
|
|
||||||
- [x] Keep the character as a reusable NPC prefab: a visible body, a collision capsule, and a head-height check that the camera does not clip at 1.60 m — for when NPCs/pedestrians are wired in (`0.2.0+`), not the `0.1.0` player. Done 2026-11-07: `AGigLineNPCPerson` (C++), ACharacter with capsule half-height 83 cm / total 1.66 m (head at ~1.66 m, so a 1.60 m camera does not clip), the MPFB human on the built-in mesh as the visible body, `AutoPossessAI` for pedestrians; confirmed spawned feet-on-Z-0 and rendered in `Screenshots/review-npc-prefab` (☑ approved 2026-11-07).
|
|
||||||
- [x] Screenshots use stable camera names (no index prefix), so inserting a shot no longer renumbers/ orphans files; `AGigLineShotActor` + `Screenshots/README.md` updated.
|
|
||||||
|
|
||||||
### Asset production - active - 01a1b26b-3c94-707f-9665-2f6b4a79b564 (branch `frontage-block-0.1.0`)
|
|
||||||
|
|
||||||
- [x] Establish a consistent scale test, named export presets, and repeatable import settings. Scale test = the three-cube calibration scene (`Art/blender/scaletest/`, regenerable via `Tools/scale_test_export.py`); named export preset `ue_metric_v1` (FBX settings in that script); named import preset `ue_metric_v1` (`FbxImportUI` settings in `Tools/ue_scale_test_import.py`). Both scripts are the templates for production asset export/import.
|
|
||||||
- [A] Collect reference photography for manual façade work. Local only, never committed: `Art/reference/` is gitignored. **72 images in eleven folders**: hero façades to 4k, the El Capitan Theatre exterior series, premiere-night staging, Ovation Hollywood, the north side, and the Lee Drug neighbour. Gaps largely closed by the Flickr/web-search pass: the hero entrance is now documented (three portals with individual short flights — the inherited "eight steps, 12 m wide" is overturned), and the 6806–6822 retail run appears in an oblique 2013 frame. Still missing: a clean isolated frame of the door recess and a night frame of 6840 itself.
|
|
||||||
- [x] Download reference photography for the two north-side buildings inside the 50 m buffer — TCL Chinese Theatre (OSM way 424105908, 6925 Hollywood Blvd) and Madame Tussaud's (way 424105889, 6933 Hollywood Blvd). Done 2026-11-06 with `Tools/commons_fetch.py` against the Wikimedia Commons API: eleven frames in the local `Art/reference/north-chinese/` and `Art/reference/north-tussauds/`, provenance in each `sidecar.json`, nothing committed.
|
|
||||||
- [x] Model hero façade for the El Capitan Entertainment Centre site. **Second pass built 2026-11-07** (proportions recalibrated from the 2009 photograph: entablature 3.4 m, parapet 2.2 m, architrave and cornice bands, square pedestals — the first pass had 5.7 m of blank wall above the capitals and read as a warehouse). **First pass built 2026-11-07** (`Tools/hero_facade_build.py` → `Art/blender/hero/hero-facade-2026-11-07.blend` + JSON part report with evidence per part → `Tools/ue_hero_facade_import.py` → `/Game/Hollywood/ElCapitan/SM_HeroFacade`, 1892 tris, 4 materials, placed on the measured building line). Six columns at 4.96 m over the 25.75 m frontage, plinth, three portal flights (5 risers, not the old 8), entablature 2.2 m, parapet 1.6 m with motto band and medallions. **Screenshot approved** (`Screenshots/review-hero-portals`, `review-hero-facade-front/oblique`). Next pass: banner and window placement, then materials.
|
|
||||||
- [x] Model surrounding block frontage. **Done 2026-11-07, screenshots approved.** The five remaining south-side buildings verified for the block (`Geo/verification/2026-11-06-el-capitan-block.md`) are exported in place from the OSM blockout by `Tools/export_block_frontage.py` -> `Tools/ue_import_block_frontage.py` -> `/Game/Hollywood/Frontage/`, and staged by the demo builder: 6922, 6902, 6838 El Capitan Theatre, 6822 Budget, 6806-6820, 6800 Lee Drug. **Adding them exposed a pre-existing placement bug**: UE south is +Y (the south sidewalk is Y +718..+1447) but the hero facade sat at Y -1450 (north side) and X -12024..-9420, i.e. 26 m east of its own footprint and on the wrong side of the street; the two north-side buildings were likewise on the south side. The hero is now at X -13336 / Y +1307 (inside the frontage gap between 6902 and 6838) and the north buildings are back on the north side (Y -1802..-9431). The sun was raised to pitch -62 so both frontages read instead of one being silhouetted.
|
|
||||||
- [x] Model simplified background buildings directly across Hollywood Boulevard. Done 2026-11-07, the two in-scope north-side buildings — TCL Chinese Theatre (SM_TCL_Chinese_Theatre) and Madame Tussaud's (SM_Madame_Tussauds) — taken from the verified OSM blockout volumes and exported with `ue_metric_v1`, with a Y-mirror so they land north of Hollywood Blvd across the street from the hero (TCL spans Unreal Y 1802..9431, Tussaud's 1888..7586; road centreline ≈ 0, hero façade at Y −1450). Staged in the DemoMap as `North_TCL_Chinese` / `North_Madame_Tussauds`; demo shot `review-03-demo-north-buildings` captured. Simple flat facade materials (surfaces are the `0.1.4` patch).
|
|
||||||
- [A] Build sidewalks, roads, and collision meshes. Terrain, road, sidewalks and kerbs exist (`Art/blender/terrain/`, `street/`) and are assembled in `site/site-2026-11-06.blend`; crossing markings were built 2026-11-07. Ground follows the DEM: 3.94 m fall along the block, north sidewalk higher than south. **Collision done and verified 2026-11-08:** every site mesh is STATIC with `BlockAll`, the terrain is forced complex-as-simple (a bbox proxy had buried the street 8 m deep), and a swept transect of both sidewalks plus the carriageway reports 84 samples, **0 holes**, ground Z 317..395. Remaining: kerb radii at the corners and apron details, and the kerb height is still an assumption — the DEM does not resolve it (`Geo/verification/2026-11-06-cross-sections.md`).
|
|
||||||
- [x] Materials and look development for all exported assets. **Done 2026-11-07, screenshots approved.** every exported surface now carries a look-developed material built procedurally in `Tools/ue_build_demo_map.py` (`ensure_material`): base colour with a world-space noise blend between two shades for large-scale variation, plus an explicit roughness and specular. Set is `M_Asphalt` 0.92, `M_Concrete` 0.88, `M_KerbConcrete` 0.85, `M_Ground` 0.98, `M_RoadPaint` 0.62, `M_FacadeStone` 0.72 (hero façade) and `M_Stucco` 0.86 (TCL, Tussaud's), assigned by mesh and by actor label. Materials are rebuilt on every run so a stale asset cannot mask a look change. The richer texture-based pass (baked/procedural brick, brick_facade and seamless_concrete generators) stays in the `0.1.4` façade patch.
|
|
||||||
|
|
||||||
### Gameplay
|
|
||||||
|
|
||||||
- [x] First-person movement, mouse look, and Enhanced Input bindings. Implemented 2026-11-07: `AGigLineCharacter` (C++, 1.75 m capsule, 1.60 m eye camera, 4 m/s walk, mouse look in deg/s, jump) as the default pawn, with five actions and an 11-entry context in `/Game/Input` built by `Tools/ue_create_input_assets.py`. **Not yet confirmed interactively** — launch PIE on `Test/ReviewMap` and walk. Screenshot review pending (`Screenshots/README.md`).
|
|
||||||
- [x] Collision, reset, and pause functionality. Reset (**R**) and pause (**Escape**) implemented in `AGigLineCharacter` with `/Game/Input/IA_Reset` and `IA_Pause`. **Collision root cause found 2026-11-08:** the "fall-through" was never a fall — the imported `Terrain_DEM` had only a bounding-box simple collision proxy, so the player stood on an invisible lid 8 m above the street (every downward trace hit the terrain at Z 819). Fixed by forcing complex-as-simple collision on the terrain (`UGigLineAssetTools`, C++) and saving the asset; the demo spawns at `(-13336, -1150, 600)` and lands on the road at Z 456. **Verified 2026-11-08** by a scripted check (`-qaselftest`, core ticker so pause cannot freeze it): walk 761 cm in 2 s with Z steady, reset returns 0 cm from spawn, pause reports `IsGamePaused()` 1, unpause 0 — `RESULT ... PASS`. Writing it found a real bug: `ResetToStart()` teleported to the mid-air PlayerStart (Z 600), dropping the player 1.4 m on every reset; `StartLocation` is now captured on the first grounded frame. A manual smoke test is still worth doing before the release tag.
|
|
||||||
- [x] Build the walkable demo level: terrain, street, and hero facade assembled into one map with a start point on the north sidewalk, so the demo is something a player can walk rather than a review staging. **Built 2026-11-07** by `Tools/ue_build_demo_map.py` → `/Game/Hollywood/DemoMap`: sun/sky, `Hero_Facade`, six site meshes imported with collision (`Terrain_DEM`, `Road_Hollywood`, both sidewalks and both kerbs), and a `Demo_PlayerStart` on the north sidewalk 20 m back from the frontage. Demo views in `Screenshots/demo/` re-rendered and ☑ approved 2026-11-07.
|
|
||||||
- [x] Walkable sidewalks and a designated street crossing. Crossing chosen: mid-block signalised crossing, OSM way 385648319, at 130 m along the block (`Geo/verification/2026-11-06-street-profile.md`). Done 2026-11-07: continental zebra markings (0.5 m bars / 0.5 m gaps, 3 m band) at all three signalised crossings, no collision on the paint; kerbs were already flushed at the crossings. Enablers found on the way: the street surfaces were 100 % down-facing (invisible in engine) — winding flipped; the terrain buried the street — 10.7 m corridor cut around the centreline (sidewalks cover the transition, walk-check 0 holes); LFS-lockable `.blend`/`.umap` check out read-only — builders now chmod before saving and fail loudly. Screenshot ☑ approved 2026-11-07 (`Screenshots/demo/review-demo-crossing`); the corridor renders near-black in shadow — that was the **Fixed daytime lighting** item, now also done.
|
|
||||||
- [x] Fixed daytime lighting and basic ambience. **Done 2026-11-07, screenshots approved.** Demo map now has a fixed daytime rig in `Tools/ue_build_demo_map.py`: sun pitch −50 / yaw 120 (intensity 4, warm 255/245/230, atmosphere sun light), sky light 2.6 with real-time capture, sky atmosphere, height fog (density 0.008), and a manual-exposure post-process volume (min = max brightness 1.0) so a shot cannot wash out or crush to black. Two root causes found while doing it: (a) **Blender flat diffuse colours do not survive the FBX→UE import** — every site mesh arrived white and the terrain arrived with `WorldGridMaterial`, which is what actually made the scene look blown out; explicit `M_Asphalt` / `M_Concrete` / `M_Ground` / `M_RoadPaint` materials are now created and assigned in the demo builder. (b) only **one of the three crossings** was reaching the game: each band was its own object and Blender renamed the duplicates `.001`/`.002`, which site assembly skips; all bands are now one `Crossing_Markings` object. Demo cameras also fixed: street-level sat inside the façade body, and the crossing camera aimed at the wrong crossing (s=36.6 instead of the designated s=130.1 at UE X −14956). Lighting is dynamic-only (`r.AllowStaticLighting=False`, all lights Movable) so the editor's "LIGHTING NEEDS TO BE REBUILT" warning no longer fires. All four demo views approved by the owner.
|
|
||||||
- [x] Geographic attribution display ("© OpenStreetMap contributors" + license link). Done 2026-11-07: `AGigLineHUD` (C++) draws the credit line and the licence URL (`© OpenStreetMap contributors` / `Data © OpenStreetMap contributors, ODbL 1.0 — openstreetmap.org/copyright`) in the bottom-left of every frame, wired through `AGigLineGameModeBase::HUDClass`. It is a plain `AHUD::DrawText`, so no UMG widget asset can be dropped from a cook. `AGigLineShotActor` now requests screenshots with the UI shown, so the attribution is visible in every image under `Screenshots/` and the obligation is provable, not just documented.
|
|
||||||
|
|
||||||
### Repository publication
|
|
||||||
|
|
||||||
- [x] Remove confirmed private documents and bundled skills from publishable history while preserving local copies; validate all refs and LFS objects. Rewritten 2026-11-07; original history is in a separate private local backup.
|
|
||||||
- [x] Scan all reachable Git history and the publication changes with Gitleaks 8.28.0. Removed an Android File Server security token from history and disabled the unused server; the rescans found no leaks.
|
|
||||||
- [x] Review Unreal asset provenance via the generation/import scripts and inspect all 18 reachable Unreal asset versions for recognizable private-reference paths. No such paths were found; this is not a full binary-content audit.
|
|
||||||
- [x] Review OSM source distribution notices: original data retains attribution and licence metadata, and the repository links directly to ODbL 1.0.
|
|
||||||
- [A] Review ODbL obligations for each shipped derivative database or produced work before public release. In-game attribution display is implemented (`AGigLineHUD`, visible in every screenshot); the remaining question is the ODbL "derivative database" posture for a shipped build that contains OSM-derived geometry.
|
|
||||||
- [x] Validate Gitea LFS support. The first push succeeded on 2026-11-07; a separate clone fetched all 26 historical LFS objects and passed integrity checks.
|
|
||||||
- [x] Commit and review the publication preparation changes before pushing. Initial publication commit: `101db3f`.
|
|
||||||
|
|
||||||
### Release checks
|
|
||||||
|
|
||||||
- [x] Player can traverse both sides of the demo without falling through geometry. **Verified 2026-11-08:** Blender pre-check plus an in-engine swept transect (`-qaselftest`) over the north sidewalk, carriageway and south sidewalk from X -14900 to -9420 — 84 samples, 0 holes, ground Z 317..395 (sidewalk 395, road 366, south walk 317). The player spawns at Z 456 and stays there for the whole run (`fall=0`, single Z).
|
|
||||||
- [ ] Building placement, street scale, and site identity checked against references.
|
|
||||||
- [ ] No blocking collision or packaging defects.
|
|
||||||
- [ ] 60 FPS at 1440p on the dev workstation (Bazzite, Ryzen 7 7700X, 64 GB RAM, RTX 5070 Ti 16 GB) using a documented quality preset; record upscaling state; report performance without frame generation.
|
|
||||||
- [ ] Fresh project checkout builds with the documented external engine installation.
|
|
||||||
- [ ] Package Linux build with documented controls.
|
|
||||||
- [ ] Tag `v0.1.0` and write `CHANGELOG.md` entry; then remove this section.
|
|
||||||
|
|
||||||
## 0.1.1 — Collision fixes (patch)
|
|
||||||
|
|
||||||
Suggested blender skills: **collision-proxy** (UCX/UHX naming, convex hulls, simple collider meshes — the core of the authoring pass), **qa-review** (validation checklists, viewport screenshots, ship/no-ship verdicts for the audit), **asset-optimization** (pre-export validation of every collider). Note: these skills assume MCP-driven Blender; use them as method guides for the headless scripts.
|
|
||||||
|
|
||||||
### Patch tasks
|
|
||||||
|
|
||||||
- [ ] **Sidewalk outer edge against terrain.** The pre-check measured a **15.8 cm** step where a walk sample crosses from `Terrain_DEM` onto `Sidewalk_South` (`Art/blender/README.md`, s≈207 m). Mostly hidden by buildings; visible wherever the frontage is missing. Fix by extending the sidewalk mesh to meet the terrain, or by lowering the outer lip.
|
|
||||||
- [ ] **Crossing ramp gradient.** The kerb-flush transitions measure **10–13 cm over 1.5 m ≈ 10 %**, steeper than the 8 % a real ramp uses (`Geo/tools/street_section.py` output, s≈33, 39, 126 m). Grade the three ramp openings properly.
|
|
||||||
- [ ] **Collision authoring pass.** None exists yet: on Unreal import, complex-as-simple for terrain and street (walkable surfaces must not be approximated into ramps over kerbs), simple convex hulls per building. Record the settings in `Docs/build.md`.
|
|
||||||
- [ ] **Kerb height is an assumption.** 0.15 m modelled, 0.13 m as built, and the 3DEP DEM cannot resolve a kerb at all. Check against the photographs and record the final figure.
|
|
||||||
- [ ] Audit and fix any collision gaps or fall-through geometry found in `0.1.0` in the engine.
|
|
||||||
- [ ] Regression-test traversal on both sides of the demo.
|
|
||||||
- [ ] Verify no new packaging defects.
|
|
||||||
- [ ] Tag `v0.1.1` and write `CHANGELOG.md` entry; then remove this section.
|
|
||||||
|
|
||||||
## 0.1.2 — Scale and façade corrections (patch)
|
|
||||||
|
|
||||||
Suggested blender skills: **references** (reference-image-match and visual-match-checklist templates — measure the guessed heights and colonnade proportions against the photographs), **archviz** (real-world scale and façade composition discipline), **camera-cinematography** (reproduce the reference camera angles when comparing renders to photos).
|
|
||||||
|
|
||||||
### Patch tasks
|
|
||||||
|
|
||||||
- [ ] Correct building scale and street proportions flagged against references.
|
|
||||||
- [ ] Refine hero façade details where reference photography shows discrepancies.
|
|
||||||
- [ ] **Replace the placeholder heights.** Three of the fifteen block buildings carry no OSM height and render at a guessed 6.4 m (`Geo/tools/block_height_report.py`). Measure them off the photographs and correct the blockout.
|
|
||||||
- [ ] Resolve the remaining unknown on the hero: the colonnade currently occupies about 45 % of the height against roughly 50–60 % in the photographs. Fix the vertical proportions.
|
|
||||||
- [ ] Tag `v0.1.2` and write `CHANGELOG.md` entry; then remove this section.
|
|
||||||
|
|
||||||
## 0.1.3 — Packaging and setup fixes (patch)
|
## 0.1.3 — Packaging and setup fixes (patch)
|
||||||
|
|
||||||
@@ -161,6 +25,7 @@ Suggested blender skills: **procedural-modeling** + **geometry-nodes** (the faç
|
|||||||
|
|
||||||
### Patch tasks
|
### Patch tasks
|
||||||
|
|
||||||
|
- [ ] **Finish the Blosm blockout cleaning/detailing (moved from the deferred `0.1.0` item).** Import state: `Art/blender/blockout/blockout-2026-11-06.blend` (86 meshes, EPSG:26911, 0.0 cm affine residual) with cleaning tooling in `Geo/tools/` and `Art/blender/blockout/blosm_blockout.py`. To do here: (a) untextured roofs/facades → **Replace placeholder materials** below; (b) Blosm `setupScript` façade styles → **Give OSM buildings a façade style** below; (c) the 3 of 15 buildings lacking height data are the `0.1.2` placeholder-heights item.
|
||||||
- [ ] **Define the façade module.** One set of reusable pieces (bay, pilaster, cornice, parapet, window, shopfront, door) at a fixed module, exported with the `ue_metric_v1` preset so every building assembles from the same kit.
|
- [ ] **Define the façade module.** One set of reusable pieces (bay, pilaster, cornice, parapet, window, shopfront, door) at a fixed module, exported with the `ue_metric_v1` preset so every building assembles from the same kit.
|
||||||
- [ ] **Replace placeholder materials.** Every blockout building currently carries Blender flat colours. Port the procedural generators from `Tools/sibling_asset_audit.py`'s sibling project (`brick_facade`, `seamless_concrete`, `asphalt`) and add stone, render, and glass, then wire them through Unreal material instances.
|
- [ ] **Replace placeholder materials.** Every blockout building currently carries Blender flat colours. Port the procedural generators from `Tools/sibling_asset_audit.py`'s sibling project (`brick_facade`, `seamless_concrete`, `asphalt`) and add stone, render, and glass, then wire them through Unreal material instances.
|
||||||
- [ ] **Give OSM buildings a façade style.** Blosm exposes `scene.blosm.setupScript` (`setup/neoclassical.py` is the example) so a building can carry a real elevation instead of a plain extrusion. Build one style script for the south frontage and one for the north.
|
- [ ] **Give OSM buildings a façade style.** Blosm exposes `scene.blosm.setupScript` (`setup/neoclassical.py` is the example) so a building can carry a real elevation instead of a plain extrusion. Build one style script for the south frontage and one for the north.
|
||||||
@@ -442,7 +307,6 @@ Suggested blender skills: **procedural-modeling** + **geometry-nodes** (the faç
|
|||||||
- [ ] Freeze features; document the freeze in `CHANGELOG.md`.
|
- [ ] Freeze features; document the freeze in `CHANGELOG.md`.
|
||||||
- [ ] Produce a public beta build for testers.
|
- [ ] Produce a public beta build for testers.
|
||||||
- [ ] Complete license audit: `THIRD_PARTY_NOTICES.md`, all MPFB assets, textures, animations, vehicles, audio.
|
- [ ] Complete license audit: `THIRD_PARTY_NOTICES.md`, all MPFB assets, textures, animations, vehicles, audio.
|
||||||
- [ ] Review ODbL obligations for any distributed derived geographic database.
|
|
||||||
- [ ] Review Epic distribution requirements before publishing.
|
- [ ] Review Epic distribution requirements before publishing.
|
||||||
- [ ] Verify a fresh checkout builds and packages reproducibly.
|
- [ ] Verify a fresh checkout builds and packages reproducibly.
|
||||||
|
|
||||||
|
|||||||
+26
-1
@@ -16,6 +16,7 @@ Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side b
|
|||||||
| Tool | Purpose |
|
| Tool | Purpose |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `check-lfs.sh` | Fails if the `lfs` filter is missing or an LFS-tracked file was committed as a plain blob |
|
| `check-lfs.sh` | Fails if the `lfs` filter is missing or an LFS-tracked file was committed as a plain blob |
|
||||||
|
| `fbx_export_preset.py` | The **single** FBX export preset (`ue_metric_v1` / `ue_character_v1`): metres→centimetres via `FBX_SCALE_NONE`, axis mapping east +X / north −Y / up +Z, `mesh_smooth_type="FACE"` (smoothing groups survive the import). Every Blender exporter imports it instead of copying the argument block; `preset_kwargs()` is bpy-free and unit-tested. |
|
||||||
| `sibling_asset_audit.py` | Reads the sibling project's prop library from GLB headers into `Art/props/reuse-manifest.json`; copies nothing |
|
| `sibling_asset_audit.py` | Reads the sibling project's prop library from GLB headers into `Art/props/reuse-manifest.json`; copies nothing |
|
||||||
| `scale_test_export.py` / `ue_scale_test_import.py` | The 1 m = 100 uu scale test; the named presets `ue_metric_v1` live in these two files |
|
| `scale_test_export.py` / `ue_scale_test_import.py` | The 1 m = 100 uu scale test; the named presets `ue_metric_v1` live in these two files |
|
||||||
|
|
||||||
@@ -57,4 +58,28 @@ Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side b
|
|||||||
| Command | Purpose |
|
| Command | Purpose |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `UnrealEditor <uproject> /Game/Hollywood/DemoMap -game -qaselftest` | Headless behaviour check: drives forward input for 2 s, then resets and pauses. Logs `GigLine QA: ... PASS/FAIL` lines and a final `RESULT`. Runs only with `-qaselftest`, so normal play is unaffected. |
|
| `UnrealEditor <uproject> /Game/Hollywood/DemoMap -game -qaselftest` | Headless behaviour check: drives forward input for 2 s, then resets and pauses. Logs `GigLine QA: ... PASS/FAIL` lines and a final `RESULT`. Runs only with `-qaselftest`, so normal play is unaffected. |
|
||||||
| `GigLine SIM` / `GigLine GROUND` log lines | Emitted every second by `AGigLineCharacter` (TEMP QA): position, fall state, movement mode, and what the ground probe hits underneath. Remove before release. |
|
| `-qaselftest` phases | Walk, reset, pause/unpause, a 16-row seam transect (0 holes), a ground-referenced wall probe (0/35) and a **facade occlusion probe** that fires sideways from the road centre and requires a blocking wall wherever a building is staged (0/9), plus a frame-time capture writing `Saved/Profiling/perf-walk-frametimes.csv` and logging `GigLine PERF:`. |
|
||||||
|
| `gigline.SetupLogs` cvar | Spawn/input setup logging, **off by default** (`DefaultEngine.ini [ConsoleVariables]`); enable at runtime with `gigline.SetupLogs 1`. |
|
||||||
|
|
||||||
|
## Reference and concept generation
|
||||||
|
|
||||||
|
| Tool | Purpose |
|
||||||
|
| --- | --- |
|
||||||
|
| `facade_detail_concepts.py` | Generates **frontal building plates** (whole elevation, entrance in its facade, or building with neighbours) from a local reference photograph with an image model (OpenRouter); close-up detail crops are deliberately not requested because they cannot be measured for proportion. `--list` is free and shows which studies have their reference present; `--all`/`--only <name>` generates. Never overwrites (writes `-v2`, `-v3`), writes a `.refgen.json` sidecar per image (model, prompt, source photo, SHA-256), and puts everything under `Art/reference/facade-details/` — gitignored, local-only, never shipped. Costs money per image, so it is never automatic. |
|
||||||
|
| `judge_screenshot.py` | (see Visual review) |
|
||||||
|
|
||||||
|
## Input injection
|
||||||
|
|
||||||
|
| Tool | Purpose |
|
||||||
|
| --- | --- |
|
||||||
|
| `input_emulation.py` | Pure-stdlib `ctypes`/`fcntl` driver that creates a virtual keyboard+mouse on `/dev/uinput`. The desktop treats it as real hardware, so it proves whether OS input actually reaches the game. Needs `/dev/uinput` writable (ACL on this host). |
|
||||||
|
| `input_test.py` | Injects a scripted sequence into the focused window: `--sequence move:2,look:3,jump:1,reset:1,pause:1,click`. |
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m pip install -r requirements-tools.txt
|
||||||
|
python3 -m unittest discover -s Tools/tests -t Tools -p 'test_*.py'
|
||||||
|
```
|
||||||
|
|
||||||
|
Stdlib `unittest`, no Blender/Unreal process. See [`tests/README.md`](tests/README.md): test the pure parts (encoding, classification, maths, parsing); if logic is buried in `main()`, extract it into a function so it can be tested.
|
||||||
@@ -19,6 +19,9 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import fbx_export_preset as fbx_preset # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
# blockout object name -> (asset name, note). Addresses from the verification doc.
|
# blockout object name -> (asset name, note). Addresses from the verification doc.
|
||||||
BUILDINGS = {
|
BUILDINGS = {
|
||||||
@@ -69,20 +72,7 @@ def main():
|
|||||||
bpy.context.view_layer.objects.active = new_obj
|
bpy.context.view_layer.objects.active = new_obj
|
||||||
|
|
||||||
path = os.path.join(args.out, asset_name + ".fbx")
|
path = os.path.join(args.out, asset_name + ".fbx")
|
||||||
bpy.ops.export_scene.fbx(
|
fbx_preset.export_static(path, selection=True)
|
||||||
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,
|
|
||||||
)
|
|
||||||
world = new_obj.matrix_world
|
world = new_obj.matrix_world
|
||||||
zs = [(world @ v.co).z for v in new_mesh.vertices]
|
zs = [(world @ v.co).z for v in new_mesh.vertices]
|
||||||
ys = [(world @ v.co).y for v in new_mesh.vertices]
|
ys = [(world @ v.co).y for v in new_mesh.vertices]
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import fbx_export_preset as fbx_preset # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
BUILDINGS = ("TCL Chinese Theatre", "Madame Tussaud's")
|
BUILDINGS = ("TCL Chinese Theatre", "Madame Tussaud's")
|
||||||
|
|
||||||
@@ -50,20 +53,7 @@ def main():
|
|||||||
bpy.context.view_layer.objects.active = new_obj
|
bpy.context.view_layer.objects.active = new_obj
|
||||||
|
|
||||||
path = os.path.join(args.out, name.replace(" ", "_").replace("'", "") + ".fbx")
|
path = os.path.join(args.out, name.replace(" ", "_").replace("'", "") + ".fbx")
|
||||||
bpy.ops.export_scene.fbx(
|
fbx_preset.export_static(path, selection=True)
|
||||||
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,
|
|
||||||
)
|
|
||||||
world = new_obj.matrix_world
|
world = new_obj.matrix_world
|
||||||
zs = [(world @ v.co).z for v in new_mesh.vertices]
|
zs = [(world @ v.co).z for v in new_mesh.vertices]
|
||||||
print(f"[north] {name:22} verts {len(new_mesh.vertices):5} z {min(zs):+.2f}..{max(zs):+.2f} m "
|
print(f"[north] {name:22} verts {len(new_mesh.vertices):5} z {min(zs):+.2f}..{max(zs):+.2f} m "
|
||||||
|
|||||||
@@ -0,0 +1,328 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate frontal building plates from the local reference photography.
|
||||||
|
|
||||||
|
Each study takes one photograph we already hold (`Art/reference/`, gitignored) as
|
||||||
|
the geometric anchor and asks an image model to re-stage it as a clean **frontal
|
||||||
|
view of the building** — the whole elevation, the entrance in its facade, or the
|
||||||
|
building with its neighbours — evenly lit, no crowd, no staging, so the modelling
|
||||||
|
pass has something measurable instead of a tourist photo.
|
||||||
|
|
||||||
|
Deliberately **not** close-up detail crops: a doorway or cornice crop cannot be
|
||||||
|
measured for proportion, which is what the modelling pass actually needs.
|
||||||
|
|
||||||
|
python3 Tools/facade_detail_concepts.py --list
|
||||||
|
python3 Tools/facade_detail_concepts.py --only door-recess,parapet-motto
|
||||||
|
python3 Tools/facade_detail_concepts.py --all
|
||||||
|
python3 Tools/facade_detail_concepts.py --all --model google/gemini-2.5-flash-image
|
||||||
|
|
||||||
|
Output: `Art/reference/facade-details/<study>.png` plus a `<study>.refgen.json`
|
||||||
|
sidecar recording the model, the source photograph, the prompt and the SHA-256 of
|
||||||
|
the result. `Art/reference/` is gitignored, so **neither the images nor the
|
||||||
|
sidecars are committed** — they are local working material. The study list and
|
||||||
|
the tool are the committed part.
|
||||||
|
|
||||||
|
Design notes
|
||||||
|
------------
|
||||||
|
* Standard library only: the OpenRouter chat-completions call is `urllib`, and
|
||||||
|
the returned `data:` URL is decoded with `base64` and written straight to disk.
|
||||||
|
* An existing output is never overwritten; `-v2`, `-v3` … are used instead, so
|
||||||
|
re-running after a prompt edit keeps both studies comparable.
|
||||||
|
* Generation costs money, so it is never automatic: `--list` is free, and every
|
||||||
|
generating run prints the model and the study count first.
|
||||||
|
* The token is read from `OPENROUTER_API_KEY` or the ignored `.env` and is never
|
||||||
|
written to a sidecar or a log line.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
REFERENCE_DIR = os.path.join(ROOT, "Art", "reference")
|
||||||
|
OUT_DIR = os.path.join(REFERENCE_DIR, "facade-details")
|
||||||
|
API_URL = "https://openrouter.ai/api/v1/chat/completions"
|
||||||
|
DEFAULT_MODEL = "google/gemini-2.5-flash-image"
|
||||||
|
|
||||||
|
# The instruction appended to every study. Kept identical to the sibling
|
||||||
|
# project's convention so the two sets stay comparable: the photograph fixes the
|
||||||
|
# geometry, the prompt only restages light, weather, people and surface.
|
||||||
|
ANCHOR_SUFFIX = (
|
||||||
|
"Use the attached reference photograph as the source of the architecture: keep "
|
||||||
|
"the building's proportions, storey count, column rhythm, band heights and "
|
||||||
|
"signage placement exactly as photographed. You may REFRAME — step back across "
|
||||||
|
"the street, zoom out or widen the field of view — so that the whole building "
|
||||||
|
"fits in the frame from the pavement to the parapet; the reference is often "
|
||||||
|
"cropped and the plate must not be. Keep the view exterior and frontal (or "
|
||||||
|
"three-quarter where asked). Restage only lighting, weather, people and surface "
|
||||||
|
"condition. Do not invent extra storeys, wings or ornaments, and do not change "
|
||||||
|
"the architecture to fit the frame."
|
||||||
|
)
|
||||||
|
|
||||||
|
# name -> (reference photo relative to Art/reference/, what the plate must show)
|
||||||
|
#
|
||||||
|
# Verified framing (vision pre-check, 2026-11-09): the model follows the reference
|
||||||
|
# crop closely, so a plate is only as wide as its source photo. `building-entrance`
|
||||||
|
# and `building-street-front` come back as full-elevation frontal views; the studies
|
||||||
|
# anchored on the tightly cropped 2009 facade photo stay cropped, because the model
|
||||||
|
# will not invent the parapet and sky that the photograph does not contain. A full
|
||||||
|
# building plate for 6840 needs a full-elevation photograph, which the local set
|
||||||
|
# does not have (see Docs/reference-notes-hollywood.md).
|
||||||
|
#
|
||||||
|
# Every study asks for a *frontal view of a building*, not a crop of one detail:
|
||||||
|
# the whole elevation, the entrance in its facade, or the building together with
|
||||||
|
# its neighbours. Close-up framing is deliberately absent — a detail crop cannot
|
||||||
|
# be measured for proportion, which is what the modelling pass needs.
|
||||||
|
STUDIES: dict[str, tuple[str, str]] = {
|
||||||
|
"building-front": (
|
||||||
|
"hollywood/el-capitan-facade-day-wikimedia-2009-10-27.jpg",
|
||||||
|
"A frontal view of the complete building shown in the reference "
|
||||||
|
"photograph: the whole elevation from the pavement to the parapet fits in "
|
||||||
|
"the frame with clear sky above the roofline and pavement below the ground "
|
||||||
|
"floor, shot straight on and centred. Nothing may be cut off at the top or "
|
||||||
|
"bottom. Even daylight, no crowd, no vehicles.",
|
||||||
|
),
|
||||||
|
"building-entrance": (
|
||||||
|
"el-capitan-entrance/01-hollywood-el-capitan-entrance.jpg",
|
||||||
|
"A frontal view of the building in the reference photograph, framed so its "
|
||||||
|
"street entrance is clearly visible as part of the facade — the doors and "
|
||||||
|
"the wall around them in proportion, not a close-up of the doorway. "
|
||||||
|
"Include the pavement in front. Even daylight, no crowd.",
|
||||||
|
),
|
||||||
|
"building-with-neighbours": (
|
||||||
|
"hollywood/el-capitan-theatre-blade-sign-day-wikimedia-2008-10.jpg",
|
||||||
|
"A straight-on view of the street wall in the reference photograph: the "
|
||||||
|
"building and its neighbouring buildings either side, each at full height "
|
||||||
|
"from pavement to parapet with sky above, the joins between them visible, "
|
||||||
|
"nothing cut off. Shot from across the road, parallel to the frontage. "
|
||||||
|
"Even daylight, no crowd.",
|
||||||
|
),
|
||||||
|
"building-street-front": (
|
||||||
|
"hollywood/walk-of-fame-terrazzo-ovation-day-cc0-2019-06-20.jpg",
|
||||||
|
"A frontal view along the pavement of the building in the reference "
|
||||||
|
"photograph: the building's frontage seen from street level with its "
|
||||||
|
"neighbours continuing either side and the pavement in the foreground. "
|
||||||
|
"Straight on, even daylight, no crowd.",
|
||||||
|
),
|
||||||
|
"building-front-oblique": (
|
||||||
|
"hollywood/el-capitan-facade-day-wikimedia-2009-10-27.jpg",
|
||||||
|
"A three-quarter view of the complete building in the reference "
|
||||||
|
"photograph: the frontal elevation and one side visible together so the "
|
||||||
|
"building's depth and mass read, with its neighbouring building at one "
|
||||||
|
"edge. The whole building stays in frame. Even daylight, no crowd.",
|
||||||
|
),
|
||||||
|
"theatre-front-with-neighbours": (
|
||||||
|
"el-capitan-theater-detail/01-el-capitan-marquee.jpg",
|
||||||
|
"A frontal view of the theatre in the reference photograph together with "
|
||||||
|
"the buildings either side of it, the whole frontage and its full height "
|
||||||
|
"in frame, shot from across the road. Even daylight, no crowd, marquee "
|
||||||
|
"lettering blocked out as unreadable texture.",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_token() -> str:
|
||||||
|
token = os.environ.get("OPENROUTER_API_KEY")
|
||||||
|
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("OPENROUTER_API_KEY="):
|
||||||
|
return line.split("=", 1)[1].strip().strip('"').strip("'")
|
||||||
|
raise SystemExit("no OpenRouter key: set OPENROUTER_API_KEY or add it to .env")
|
||||||
|
|
||||||
|
|
||||||
|
def build_prompt(study: str) -> str:
|
||||||
|
"""The full prompt for a study: its request plus the fixed anchor suffix."""
|
||||||
|
if study not in STUDIES:
|
||||||
|
raise KeyError(f"unknown study {study!r}; known: {sorted(STUDIES)}")
|
||||||
|
return f"{STUDIES[study][1]}\n\n{ANCHOR_SUFFIX}"
|
||||||
|
|
||||||
|
|
||||||
|
def reference_path(study: str) -> str:
|
||||||
|
return os.path.join(REFERENCE_DIR, STUDIES[study][0])
|
||||||
|
|
||||||
|
|
||||||
|
def missing_references() -> list[str]:
|
||||||
|
"""Studies whose source photograph is not present locally."""
|
||||||
|
return [name for name in STUDIES if not os.path.isfile(reference_path(name))]
|
||||||
|
|
||||||
|
|
||||||
|
def next_free_path(study: str, directory: str = OUT_DIR) -> str:
|
||||||
|
"""`<study>.png`, or `<study>-v2.png`, `-v3` … if the name is taken."""
|
||||||
|
base = os.path.join(directory, f"{study}.png")
|
||||||
|
if not os.path.exists(base):
|
||||||
|
return base
|
||||||
|
index = 2
|
||||||
|
while True:
|
||||||
|
candidate = os.path.join(directory, f"{study}-v{index}.png")
|
||||||
|
if not os.path.exists(candidate):
|
||||||
|
return candidate
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
|
||||||
|
def data_url(path: str) -> str:
|
||||||
|
suffix = os.path.splitext(path)[1].lower()
|
||||||
|
mime = {".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png"}.get(suffix)
|
||||||
|
if not mime:
|
||||||
|
raise SystemExit(f"unsupported reference type: {path}")
|
||||||
|
with open(path, "rb") as handle:
|
||||||
|
return f"data:{mime};base64,{base64.b64encode(handle.read()).decode('ascii')}"
|
||||||
|
|
||||||
|
|
||||||
|
def decode_image(payload: dict) -> bytes:
|
||||||
|
"""Extract the returned image bytes from a chat-completions payload."""
|
||||||
|
message = (payload.get("choices") or [{}])[0].get("message", {})
|
||||||
|
urls = [item.get("image_url", {}).get("url", "")
|
||||||
|
for item in (message.get("images") or []) if isinstance(item, dict)]
|
||||||
|
if not urls:
|
||||||
|
match = re.search(r"data:image/[a-z]+;base64,[A-Za-z0-9+/=]+",
|
||||||
|
str(message.get("content") or ""))
|
||||||
|
if match:
|
||||||
|
urls.append(match.group(0))
|
||||||
|
if not urls or not urls[0]:
|
||||||
|
raise SystemExit(f"model returned no image data: {json.dumps(payload)[:400]}")
|
||||||
|
url = urls[0]
|
||||||
|
if url.startswith("data:"):
|
||||||
|
return base64.b64decode(url.split(",", 1)[1])
|
||||||
|
with urllib.request.urlopen(urllib.request.Request(url), timeout=120) as response:
|
||||||
|
return response.read()
|
||||||
|
|
||||||
|
|
||||||
|
def generate(study: str, token: str, model: str, force: bool = False) -> str:
|
||||||
|
"""Generate one study; returns the written path. Never overwrites by default."""
|
||||||
|
path = next_free_path(study)
|
||||||
|
if force and os.path.exists(os.path.join(OUT_DIR, f"{study}.png")):
|
||||||
|
path = os.path.join(OUT_DIR, f"{study}.png")
|
||||||
|
# Write to a temp name then replace, so a failed call cannot destroy the
|
||||||
|
# previous result.
|
||||||
|
target = path + ".new"
|
||||||
|
else:
|
||||||
|
target = path
|
||||||
|
|
||||||
|
body = {
|
||||||
|
"model": model,
|
||||||
|
"messages": [{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{"type": "text", "text": build_prompt(study)},
|
||||||
|
{"type": "image_url", "image_url": {"url": data_url(reference_path(study))}},
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
request = urllib.request.Request(
|
||||||
|
API_URL,
|
||||||
|
data=json.dumps(body).encode("utf-8"),
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"HTTP-Referer": "https://git.allucanget.biz/zwitschi/gig-line",
|
||||||
|
"X-Title": "gig-line facade details",
|
||||||
|
},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = None
|
||||||
|
for attempt in range(3):
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(request, timeout=180) as response:
|
||||||
|
payload = json.load(response)
|
||||||
|
break
|
||||||
|
except urllib.error.HTTPError as exc:
|
||||||
|
if exc.code == 429 or exc.code >= 500:
|
||||||
|
wait = 5 * (attempt + 1)
|
||||||
|
sys.stderr.write(f" HTTP {exc.code}; retrying in {wait}s\n")
|
||||||
|
time.sleep(wait)
|
||||||
|
continue
|
||||||
|
detail = exc.read().decode("utf-8", "replace")[:300]
|
||||||
|
raise SystemExit(f"{study}: HTTP {exc.code} — {detail}")
|
||||||
|
except urllib.error.URLError as exc:
|
||||||
|
sys.stderr.write(f" network error ({exc.reason}); retrying\n")
|
||||||
|
time.sleep(5 * (attempt + 1))
|
||||||
|
if payload is None:
|
||||||
|
raise SystemExit(f"{study}: no response after retries")
|
||||||
|
|
||||||
|
image = decode_image(payload)
|
||||||
|
os.makedirs(OUT_DIR, exist_ok=True)
|
||||||
|
with open(target, "wb") as handle:
|
||||||
|
handle.write(image)
|
||||||
|
if target != path:
|
||||||
|
os.replace(target, path)
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
sidecar = {
|
||||||
|
"study": study,
|
||||||
|
"generated": os.path.basename(path),
|
||||||
|
"generated_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||||
|
"model": model,
|
||||||
|
"prompt": build_prompt(study),
|
||||||
|
"reference": {
|
||||||
|
"file": STUDIES[study][0],
|
||||||
|
"note": "local reference photography; Art/reference is gitignored and never shipped",
|
||||||
|
},
|
||||||
|
"sha256": hashlib.sha256(image).hexdigest(),
|
||||||
|
"bytes": len(image),
|
||||||
|
"redistribution": "local development reference only; not committed, not shipped",
|
||||||
|
}
|
||||||
|
sidecar_path = os.path.splitext(path)[0] + ".refgen.json"
|
||||||
|
with open(sidecar_path, "w", encoding="utf-8") as handle:
|
||||||
|
json.dump(sidecar, handle, indent=2)
|
||||||
|
if token in json.dumps(sidecar):
|
||||||
|
raise SystemExit("refusing to write a sidecar containing the token")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None) -> int:
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
ap.add_argument("--list", action="store_true", help="list studies and exit (free)")
|
||||||
|
ap.add_argument("--all", action="store_true", help="generate every study")
|
||||||
|
ap.add_argument("--only", help="comma-separated study names")
|
||||||
|
ap.add_argument("--model", default=DEFAULT_MODEL)
|
||||||
|
ap.add_argument("--force", action="store_true",
|
||||||
|
help="replace the existing <study>.png instead of writing a -vN")
|
||||||
|
args = ap.parse_args(argv)
|
||||||
|
|
||||||
|
if args.list or not (args.all or args.only):
|
||||||
|
missing = set(missing_references())
|
||||||
|
print(f"{len(STUDIES)} studies; model {args.model}")
|
||||||
|
for name, (ref, _prompt) in STUDIES.items():
|
||||||
|
state = "MISSING reference" if name in missing else "ready"
|
||||||
|
print(f" {name:22} {state:18} <- {ref}")
|
||||||
|
if missing:
|
||||||
|
print(f"\n{len(missing)} study(ies) cannot run without their reference photograph.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
names = list(STUDIES) if args.all else [s.strip() for s in args.only.split(",") if s.strip()]
|
||||||
|
unknown = [n for n in names if n not in STUDIES]
|
||||||
|
if unknown:
|
||||||
|
raise SystemExit(f"unknown study(ies): {unknown}; known: {sorted(STUDIES)}")
|
||||||
|
blocked = [n for n in names if n in set(missing_references())]
|
||||||
|
if blocked:
|
||||||
|
raise SystemExit(f"missing reference photograph for: {blocked}")
|
||||||
|
token = load_token()
|
||||||
|
|
||||||
|
print(f"generating {len(names)} study(ies) with {args.model} -> {OUT_DIR}")
|
||||||
|
for index, name in enumerate(names, 1):
|
||||||
|
print(f"[{index}/{len(names)}] {name}")
|
||||||
|
try:
|
||||||
|
path = generate(name, token, args.model, force=args.force)
|
||||||
|
except SystemExit as exc:
|
||||||
|
sys.stderr.write(f" FAILED: {exc}\n")
|
||||||
|
continue
|
||||||
|
print(f" wrote {os.path.relpath(path, ROOT)}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""The one FBX export preset for this project ("ue_metric_v1"/"ue_character_v1").
|
||||||
|
|
||||||
|
Before this module the same `bpy.ops.export_scene.fbx(...)` argument block was
|
||||||
|
copied into six scripts (hero, frontage, north buildings, site, scale test, MPFB
|
||||||
|
character). They had already drifted only by accident, and every one of them
|
||||||
|
carried `mesh_smooth_type="OFF"` — which is why Unreal logged "No smoothing group
|
||||||
|
information was found for this mesh" on every import and round parts rendered
|
||||||
|
faceted. One preset, one place to fix that.
|
||||||
|
|
||||||
|
Use from inside Blender:
|
||||||
|
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "Tools"))
|
||||||
|
import fbx_export_preset as preset
|
||||||
|
|
||||||
|
preset.export_static(path) # whole scene, MESH only
|
||||||
|
preset.export_static(path, selection=True) # selected objects only
|
||||||
|
preset.export_character(path) # armature + mesh, character flags
|
||||||
|
|
||||||
|
`preset_kwargs()` is pure (no `bpy`), so the argument set can be unit-tested
|
||||||
|
without Blender — see Tools/tests/test_fbx_export_preset.py.
|
||||||
|
|
||||||
|
Why these values:
|
||||||
|
* `FBX_SCALE_NONE` + `apply_unit_scale` keeps Blender metres → Unreal centimetres
|
||||||
|
at exactly ×100, verified by the three-cube scale test (1 m → 100.00 uu).
|
||||||
|
* `axis_forward="-Y"`, `axis_up="Z"` is the project's recorded mapping: east → +X,
|
||||||
|
north → −Y, up → +Z.
|
||||||
|
* `mesh_smooth_type="FACE"` exports smoothing groups. `"OFF"` (the old value)
|
||||||
|
drops them, so Unreal rebuilds flat normals and every cylinder is faceted.
|
||||||
|
* `use_mesh_modifiers=True` bakes the bevel/modifier stack at export; the source
|
||||||
|
`.blend` stays non-destructive.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
# Shared by every mesh export. Values are the recorded ue_metric_v1 preset.
|
||||||
|
_METRIC = {
|
||||||
|
"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": "FACE",
|
||||||
|
"path_mode": "AUTO",
|
||||||
|
"bake_space_transform": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Character exports differ only in the object set and the armature/path flags.
|
||||||
|
_CHARACTER = dict(
|
||||||
|
_METRIC,
|
||||||
|
object_types={"ARMATURE", "MESH"},
|
||||||
|
add_leaf_bones=False,
|
||||||
|
path_mode="COPY",
|
||||||
|
embed_textures=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
PRESETS = {"ue_metric_v1": _METRIC, "ue_character_v1": _CHARACTER}
|
||||||
|
|
||||||
|
|
||||||
|
def preset_kwargs(preset: str = "ue_metric_v1", selection: bool = False, **overrides) -> dict:
|
||||||
|
"""Return the `bpy.ops.export_scene.fbx` argument dict (no `bpy` needed).
|
||||||
|
|
||||||
|
`preset` selects the base set, `selection` decides whether only selected
|
||||||
|
objects are exported, and `overrides` wins last for the rare special case.
|
||||||
|
"""
|
||||||
|
if preset not in PRESETS:
|
||||||
|
raise KeyError(f"unknown preset {preset!r}; known: {sorted(PRESETS)}")
|
||||||
|
kwargs = dict(PRESETS[preset])
|
||||||
|
kwargs["use_selection"] = bool(selection)
|
||||||
|
kwargs.update(overrides)
|
||||||
|
if "filepath" in kwargs:
|
||||||
|
raise ValueError("pass the path to export_static/export_character, not as an override")
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
|
def _export(path: str, preset: str, selection: bool, overrides: dict) -> str:
|
||||||
|
import bpy # imported here so `preset_kwargs` stays usable without Blender
|
||||||
|
|
||||||
|
kwargs = preset_kwargs(preset, selection=selection, **overrides)
|
||||||
|
bpy.ops.export_scene.fbx(filepath=path, **kwargs)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def export_static(path: str, selection: bool = False, **overrides) -> str:
|
||||||
|
"""Export meshes with the ue_metric_v1 preset. Returns the path."""
|
||||||
|
return _export(path, "ue_metric_v1", selection, overrides)
|
||||||
|
|
||||||
|
|
||||||
|
def export_character(path: str, selection: bool = False, **overrides) -> str:
|
||||||
|
"""Export armature + mesh with the ue_character_v1 preset. Returns the path."""
|
||||||
|
return _export(path, "ue_character_v1", selection, overrides)
|
||||||
+91
-29
@@ -33,6 +33,9 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import fbx_export_preset as fbx_preset # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
# --- measured / derived ------------------------------------------------------
|
# --- measured / derived ------------------------------------------------------
|
||||||
FRONTAGE_W = 25.75
|
FRONTAGE_W = 25.75
|
||||||
@@ -50,8 +53,12 @@ FRONTAGE_Y = 0.0 # front plane sits at the building line of the block fra
|
|||||||
PEDESTAL_H = 1.45 # column pedestal top; entrance platform sits on it
|
PEDESTAL_H = 1.45 # column pedestal top; entrance platform sits on it
|
||||||
COLUMN_D = 0.95 # Ionic shaft diameter at the base
|
COLUMN_D = 0.95 # Ionic shaft diameter at the base
|
||||||
CAPITAL_H = 0.95
|
CAPITAL_H = 0.95
|
||||||
ENTABLATURE_H = 3.40 # band between the capitals and the motto panel
|
ENTABLATURE_H = 3.00 # band between the capitals and the motto panel
|
||||||
PARAPET_H = 2.20 # motto panel and cornice above the entablature
|
# 3.40 -> 3.00 on 2026-11-09: the photographs put the
|
||||||
|
# colonnade at 50-60 % of the 14.5 m elevation, and our
|
||||||
|
# 3.40/2.20 entablature+parapet left it at 47.6 %. Thinning
|
||||||
|
# both lifts the colonnade to 7.70 m = 53.1 %, inside the band.
|
||||||
|
PARAPET_H = 1.80 # motto panel and cornice above the entablature (see above)
|
||||||
PORTAL_W = 3.10 # each of the three door openings
|
PORTAL_W = 3.10 # each of the three door openings
|
||||||
PORTAL_H = 3.40
|
PORTAL_H = 3.40
|
||||||
STEP_TREAD = 0.34
|
STEP_TREAD = 0.34
|
||||||
@@ -70,7 +77,7 @@ def cube(name, x, y, z, dx, dy, dz, material=None):
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
def cylinder(name, x, y, z0, z1, radius, material, vertices=24):
|
def cylinder(name, x, y, z0, z1, radius, material, vertices=24, smooth=True):
|
||||||
bpy.ops.mesh.primitive_cylinder_add(
|
bpy.ops.mesh.primitive_cylinder_add(
|
||||||
vertices=vertices,
|
vertices=vertices,
|
||||||
radius=radius,
|
radius=radius,
|
||||||
@@ -81,6 +88,30 @@ def cylinder(name, x, y, z0, z1, radius, material, vertices=24):
|
|||||||
obj.name = name
|
obj.name = name
|
||||||
if material:
|
if material:
|
||||||
obj.data.materials.append(material)
|
obj.data.materials.append(material)
|
||||||
|
if smooth:
|
||||||
|
# Round parts must shade smoothly *around* the axis while keeping the
|
||||||
|
# caps creased. Without this the columns read as faceted prisms, and the
|
||||||
|
# FBX carries no smoothing groups at all (Unreal warns about it).
|
||||||
|
bpy.ops.object.shade_smooth_by_angle(angle=math.radians(35.0))
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def disc_on_wall(name, x, y, z, radius, thickness, material=None, vertices=24):
|
||||||
|
"""A round disc mounted vertically on a wall face.
|
||||||
|
|
||||||
|
`cylinder()` builds along Z, so a "medallion" made with it is a pancake lying
|
||||||
|
flat on top of the parapet rather than a disc on its face — which is what the
|
||||||
|
first build did (1.1 m across in Y, 0.02 m thick in Z, sitting at the coping).
|
||||||
|
This rotates the cylinder 90 degrees about X so the axis points along Y and
|
||||||
|
the disc faces the street.
|
||||||
|
"""
|
||||||
|
obj = cylinder(name, x, y, z - thickness / 2.0, z + thickness / 2.0,
|
||||||
|
radius, material, vertices=vertices)
|
||||||
|
obj.rotation_euler = (math.radians(90.0), 0.0, 0.0)
|
||||||
|
bpy.ops.object.select_all(action="DESELECT")
|
||||||
|
obj.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = obj
|
||||||
|
bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
@@ -132,14 +163,32 @@ def build():
|
|||||||
col_xs.append(x)
|
col_xs.append(x)
|
||||||
cube(f"Hero_Pedestal_{i + 1}", x, -0.62, PEDESTAL_H + 0.27,
|
cube(f"Hero_Pedestal_{i + 1}", x, -0.62, PEDESTAL_H + 0.27,
|
||||||
1.24, 1.24, 0.54, stone)
|
1.24, 1.24, 0.54, stone)
|
||||||
cylinder(f"Hero_Column_{i + 1}", x, -0.55, PEDESTAL_H + 0.55,
|
# Base moulding: a taller, wider cylinder where the shaft meets the
|
||||||
PEDESTAL_H + 0.55 + (HEIGHT - ENTABLATURE_H - PARAPET_H - PEDESTAL_H - 0.55 - CAPITAL_H),
|
# pedestal. Without it the shaft starts straight off a square plinth,
|
||||||
|
# which is not how an Ionic base reads in silhouette.
|
||||||
|
base_z0 = PEDESTAL_H + 0.55
|
||||||
|
base_h = 0.22
|
||||||
|
cylinder(f"Hero_ColumnBase_{i + 1}", x, -0.55, base_z0, base_z0 + base_h,
|
||||||
|
0.68, stone, vertices=20)
|
||||||
|
shaft_z0 = base_z0 + base_h
|
||||||
|
cap_z0 = HEIGHT - ENTABLATURE_H - PARAPET_H - CAPITAL_H
|
||||||
|
cylinder(f"Hero_Column_{i + 1}", x, -0.55, shaft_z0, cap_z0,
|
||||||
COLUMN_D / 2.0, stone)
|
COLUMN_D / 2.0, stone)
|
||||||
cylinder(f"Hero_Capital_{i + 1}", x, -0.55,
|
# Capital block, then the square abacus that carries the architrave.
|
||||||
HEIGHT - ENTABLATURE_H - PARAPET_H - CAPITAL_H,
|
# The abacus takes the top 0.14 m of the capital zone so the colonnade
|
||||||
HEIGHT - ENTABLATURE_H - PARAPET_H, 0.62, stone, vertices=16)
|
# still ends at HEIGHT - ENTABLATURE_H - PARAPET_H (proportion unchanged).
|
||||||
|
abacus_h = 0.14
|
||||||
|
cylinder(f"Hero_Capital_{i + 1}", x, -0.55, cap_z0,
|
||||||
|
HEIGHT - ENTABLATURE_H - PARAPET_H - abacus_h, 0.62, stone, vertices=16)
|
||||||
|
cube(f"Hero_Abacus_{i + 1}", x, -0.55,
|
||||||
|
HEIGHT - ENTABLATURE_H - PARAPET_H - abacus_h / 2.0,
|
||||||
|
1.30, 1.30, abacus_h, stone)
|
||||||
note("Hero_Column_x6", (COLUMN_D, COLUMN_D, HEIGHT - ENTABLATURE_H - PARAPET_H - PEDESTAL_H),
|
note("Hero_Column_x6", (COLUMN_D, COLUMN_D, HEIGHT - ENTABLATURE_H - PARAPET_H - PEDESTAL_H),
|
||||||
f"6 columns at {COLUMN_BAY} m centres (sibling notes x measured frontage)")
|
f"6 columns at {COLUMN_BAY} m centres (sibling notes x measured frontage)")
|
||||||
|
note("Hero_ColumnBase_x6", (1.36, 1.36, 0.22),
|
||||||
|
"assumption: Ionic base moulding; photographs show a wider base at the shaft foot")
|
||||||
|
note("Hero_Abacus_x6", (1.30, 1.30, 0.14),
|
||||||
|
"assumption: square abacus under the architrave, as Ionic capitals carry")
|
||||||
|
|
||||||
# --- entablature and parapet ------------------------------------------
|
# --- entablature and parapet ------------------------------------------
|
||||||
ent_z0 = HEIGHT - ENTABLATURE_H - PARAPET_H
|
ent_z0 = HEIGHT - ENTABLATURE_H - PARAPET_H
|
||||||
@@ -147,9 +196,25 @@ def build():
|
|||||||
# elevation reads as layered stone rather than one flat plane.
|
# elevation reads as layered stone rather than one flat plane.
|
||||||
cube("Hero_Architrave", 0.0, -1.55, ent_z0 + 0.42, FRONTAGE_W, 1.65, 0.84, stone)
|
cube("Hero_Architrave", 0.0, -1.55, ent_z0 + 0.42, FRONTAGE_W, 1.65, 0.84, stone)
|
||||||
note("Hero_Architrave", (FRONTAGE_W, 1.65, 0.84), "assumption: stacked entablature bands")
|
note("Hero_Architrave", (FRONTAGE_W, 1.65, 0.84), "assumption: stacked entablature bands")
|
||||||
# Cornice under the parapet.
|
# Cornice under the parapet: a projecting corona carried on a dentil course,
|
||||||
cube("Hero_Cornice", 0.0, -1.6, HEIGHT - PARAPET_H - 0.18, FRONTAGE_W, 1.75, 0.36, stone)
|
# so the top of the order reads as layered stone with a shadow line rather
|
||||||
|
# than one flat box. Dentils hang under the corona and stand 0.25 m clear of
|
||||||
|
# it, which is what gives the elevation its cornice shadow at street level.
|
||||||
|
cornice_z = HEIGHT - PARAPET_H - 0.18
|
||||||
|
cube("Hero_Cornice", 0.0, -1.6, cornice_z, FRONTAGE_W, 1.75, 0.36, stone)
|
||||||
note("Hero_Cornice", (FRONTAGE_W, 1.75, 0.36), "assumption: projecting cornice line")
|
note("Hero_Cornice", (FRONTAGE_W, 1.75, 0.36), "assumption: projecting cornice line")
|
||||||
|
DENTIL_W, DENTIL_D, DENTIL_H, DENTIL_GAP = 0.18, 0.30, 0.22, 0.27
|
||||||
|
dentil_y = -2.30
|
||||||
|
dentil_z = cornice_z - 0.18 - DENTIL_H / 2.0
|
||||||
|
dentil_span = FRONTAGE_W - 2.0
|
||||||
|
dentil_count = int(dentil_span / (DENTIL_W + DENTIL_GAP))
|
||||||
|
for i in range(dentil_count):
|
||||||
|
dx = -dentil_span / 2.0 + (DENTIL_W + DENTIL_GAP) * i + DENTIL_W / 2.0
|
||||||
|
cube(f"Hero_Dentil_{i + 1}", dx, dentil_y, dentil_z,
|
||||||
|
DENTIL_W, DENTIL_D, DENTIL_H, stone)
|
||||||
|
note("Hero_Dentil_xN", (DENTIL_W, DENTIL_D, DENTIL_H),
|
||||||
|
f"{dentil_count} dentils at {DENTIL_W + DENTIL_GAP:.2f} m centres under the corona; "
|
||||||
|
"assumption: photographed cornice reads as a dentilled course")
|
||||||
cube("Hero_Entablature", 0.0, -0.75, ent_z0 + ENTABLATURE_H / 2.0,
|
cube("Hero_Entablature", 0.0, -0.75, ent_z0 + ENTABLATURE_H / 2.0,
|
||||||
FRONTAGE_W, 1.5, ENTABLATURE_H, stone)
|
FRONTAGE_W, 1.5, ENTABLATURE_H, stone)
|
||||||
note("Hero_Entablature", (FRONTAGE_W, 1.5, ENTABLATURE_H), "assumption: entablature depth")
|
note("Hero_Entablature", (FRONTAGE_W, 1.5, ENTABLATURE_H), "assumption: entablature depth")
|
||||||
@@ -163,11 +228,13 @@ def build():
|
|||||||
FRONTAGE_W - 3.0, 0.08, 0.85, stone_dark)
|
FRONTAGE_W - 3.0, 0.08, 0.85, stone_dark)
|
||||||
note("Hero_MottoBand", (FRONTAGE_W - 3.0, 0.08, 0.85), "photographs: motto panel above the colonnade")
|
note("Hero_MottoBand", (FRONTAGE_W - 3.0, 0.08, 0.85), "photographs: motto panel above the colonnade")
|
||||||
|
|
||||||
# Medallions either side of the inscription.
|
# Medallions either side of the inscription: discs on the parapet face, not
|
||||||
|
# pancakes on the coping (see disc_on_wall).
|
||||||
for i, x in enumerate((-6.2, 6.2), start=1):
|
for i, x in enumerate((-6.2, 6.2), start=1):
|
||||||
cylinder(f"Hero_Medallion_{i}", x, -1.36, HEIGHT - PARAPET_H / 2.0 + 0.42,
|
disc_on_wall(f"Hero_Medallion_{i}", x, -1.30, HEIGHT - PARAPET_H / 2.0,
|
||||||
HEIGHT - PARAPET_H / 2.0 + 0.44, 0.55, stone_dark, vertices=20)
|
0.55, 0.04, stone_dark, vertices=20)
|
||||||
note("Hero_Medallion_x2", (1.1, 1.1, 0.02), "photographs: two stone discs beside the motto")
|
note("Hero_Medallion_x2", (1.1, 0.04, 1.1),
|
||||||
|
"photographs: two stone discs beside the motto, mounted on the parapet face")
|
||||||
|
|
||||||
# --- entrances ---------------------------------------------------------
|
# --- entrances ---------------------------------------------------------
|
||||||
# Three portals between column pairs, each with its own short flight. The
|
# Three portals between column pairs, each with its own short flight. The
|
||||||
@@ -241,32 +308,27 @@ def build():
|
|||||||
|
|
||||||
# --- windows -----------------------------------------------------------
|
# --- windows -----------------------------------------------------------
|
||||||
# Two ranks of windows between the columns and behind the colonnade.
|
# Two ranks of windows between the columns and behind the colonnade.
|
||||||
|
# Y must sit on the wall plane: the mass front face is at Y -0.50 and the
|
||||||
|
# columns stand proud to -1.02. These were at -1.98, i.e. 1.44 m in FRONT of
|
||||||
|
# the wall and ahead of the column line, so the glazing floated in the
|
||||||
|
# colonnade instead of reading as openings behind it (visible in the
|
||||||
|
# review-hero-facade-* shots). WINDOW_Y puts the pane 6 cm proud of the wall,
|
||||||
|
# safely behind the columns.
|
||||||
|
WINDOW_Y = -0.56
|
||||||
for rank, z in enumerate((4.4, 8.2), start=1):
|
for rank, z in enumerate((4.4, 8.2), start=1):
|
||||||
for i, x in enumerate(col_xs[:-1], start=1):
|
for i, x in enumerate(col_xs[:-1], start=1):
|
||||||
wx = x + COLUMN_BAY / 2.0
|
wx = x + COLUMN_BAY / 2.0
|
||||||
cube(f"Hero_Window_{rank}_{i}", wx, -1.98, z, 2.0, 0.08, 1.9, glass)
|
cube(f"Hero_Window_{rank}_{i}", wx, WINDOW_Y, z, 2.0, 0.08, 1.9, glass)
|
||||||
note("Hero_Window_x10", (2.0, 0.08, 1.9), "photographs: window bands behind the colonnade")
|
note("Hero_Window_x10", (2.0, 0.08, 1.9), "photographs: window bands behind the colonnade")
|
||||||
|
|
||||||
return parts, lines
|
return parts, lines
|
||||||
|
|
||||||
|
|
||||||
def export_fbx(path):
|
def export_fbx(path):
|
||||||
|
# Preset lives in Tools/fbx_export_preset.py (ue_metric_v1).
|
||||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
# Named export preset "ue_metric_v1" - same settings as Tools/scale_test_export.py.
|
# Named export preset "ue_metric_v1" - same settings as Tools/scale_test_export.py.
|
||||||
bpy.ops.export_scene.fbx(
|
fbx_preset.export_static(path)
|
||||||
filepath=path,
|
|
||||||
use_selection=False,
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
print(f"[hero] exported FBX preset ue_metric_v1 -> {path}")
|
print(f"[hero] exported FBX preset ue_metric_v1 -> {path}")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,405 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Pure-Python uinput driver to inject OS-level keyboard and mouse input.
|
||||||
|
|
||||||
|
This is a small, dependency-free input driver used by the demo QA harness
|
||||||
|
(`Tools/input_test.py`) to prove whether OS input actually reaches the game.
|
||||||
|
It talks to ``/dev/uinput`` directly through ``ctypes``, so it creates a real
|
||||||
|
virtual input device that the kernel and the desktop session (X11 or Wayland)
|
||||||
|
treat as genuine hardware. The game cannot tell simulated input from a real
|
||||||
|
keyboard/mouse.
|
||||||
|
|
||||||
|
No third-party packages are required. ``/dev/uinput`` must be writable by the
|
||||||
|
calling user (on this machine the system ACL grants `zwitschi` rw; if it is
|
||||||
|
not, add a udev rule or run as a user in the `input` group).
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
from input_emulation import InputDevice
|
||||||
|
|
||||||
|
with InputDevice() as dev:
|
||||||
|
dev.key('w') # press and release W -> types 'w'? no: key code 17
|
||||||
|
dev.hold('w', 0.5) # hold W for half a second (run forward)
|
||||||
|
dev.move(120, 0) # nudge the mouse +120 px on X (look right)
|
||||||
|
|
||||||
|
All calls log using Python's `logging` module so a scripted test can show the
|
||||||
|
exact injection timeline next to the game log.
|
||||||
|
|
||||||
|
Design notes
|
||||||
|
------------
|
||||||
|
* Uses the newer ``UI_DEV_SETUP`` ioctl with a ``struct uinput_setup``, falling
|
||||||
|
back to the legacy ``write(struct uinput_user_dev)`` method if the kernel
|
||||||
|
rejects it (very old kernels). On this kernel the setup ioctl path is used.
|
||||||
|
* The device exposes EV_KEY (keyboard), EV_REL (relative mouse) and
|
||||||
|
EV_SYN (synchronisation). It does not declare an absolute axis, so it is
|
||||||
|
treated as a relative device and the desktop does not try to give it an
|
||||||
|
on-screen pointer role.
|
||||||
|
* A single device with both keys and relative axes is created; for a game this
|
||||||
|
is fine. If a separate gamepad (EV_ABS/EV_FF) is wanted later, extend this
|
||||||
|
file with a second device class that also sets ABS bits.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
import ctypes.util
|
||||||
|
import fcntl
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
log = logging.getLogger("uinput")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# ioctl machinery
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# _IOC(dir, type, nr, size) is the standard Linux ioctl encoding.
|
||||||
|
_IOC_NRBITS = 8
|
||||||
|
_IOC_TYPEBITS = 8
|
||||||
|
_IOC_SIZEBITS = 14
|
||||||
|
_IOC_DIRBITS = 2
|
||||||
|
_IOC_NRSHIFT = 0
|
||||||
|
_IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS
|
||||||
|
_IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS
|
||||||
|
_IOC_DIRSHIFT = _IOC_SIZESHIFT + _IOC_SIZEBITS
|
||||||
|
_IOC_NONE = 0
|
||||||
|
_IOC_WRITE = 1
|
||||||
|
_IOC_READ = 2
|
||||||
|
|
||||||
|
|
||||||
|
def _ioc(direction, size, type_, nr):
|
||||||
|
# type_ may be a single character (ioctl base letter) or already an int.
|
||||||
|
if isinstance(type_, str):
|
||||||
|
type_ = ord(type_) if len(type_) == 1 else 0
|
||||||
|
return (
|
||||||
|
(size << _IOC_SIZESHIFT)
|
||||||
|
| (type_ << _IOC_TYPESHIFT)
|
||||||
|
| (nr << _IOC_NRSHIFT)
|
||||||
|
| (direction << _IOC_DIRSHIFT)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _IOW(type_, nr, size):
|
||||||
|
return _ioc(_IOC_WRITE, size, type_, nr)
|
||||||
|
|
||||||
|
|
||||||
|
def _IOR(type_, nr, size):
|
||||||
|
return _ioc(_IOC_READ, size, type_, nr)
|
||||||
|
|
||||||
|
|
||||||
|
def _IO(type_, nr):
|
||||||
|
return _ioc(_IOC_NONE, 0, type_, nr)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Linux constants (from linux/input.h, linux/uinput.h, input-event-codes.h)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UINPUT_IOCTL_BASE = ord("U")
|
||||||
|
UINPUT_MAX_NAME_SIZE = 80
|
||||||
|
|
||||||
|
UI_SET_EVBIT = _IOW(UINPUT_IOCTL_BASE, 100, ctypes.sizeof(ctypes.c_int))
|
||||||
|
UI_SET_KEYBIT = _IOW(UINPUT_IOCTL_BASE, 101, ctypes.sizeof(ctypes.c_int))
|
||||||
|
UI_SET_RELBIT = _IOW(UINPUT_IOCTL_BASE, 102, ctypes.sizeof(ctypes.c_int))
|
||||||
|
UI_DEV_CREATE = _IO(UINPUT_IOCTL_BASE, 1)
|
||||||
|
UI_DEV_DESTROY = _IO(UINPUT_IOCTL_BASE, 2)
|
||||||
|
UI_DEV_SETUP = _IOW(UINPUT_IOCTL_BASE, 3, 0) # size patched below
|
||||||
|
|
||||||
|
EV_SYN = 0x00
|
||||||
|
EV_KEY = 0x01
|
||||||
|
EV_REL = 0x02
|
||||||
|
|
||||||
|
REL_X = 0x00
|
||||||
|
REL_Y = 0x01
|
||||||
|
REL_WHEEL = 0x08
|
||||||
|
|
||||||
|
BTN_LEFT = 0x110
|
||||||
|
BTN_RIGHT = 0x111
|
||||||
|
|
||||||
|
BUS_USB = 0x03
|
||||||
|
INPUT_PROP_DIRECT = 0x01
|
||||||
|
UINPUT_VERSION = 5 # linux <input.h> UINPUT_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
class _InputId(ctypes.Structure):
|
||||||
|
_fields_ = [
|
||||||
|
("bustype", ctypes.c_uint16),
|
||||||
|
("vendor", ctypes.c_uint16),
|
||||||
|
("product", ctypes.c_uint16),
|
||||||
|
("version", ctypes.c_uint16),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class _UinputSetup(ctypes.Structure):
|
||||||
|
"""Modern uinput_setup: input_id + padded name + ff_effects_max."""
|
||||||
|
|
||||||
|
_fields_ = [
|
||||||
|
("id", _InputId),
|
||||||
|
("name", ctypes.c_char * UINPUT_MAX_NAME_SIZE),
|
||||||
|
("ff_effects_max", ctypes.c_uint32),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Patch the UI_DEV_SETUP size now that the struct layout is known.
|
||||||
|
UI_DEV_SETUP = _IOW(UINPUT_IOCTL_BASE, 3, ctypes.sizeof(_UinputSetup))
|
||||||
|
|
||||||
|
|
||||||
|
class _InputEvent(ctypes.Structure):
|
||||||
|
"""struct input_event on 64-bit Linux: two longs (timeval) + 2x16 + 32-bit."""
|
||||||
|
|
||||||
|
_fields_ = [
|
||||||
|
("tv_sec", ctypes.c_long),
|
||||||
|
("tv_usec", ctypes.c_long),
|
||||||
|
("type", ctypes.c_uint16),
|
||||||
|
("code", ctypes.c_uint16),
|
||||||
|
("value", ctypes.c_int32),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class _LegacyUserDev(ctypes.Structure):
|
||||||
|
"""Legacy uinput_user_dev fallback (name + id + ff + absolute ranges)."""
|
||||||
|
|
||||||
|
_fields_ = [
|
||||||
|
("name", ctypes.c_char * UINPUT_MAX_NAME_SIZE),
|
||||||
|
("id", _InputId),
|
||||||
|
("ff_effects_max", ctypes.c_uint32),
|
||||||
|
("absmax", ctypes.c_int32 * 64),
|
||||||
|
("absmin", ctypes.c_int32 * 64),
|
||||||
|
("absfuzz", ctypes.c_int32 * 64),
|
||||||
|
("absflat", ctypes.c_int32 * 64),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# libc
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _ioctl(fd, request, arg):
|
||||||
|
"""Issue an ioctl via fcntl.ioctl.
|
||||||
|
|
||||||
|
The uinput bit-fill ioctls (UI_SET_EVBIT/KEYBIT/RELBIT) take the value
|
||||||
|
directly, not a pointer: pass the int itself. The struct ioctls pass a
|
||||||
|
ctypes Structure (buffer protocol).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = fcntl.ioctl(fd, request, arg)
|
||||||
|
except OSError as exc:
|
||||||
|
raise OSError(exc.errno, os.strerror(exc.errno), request) from exc
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Key code lookup
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Linux key codes referenced by the demo controls. All keys exposed by the
|
||||||
|
# device are declared in KEYCODES; the driver enables every one of them, which
|
||||||
|
# is a superset and lets the harness send any.
|
||||||
|
KEYCODES = {
|
||||||
|
"w": 17, "s": 31, "a": 30, "d": 32,
|
||||||
|
"space": 57, "r": 19, "esc": 1,
|
||||||
|
"up": 103, "down": 108, "left": 105, "right": 106,
|
||||||
|
"enter": 28, "tab": 15, "backspace": 14, "shift": 42, "ctrl": 29,
|
||||||
|
"alt": 56, "1": 2, "2": 3, "3": 4, "0": 11,
|
||||||
|
}
|
||||||
|
KEY_ESC = 1
|
||||||
|
|
||||||
|
# All EV_KEY codes we want to claim (just the ones in KEYCODES plus a few).
|
||||||
|
_ALL_KEYCODES = sorted(set(KEYCODES.values()))
|
||||||
|
_ALL_RELS = [REL_X, REL_Y, REL_WHEEL]
|
||||||
|
_ALL_BTNS = [BTN_LEFT, BTN_RIGHT]
|
||||||
|
|
||||||
|
|
||||||
|
class UInputError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InputDevice:
|
||||||
|
"""A virtual USB keyboard+mouse device backed by /dev/uinput.
|
||||||
|
|
||||||
|
Use as a context manager: the device is destroyed on exit, so a hang or
|
||||||
|
crash never leaves a phantom input device behind. All methods emit log
|
||||||
|
records so a test can reconstruct exactly what was injected and when.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name="GigLine QA Input", path="/dev/uinput"):
|
||||||
|
self.name = name
|
||||||
|
self.path = path
|
||||||
|
self._fd = None
|
||||||
|
self._open_and_setup()
|
||||||
|
|
||||||
|
# -- lifecycle ----------------------------------------------------------
|
||||||
|
|
||||||
|
def _open_and_setup(self):
|
||||||
|
try:
|
||||||
|
self._fd = os.open(self.path, os.O_WRONLY | os.O_NONBLOCK)
|
||||||
|
except OSError as exc:
|
||||||
|
raise UInputError(
|
||||||
|
f"cannot open {self.path}: {exc}. "
|
||||||
|
"Needs a udev rule or membership in the 'input' group."
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
fd = self._fd
|
||||||
|
try:
|
||||||
|
# Declare which event types the device can emit.
|
||||||
|
# These bit-fill ioctls take the value directly (not a pointer).
|
||||||
|
for bit in (EV_KEY, EV_REL, EV_SYN):
|
||||||
|
_ioctl(fd, UI_SET_EVBIT, bit)
|
||||||
|
for code in _ALL_KEYCODES + _ALL_BTNS:
|
||||||
|
_ioctl(fd, UI_SET_KEYBIT, code)
|
||||||
|
for rel in _ALL_RELS:
|
||||||
|
_ioctl(fd, UI_SET_RELBIT, rel)
|
||||||
|
|
||||||
|
# Modern path.
|
||||||
|
setup = _UinputSetup()
|
||||||
|
setup.id.bustype = BUS_USB
|
||||||
|
setup.id.vendor = 0x1234
|
||||||
|
setup.id.product = 0x5678
|
||||||
|
setup.id.version = UINPUT_VERSION
|
||||||
|
setup.name = self.name.encode()[: UINPUT_MAX_NAME_SIZE - 1]
|
||||||
|
setup.ff_effects_max = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
_ioctl(fd, UI_DEV_SETUP, setup)
|
||||||
|
except OSError:
|
||||||
|
# Very old kernels: fall back to writing uinput_user_dev.
|
||||||
|
legacy = _LegacyUserDev()
|
||||||
|
legacy.id = setup.id
|
||||||
|
legacy.name = setup.name
|
||||||
|
legacy.ff_effects_max = 0
|
||||||
|
os.write(fd, legacy)
|
||||||
|
log.warning("kernel rejected UI_DEV_SETUP; used legacy write path")
|
||||||
|
|
||||||
|
_ioctl(fd, UI_DEV_CREATE, 0)
|
||||||
|
# Give the input core a moment to register the device node.
|
||||||
|
time.sleep(0.2)
|
||||||
|
except Exception:
|
||||||
|
self.close()
|
||||||
|
raise
|
||||||
|
|
||||||
|
log.info("virtual input device %r created on %s", self.name, self.path)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self._fd is not None:
|
||||||
|
try:
|
||||||
|
_ioctl(self._fd, UI_DEV_DESTROY, 0)
|
||||||
|
except (OSError, Exception):
|
||||||
|
pass
|
||||||
|
os.close(self._fd)
|
||||||
|
self._fd = None
|
||||||
|
log.info("virtual input device %r destroyed", self.name)
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *exc):
|
||||||
|
self.close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# -- event emission -----------------------------------------------------
|
||||||
|
|
||||||
|
def _emit(self, etype, code, value):
|
||||||
|
"""Write an input_event (2 longs timeval + u16/u16/s32) + SYN_REPORT."""
|
||||||
|
if self._fd is None:
|
||||||
|
raise UInputError("device is closed")
|
||||||
|
# struct input_event: __kernel_old_timeval (2x long) + type/code/value.
|
||||||
|
ev = struct.pack("llHHi", int(time.time()), 0, etype, code, int(value))
|
||||||
|
try:
|
||||||
|
os.write(self._fd, ev)
|
||||||
|
except OSError as exc:
|
||||||
|
log.error("write failed: %s", exc)
|
||||||
|
raise
|
||||||
|
os.write(self._fd, struct.pack("llHHi", 0, 0, EV_SYN, 0, 0))
|
||||||
|
|
||||||
|
# -- high-level actions -------------------------------------------------
|
||||||
|
|
||||||
|
def _key_code(self, key):
|
||||||
|
if isinstance(key, int):
|
||||||
|
return key
|
||||||
|
key = key.lower()
|
||||||
|
if key not in KEYCODES:
|
||||||
|
raise UInputError(f"unknown key {key!r}; known: {sorted(KEYCODES)}")
|
||||||
|
return KEYCODES[key]
|
||||||
|
|
||||||
|
def press(self, key):
|
||||||
|
"""Press a key (no release)."""
|
||||||
|
code = self._key_code(key)
|
||||||
|
self._emit(EV_KEY, code, 1)
|
||||||
|
log.info("KEY down %-6s (code %d)", key, code)
|
||||||
|
|
||||||
|
def release(self, key):
|
||||||
|
"""Release a previously pressed key."""
|
||||||
|
code = self._key_code(key)
|
||||||
|
self._emit(EV_KEY, code, 0)
|
||||||
|
log.info("KEY up %-6s (code %d)", key, code)
|
||||||
|
|
||||||
|
def key(self, key, delay=None):
|
||||||
|
"""Full press+release of a single key. Optional delay between them."""
|
||||||
|
self.press(key)
|
||||||
|
if delay is not None:
|
||||||
|
time.sleep(delay)
|
||||||
|
self.release(key)
|
||||||
|
|
||||||
|
def hold(self, key, duration):
|
||||||
|
"""Hold a key down for `duration` seconds, then release it."""
|
||||||
|
self.press(key)
|
||||||
|
time.sleep(duration)
|
||||||
|
self.release(key)
|
||||||
|
log.info("HELD %-6s for %.2fs", key, duration)
|
||||||
|
|
||||||
|
def chord(self, *keys):
|
||||||
|
"""Press all keys together, then release all in reverse."""
|
||||||
|
codes = [self._key_code(k) for k in keys]
|
||||||
|
for k, c in zip(keys, codes):
|
||||||
|
self._emit(EV_KEY, c, 1)
|
||||||
|
log.info("KEY down %-6s", k)
|
||||||
|
for k, c in zip(reversed(keys), reversed(codes)):
|
||||||
|
self._emit(EV_KEY, c, 0)
|
||||||
|
log.info("KEY up %-6s", k)
|
||||||
|
log.info("CHORD %s", "+".join(keys))
|
||||||
|
|
||||||
|
def move(self, dx, dy, delay=None):
|
||||||
|
"""Move the (relative) mouse by dx,dy pixels. Watches the pointer."""
|
||||||
|
self._emit(EV_REL, REL_X, int(dx))
|
||||||
|
self._emit(EV_REL, REL_Y, int(dy))
|
||||||
|
if delay is not None:
|
||||||
|
time.sleep(delay)
|
||||||
|
log.info("MOUSE move (%+d, %+d)", int(dx), int(dy))
|
||||||
|
|
||||||
|
def bpress(self, button="left"):
|
||||||
|
self._emit(EV_KEY, BTN_LEFT if button.lower() == "left" else BTN_RIGHT, 1)
|
||||||
|
log.info("BUTTON down %s", button)
|
||||||
|
|
||||||
|
def brelease(self, button="left"):
|
||||||
|
self._emit(EV_KEY, BTN_LEFT if button.lower() == "left" else BTN_RIGHT, 0)
|
||||||
|
log.info("BUTTON up %s", button)
|
||||||
|
|
||||||
|
def bclick(self, button="left"):
|
||||||
|
self.bpress(button)
|
||||||
|
time.sleep(0.02)
|
||||||
|
self.brelease(button)
|
||||||
|
log.info("BUTTON click %s", button)
|
||||||
|
|
||||||
|
def wheel(self, ticks):
|
||||||
|
self._emit(EV_REL, REL_WHEEL, int(ticks))
|
||||||
|
log.info("WHEEL %+d", int(ticks))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# A tiny smoke test: create the device, tap W, nudge the mouse, quit.
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(message)s")
|
||||||
|
try:
|
||||||
|
with InputDevice() as dev:
|
||||||
|
print("# device ready; sending W key + mouse nudge ...")
|
||||||
|
dev.key("w")
|
||||||
|
dev.move(80, 0)
|
||||||
|
time.sleep(0.3)
|
||||||
|
print("# ok, device destroyed cleanly")
|
||||||
|
except UInputError as exc:
|
||||||
|
print(f"ERROR: {exc}")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""QA harness: inject scripted OS-level keyboard/mouse input at the running game.
|
||||||
|
|
||||||
|
This is the tool that proves whether OS input actually reaches the game. It uses
|
||||||
|
`Tools/input_emulation.py` to create a virtual input device directly on
|
||||||
|
`/dev/uinput`, so a focused game window receives the injected keys and mouse
|
||||||
|
movement as genuine hardware input — indistinguishable from a real keyboard.
|
||||||
|
|
||||||
|
Typical workflow
|
||||||
|
----------------
|
||||||
|
The game must already be running and focused (it will not be launched here; the
|
||||||
|
user brings up the window). Run this script from a separate terminal:
|
||||||
|
|
||||||
|
python3 Tools/input_test.py --sequence move:2,look:3,jump:1,reset:1,pause:1
|
||||||
|
|
||||||
|
It prints a per-step timeline (seconds) to stdout, which you can compare against
|
||||||
|
the `GigLine IN:` rows in the game's `Saved/Logs`. If the game log shows
|
||||||
|
`MoveForward fired` / `Look fired` for the injected steps, OS input is reaching
|
||||||
|
the pawn and the remaining problem is not input delivery.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
python3 Tools/input_test.py # default smoke sequence
|
||||||
|
python3 Tools/input_test.py --sequence move:2,look:3,jump:1,reset:1,pause:1
|
||||||
|
python3 Tools/input_test.py --device /dev/uinput --name "GigLine QA Input"
|
||||||
|
|
||||||
|
Steps supported in `--sequence` (comma-separated, `name:count`):
|
||||||
|
move hold W forward (each unit = ~0.5 s)
|
||||||
|
back hold S
|
||||||
|
strafe hold D
|
||||||
|
left hold A
|
||||||
|
look nudge mouse right (rotate camera)
|
||||||
|
jump press Space
|
||||||
|
reset press R
|
||||||
|
pause press Escape
|
||||||
|
click left mouse click
|
||||||
|
|
||||||
|
Design
|
||||||
|
------
|
||||||
|
* The device is kept open for the whole run and destroyed on exit (context
|
||||||
|
manager), so nothing persists after the harness quits.
|
||||||
|
* Each step logs an absolute timestamp so you can line it up with the game log
|
||||||
|
wall-clock times (the game logs to `Saved/Logs/GigLine.log` with the same
|
||||||
|
system clock).
|
||||||
|
* No third-party packages: only the Python standard library.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
from input_emulation import InputDevice, UInputError
|
||||||
|
|
||||||
|
# Duration of one "unit" for hold-based movement steps, in seconds.
|
||||||
|
MOVE_STEP_S = 0.5
|
||||||
|
|
||||||
|
# Handlers map step names to a callable(dev) that performs the key/mouse action.
|
||||||
|
def _move(dev):
|
||||||
|
dev.hold("w", MOVE_STEP_S)
|
||||||
|
|
||||||
|
|
||||||
|
def _back(dev):
|
||||||
|
dev.hold("s", MOVE_STEP_S)
|
||||||
|
|
||||||
|
|
||||||
|
def _strafe(dev):
|
||||||
|
dev.hold("d", MOVE_STEP_S)
|
||||||
|
|
||||||
|
|
||||||
|
def _left(dev):
|
||||||
|
dev.hold("a", MOVE_STEP_S)
|
||||||
|
|
||||||
|
|
||||||
|
def _look(dev):
|
||||||
|
# nudge the mouse +240 px on X repeatedly so the camera has time to respond.
|
||||||
|
for _ in range(6):
|
||||||
|
dev.move(240, 0, delay=0.03)
|
||||||
|
time.sleep(0.15)
|
||||||
|
|
||||||
|
|
||||||
|
def _jump(dev):
|
||||||
|
dev.key("space", delay=0.05)
|
||||||
|
|
||||||
|
|
||||||
|
def _reset(dev):
|
||||||
|
dev.key("r", delay=0.05)
|
||||||
|
|
||||||
|
|
||||||
|
def _pause(dev):
|
||||||
|
dev.key("esc", delay=0.05)
|
||||||
|
|
||||||
|
|
||||||
|
def _click(dev):
|
||||||
|
dev.bclick("left")
|
||||||
|
|
||||||
|
|
||||||
|
STEPS = {
|
||||||
|
"move": _move, "back": _back, "strafe": _strafe, "left": _left,
|
||||||
|
"look": _look, "jump": _jump, "reset": _reset, "pause": _pause,
|
||||||
|
"click": _click,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_sequence(text):
|
||||||
|
"""'move:2,look:1,jump:1' -> ordered list of (name, count)."""
|
||||||
|
seq = []
|
||||||
|
for item in (text or "").split(","):
|
||||||
|
item = item.strip()
|
||||||
|
if not item:
|
||||||
|
continue
|
||||||
|
if ":" in item:
|
||||||
|
name, _, count = item.partition(":")
|
||||||
|
count = int(count or "1")
|
||||||
|
else:
|
||||||
|
name, count = item, 1
|
||||||
|
if name not in STEPS:
|
||||||
|
raise ValueError(f"unknown step {name!r}; known: {sorted(STEPS)}")
|
||||||
|
seq.append((name, count))
|
||||||
|
if not seq:
|
||||||
|
seq = [("move", 2), ("look", 1), ("jump", 1), ("reset", 1), ("pause", 1)]
|
||||||
|
return seq
|
||||||
|
|
||||||
|
|
||||||
|
def run_sequence(dev, seq):
|
||||||
|
t0 = time.time()
|
||||||
|
for name, count in seq:
|
||||||
|
for _ in range(count):
|
||||||
|
stamp = time.time() - t0
|
||||||
|
print(f"[{stamp:6.3f}s] -> {name}")
|
||||||
|
# A brief settle so distinct keystrokes are logged separately.
|
||||||
|
time.sleep(0.1)
|
||||||
|
STEPS[name](dev)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
ap.add_argument("--sequence",
|
||||||
|
help="comma-separated steps and repeats, e.g. move:2,look:3")
|
||||||
|
ap.add_argument("--device", default="/dev/uinput", help="uinput device path")
|
||||||
|
ap.add_argument("--name", default="GigLine QA Input", help="virtual device name")
|
||||||
|
ap.add_argument("-v", "--verbose", action="store_true",
|
||||||
|
help="log every injected event via logging")
|
||||||
|
args = ap.parse_args(argv)
|
||||||
|
|
||||||
|
if args.verbose:
|
||||||
|
logging.basicConfig(level=logging.INFO,
|
||||||
|
format="%(asctime)s.%(msecs)03d %(name)s %(message)s",
|
||||||
|
datefmt="%H:%M:%S")
|
||||||
|
try:
|
||||||
|
seq = parse_sequence(args.sequence)
|
||||||
|
except ValueError as exc:
|
||||||
|
ap.error(str(exc))
|
||||||
|
|
||||||
|
try:
|
||||||
|
with InputDevice(name=args.name, path=args.device) as dev:
|
||||||
|
print(f"# virtual device {args.name!r} active; send sequence now")
|
||||||
|
print(f"# steps: {' '.join(f'{n}x{c}' for n, c in seq)}")
|
||||||
|
run_sequence(dev, seq)
|
||||||
|
except UInputError as exc:
|
||||||
|
print(f"ERROR: {exc}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n# interrupted; device destroyed on exit")
|
||||||
|
return 130
|
||||||
|
print("# done; inputs sent")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -19,6 +19,9 @@ import sys
|
|||||||
import zlib
|
import zlib
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import fbx_export_preset as fbx_preset # noqa: E402
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
|
|
||||||
from mathutils import Euler
|
from mathutils import Euler
|
||||||
@@ -149,22 +152,7 @@ def make_diffuse(human, size=1024):
|
|||||||
def export_fbx(human, path):
|
def export_fbx(human, path):
|
||||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
# Named export preset "ue_character_v1" — change only via a new preset name.
|
# Named export preset "ue_character_v1" — change only via a new preset name.
|
||||||
bpy.ops.export_scene.fbx(
|
fbx_preset.export_character(path)
|
||||||
filepath=path,
|
|
||||||
use_selection=False,
|
|
||||||
object_types={"ARMATURE", "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,
|
|
||||||
add_leaf_bones=False,
|
|
||||||
path_mode="COPY",
|
|
||||||
embed_textures=False,
|
|
||||||
mesh_smooth_type="OFF",
|
|
||||||
bake_space_transform=False,
|
|
||||||
)
|
|
||||||
log(f"export: FBX preset ue_character_v1 -> {path}")
|
log(f"export: FBX preset ue_character_v1 -> {path}")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Executable
+12
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Open the Unreal editor focused on the walkable demo so you can press Play (PIE)
|
||||||
|
# to compare in-editor input intake vs the packaged standalone build.
|
||||||
|
#
|
||||||
|
# Usage: Tools/run_pie.sh
|
||||||
|
# Then in the editor: press the green Play button (Alt+P or the PIE toolbar).
|
||||||
|
set -e
|
||||||
|
ENGINE="${UE_ENGINE:-/run/media/system/DATA/Engines/UE_5.8.4}"
|
||||||
|
PROJ="$(cd "$(dirname "$0")/.." && pwd)/Game/GigLine/GigLine.uproject"
|
||||||
|
echo "Opening $PROJ on /Game/Hollywood/DemoMap ..."
|
||||||
|
cd "$ENGINE"
|
||||||
|
exec "./Engine/Binaries/Linux/UnrealEditor" "$PROJ" /Game/Hollywood/DemoMap
|
||||||
@@ -16,6 +16,9 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import fbx_export_preset as fbx_preset # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def build_scene():
|
def build_scene():
|
||||||
@@ -44,20 +47,7 @@ def build_scene():
|
|||||||
def export_fbx(path):
|
def export_fbx(path):
|
||||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
# Named export preset "ue_metric_v1" - change only via a new preset name.
|
# Named export preset "ue_metric_v1" - change only via a new preset name.
|
||||||
bpy.ops.export_scene.fbx(
|
fbx_preset.export_static(path)
|
||||||
filepath=path,
|
|
||||||
use_selection=False,
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
print(f"[scale_test] exported FBX preset ue_metric_v1 -> {path}")
|
print(f"[scale_test] exported FBX preset ue_metric_v1 -> {path}")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Tooling tests
|
||||||
|
|
||||||
|
Standard-library `unittest` tests for the host-side tools in `Tools/`. They never start Blender, Unreal, or the game; anything that needs those processes is verified by the tool itself and recorded in `Docs/release-checks.md` instead.
|
||||||
|
|
||||||
|
Run from the repository root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m pip install -r requirements-tools.txt
|
||||||
|
python3 -m unittest discover -s Tools/tests -t Tools -p 'test_*.py'
|
||||||
|
```
|
||||||
|
|
||||||
|
The `-t Tools` top-level directory is required: the tests import the tool modules by their plain names (`import input_emulation`), which is why `Tools/tests/` carries an `__init__.py`.
|
||||||
|
|
||||||
|
Individual test:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m unittest Tools.tests.test_input_emulation -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## What belongs here
|
||||||
|
|
||||||
|
Test the **pure** parts — argument parsing, key/coordinate encoding, geometry maths, licence gating, report formatting, decision classification. Network, Blender and Unreal calls stay out: a test that needs a paid API, a display, or an engine process is a manual check, not a unit test.
|
||||||
|
|
||||||
|
When a tool's logic is buried in `main()`, extract it into a function so it can be tested directly. That is a tooling improvement, not test scaffolding.
|
||||||
|
|
||||||
|
Geo (`Geo/tools/`) and Blender (`Art/blender/*/`) builders are tested the same way once their pure parts are extracted; note the module path in the test.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Package marker so `python3 -m unittest Tools.tests.<module>` resolves."""
|
||||||
Loaded 100 of 109 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user