Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
744bf8f357 | ||
|
|
c72bc41af1 | ||
|
|
803e77473c | ||
|
|
3942e9a215 | ||
|
|
4132f42ca1 | ||
|
|
c53386fcf4 | ||
|
|
2614d259aa | ||
|
|
4e703fcfd9 |
No files matched your search
@@ -210,8 +210,8 @@ Any change that affects the game's appearance — meshes, materials, lighting, l
|
||||
|
||||
1. Run `Tools/render_screenshots.sh` (rebuilds `Game/GigLine/Content/Test/ReviewMap.umap` from current content, runs the game on it, captures the fixed camera list, copies the PNGs into `Screenshots/`).
|
||||
2. Optional pre-check with a vision model: `Tools/judge_screenshot.py --image <shot.png> --expect "<what it should show>"` (or `--shots` to run every shot in the README) flags likely-missing content before a human looks. It reads `OPENROUTER_API_KEY` from `.env` (ignored). Small helper only — it never replaces the human verdict.
|
||||
3. Update `Screenshots/README.md`: one row per screenshot with what it shows and an approval state; describe any new shots or scene changes in the same change.
|
||||
3. Update `Screenshots/README.md`: the image is **embedded inline** (``) with its view, expected content, verification and approval state, plus one row in the status table at the top. The point is that the review happens in the file; a path alone is not a review. Describe any new shots or scene changes in the same change.
|
||||
4. The PNGs **are committed** so reviewers see exactly what the build rendered (`.gitignore` does not exclude them). Keep them in sync with this README: drop retired shots in the same change.
|
||||
5. Wait for the user's approval before treating a visual change as done; record approval by ticking the table in `Screenshots/README.md`.
|
||||
5. Wait for the user's approval before treating a visual change as done; record approval by ticking both the shot's section and its row in the status table in `Screenshots/README.md`.
|
||||
|
||||
New content that needs a different camera angle gets a new entry in the shot list in `Tools/ue_build_review_map.py`.
|
||||
@@ -0,0 +1,64 @@
|
||||
# Reference analysis — retail frontage, 0.1.6
|
||||
|
||||
Phase 0 of the `references` pack (reference-image-match). Written before any geometry, because the pass that produced the hero façade skipped it and cost two rebuilds.
|
||||
|
||||
## Source
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| Image | `Art/reference/retail-neighbours/01-lee-drug-and-neighbor.jpg` (1920×953) |
|
||||
| Author / licence | Gb321, own work, **CC BY-SA 4.0** — attribution required |
|
||||
| Captured | 2024-07-19 11:11 local |
|
||||
| Description | "Lee Drug and 6806 Hollywood Buildings in Hollywood" |
|
||||
| Coverage | Both target buildings in one frame, mid-morning sun from the south-east |
|
||||
|
||||
**One photograph is the whole evidence base for 6800 and 6806–6820.** It is a street-level oblique, not an elevation: verticals converge, the ground floor of 6806–6820 is partly behind a parked car, and the Lee Drug shopfront is behind a bus. Everything below that the photo does not resolve is marked _assumed_ and is carried into the part report.
|
||||
|
||||
**6822 Budget has no photograph.** It is a 6.0 m × 5.5 m single-storey sliver west of 6806–6820, and the TODO item already records that Commons has nothing usable. It stays a blockout extrusion and the item is deferred rather than guessed.
|
||||
|
||||
## Measured (not from the photo)
|
||||
|
||||
From `Geo/raw/osm-api-map-2026-11-06.osm` and the blockout, in the shared frame (metres, X east, Y north, Z from the project datum):
|
||||
|
||||
| Building | OSM way | X | Front face Y | Height | Depth |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 6800 Lee Drug | 424024149 | −33.20 … −14.37 | −14.63 | 12.10 m | 33.5 m |
|
||||
| 6806–6820 | 424024150 | −66.08 … −33.07 | −12.52 | 12.10 m | 30.8 m |
|
||||
| 6822 Budget | 424024144 | −78.54 … −72.52 | −12.92 | 5.50 m | 15.4 m |
|
||||
|
||||
Both target buildings are 12.10 m tall, which makes the _relative_ proportions in the photo directly usable: the parapet line, the cornice and the shopfront line can be read as fractions of the frame height.
|
||||
|
||||
## 6800 Lee Drug — reading
|
||||
|
||||
| Category | Observation |
|
||||
| --- | --- |
|
||||
| Subject | Corner retail block at Hollywood × Highland; the street face and the corner return are both visible |
|
||||
| Silhouette | Long low box, broken by a **rounded corner tower** that rises above the frontage parapet; the parapet is **stepped**, not flat |
|
||||
| Storeys | Two zones, not storeys: a tall shopfront zone and a single tall display wall above it |
|
||||
| Proportion (vertical) | Shopfront ≈ 0 → 0.36 H; bronze cornice ≈ 0.36 → 0.43 H; mural + lettering wall ≈ 0.43 → 0.82 H; stepped parapet ≈ 0.82 → 1.0 H (_assumed split; read off the frame, ±0.03 H_) |
|
||||
| Proportion (horizontal) | Corner tower occupies ≈ 4 m of the east end; eight mural panels at roughly 2.2 m centres along the frontage |
|
||||
| Colour | Cream stucco wall `#EDE6D8`, bronze/brown cornice `#6B4A2F`, near-black mural plates `#1C1A18`, dark glazing `#20262A` (_sampled from the frame_) |
|
||||
| Unique features | Stepped Deco parapet with 4 horizontal reveal lines; projecting bronze cornice; row of black celebrity-portrait panels; large dark "SOUVENIRS OF …" lettering on the upper wall; shopfront glazing with slim mullions |
|
||||
| Mood | 1930s Deco retail, freshly painted, bright midday |
|
||||
|
||||
## 6806–6820 — reading
|
||||
|
||||
| Category | Observation |
|
||||
| --- | --- |
|
||||
| Silhouette | Flat-fronted two-storey block; parapet steps up in fins at the west end |
|
||||
| Proportion (vertical) | Shopfront ≈ 0 → 0.32 H; burgundy fascia band ≈ 0.32 → 0.38 H; window storey ≈ 0.38 → 0.62 H; blank wall + parapet ≈ 0.62 → 1.0 H (_assumed_) |
|
||||
| Horizontal | Brown pilaster strips divide the frontage into roughly 3 m bays; seven upper windows across 33 m |
|
||||
| Colour | Same cream stucco; brown pilasters `#6B4A2F`; **burgundy-green** fascia `#5B3A3A`; white window frames |
|
||||
| Unique features | Banded parapet with 3 reveal lines; white-framed upper windows with security bars; shopfront glazing behind a burgundy fascia |
|
||||
|
||||
## Camera match (for the review shot)
|
||||
|
||||
The photo is a street-level oblique from the **north** side looking south-east, subject filling ~80 % of the frame width. The demo review shot must reproduce that: camera at 1.7 m on the north sidewalk, yaw toward the south frontage, 35–42° lens, both buildings in frame. A wide block view will not compare usefully against this crop.
|
||||
|
||||
## Assumptions to carry into the model report
|
||||
|
||||
1. Vertical proportions, as above — the photo's convergence makes a pixel-perfect read impossible.
|
||||
2. Mural panel size and spacing; the lettering is modelled as a recessed plate, not as text.
|
||||
3. Shopfront bay spacing on both buildings; the bus hides most of Lee Drug's ground floor.
|
||||
4. Parapet reveal line count (4 on Lee Drug, 3 on 6806–6820).
|
||||
5. Corner tower radius; only the outer curve is visible.
|
||||
@@ -8,6 +8,46 @@ Format: [Keep a Changelog](https://keepachangelog.com/) style, versions follow [
|
||||
|
||||
Nothing yet.
|
||||
|
||||
## [0.1.6] — 2026-11-09
|
||||
|
||||
Retail frontage. The two retail buildings beside the hero are modelled from their own photograph instead of the generic kit, and the photograph is treated as evidence: an analysis is written before any geometry, and every dimension the image does not resolve is carried into the part report as an assumption.
|
||||
|
||||
### Added
|
||||
|
||||
- **`Tools/retail_frontage_build.py`**: 6800 Lee Drug and 6806–6820 built from the one usable reference (`Art/reference/retail-neighbours/`, Gb321, CC BY-SA 4.0). Lee Drug gets a bronze cornice, storefront glazing with mullions, framed portrait panels, a painted lettering band, two-tier parapet with reveals and a fluted corner tower; 6806–6820 gets a burgundy fascia, brown pilasters, white-framed windows with security bars and its own banded parapet. Built in world coordinates from the blockout bounds (Lee Drug X −33.20…−14.37, face Y −14.63, H 12.10 m; 6806–6820 X −66.08…−33.07), joined to one mesh per building with a JSON part report.
|
||||
- **`Tools/ue_import_retail_detail.py`**: imports the two meshes with complex-as-simple collision — trim decorates a solid the player already collides with and must not add snag points.
|
||||
- **`Art/blender/retail/ref-analysis.md`**: the reference analysis the `references` skill pack requires, written before modelling.
|
||||
- **`Tools/_common.py`** and `Tools/tests/test_retail_frontage.py`: the bay-layout arithmetic, extracted so it is testable without Blender (4 tests).
|
||||
- **Review shot 5**, `demo/review-demo-retail-frontage`, embedded in `Screenshots/README.md`.
|
||||
|
||||
### Changed
|
||||
|
||||
- **The generic façade kit skips the two modelled buildings** (`MODELLED` list in `ue_apply_facade_kit.py`). It applies to every `Frontage_*`/`North_*` actor, so it was pasting a second, contradicting façade language over the new trim.
|
||||
- **Retail trim stages as `RetailTrim_*`** and is spawned through the demo builder, so it is regenerable rather than hand-placed.
|
||||
- Trim stucco, white and glass slots are mapped onto the demo's look-dev materials so the walls sit in the same noise and texture family as the rest of the block; the tones sampled from the photograph (bronze, burgundy, mural, lettering) stay as authored.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`Tools/render_screenshots.sh` never captured anything.** The capture step called `./Engine/...` while every commandlet above it went through `run_cmdlet`'s `cd "$ENGINE"`. Under `set -e` the script died at exit 127 _after_ rebuilding the maps, so the PNGs silently stayed a day old and `--verify` never ran.
|
||||
- **Duplicate `RetailTrim_*` actors on every rebuild**: the demo builder's clear list did not include the new label, so each run stacked another copy. Caught by the scene audit, not by eye.
|
||||
- **Mural plates were placed 0.20 m inside the solid blockout volume** and never rendered; the inset is now faked with a proud frame and a near-flush plate.
|
||||
- **Trim detail sat 0.05 m off the wall**, under a pixel of depth at 40 m, so the wall won the depth test. Trim planes now stand 0.10–0.20 m proud.
|
||||
- **6806–6820 parapet started at 0.62 H**, leaving a 4.6 m blank band; the window storey and parapet moved up to follow the photograph.
|
||||
- **Mural panels, parapet and lettering were sized from the photograph's proportions, not the inherited defaults**: the first pass left a parapet that read as one blank white band.
|
||||
|
||||
### Verified
|
||||
|
||||
- `render_screenshots.sh --verify`: **5/5 shots OK**; scene audit **0 failures** (every mesh materialed, all spans in tolerance, no duplicate labels).
|
||||
- Owner approved `demo-retail-frontage` (second pass) 2026-11-09; all five demo shots now carry approval.
|
||||
- 82 tool tests pass.
|
||||
|
||||
### Known limitations
|
||||
|
||||
- **Wall lettering and the painted band do not read at 40 m.** A plate at 0.10 m still vanishes; this is a decal/texture problem, not geometry. Moved to `0.9.0 — Realistic buildings`.
|
||||
- **The parapet has no moulded profile** and the corner tower is a fluted cylinder with two cap rings — both need the façade system in `0.9.0`.
|
||||
- **6822 Budget is untouched**: no photograph of it exists. Deferred and blocked in `0.9.0`.
|
||||
- The only reference for these two buildings is a single street-level oblique with a bus across Lee Drug's shopfront and a car across 6806's; several vertical proportions are assumptions recorded in the part report.
|
||||
|
||||
## [0.1.5] — 2026-11-09
|
||||
|
||||
Façade finishing. The kit gets the hero detail it was missing, and a class of silent defect is found and gated: the map had been rendering with **604 of 620 mesh actors unmaterialed** without a single error.
|
||||
|
||||
@@ -47,6 +47,7 @@ Static meshes and materials in `/Game/Hollywood/Site/`:
|
||||
| `SM_HeroFacade` | El Capitan Entertainment Centre detailed façade |
|
||||
| `SM_ElCapitanTheatre`, `SM_Budget_6822`, `SM_Hollywood_6806`, `SM_Hollywood_6902`, `SM_Hollywood_6922`, `SM_LeeDrug_6800` | South block frontage (6 buildings) |
|
||||
| `SM_Madame_Tussauds`, `SM_TCL_Chinese_Theatre` | North-side buildings (within 50 m) |
|
||||
| `SM_Retail_LeeDrug`, `SM_Retail_6806` | Retail trim over 6800 and 6806–6820, modelled from their own photograph (`Tools/retail_frontage_build.py`, staged as `RetailTrim_*`). `6822 Budget` stays a bare volume: no photograph exists. |
|
||||
|
||||
### Art Pipeline
|
||||
|
||||
@@ -54,13 +55,14 @@ Python scripts in `Tools/` drive the content pipeline:
|
||||
|
||||
| Script group | Purpose |
|
||||
| --- | --- |
|
||||
| `export_*`, `hero_facade_build` | Blender headless export (FBX) |
|
||||
| `ue_import_*`, `ue_hero_facade_import` | Unreal headless import + placement |
|
||||
| `export_*`, `hero_facade_build`, `retail_frontage_build`, `theatre_trim_build`, `facade_kit_build` | Blender headless export (FBX); trim builders work in world coordinates |
|
||||
| `ue_import_*`, `ue_hero_facade_import`, `ue_import_retail_detail` | Unreal headless import + placement |
|
||||
| `ue_build_demo_map`, `ue_build_review_map` | Map assembly (lighting, meshes, player start) |
|
||||
| `ue_create_input_assets` | Enhanced Input actions + mapping context |
|
||||
| `render_screenshots.sh` | Screenshot capture pipeline |
|
||||
| `judge_screenshot.py` | Vision-model pre-check (optional) |
|
||||
| `input_emulation.py`, `input_test.py` | OS-level virtual keyboard/mouse injection for input verification |
|
||||
| `Tools/_common.py` | Pure helper shared by Blender builders, importable without Blender (bay layout) |
|
||||
| `Tools/tests/` | Stdlib `unittest` tests for the pure parts of the tools (`requirements-tools.txt`) |
|
||||
| `Geo/tools/mapbox_buildings.py` | Second height source: Mapbox `mapbox-streets-v8` building data vs our OSM heights |
|
||||
|
||||
|
||||
@@ -55,3 +55,11 @@
|
||||
- **Decision**: All Blender work driven by Python scripts in `Tools/`. Skill packs used as method guides (checklists, naming, proportions) only.
|
||||
|
||||
- **Consequences**: Scripts are version-controlled and repeatable. No live Blender session needed for CI.
|
||||
|
||||
## ADR-8: Realistic building façades are a minor milestone, not a patch
|
||||
|
||||
- **Context**: `0.1.4` carried the first façade pass as a _patch_, while the release policy says patches contain fixes and refinements and never new scope. The `0.1.6` retail pass then showed what that costs: a façade built in a patch has no release gate of its own, so a first pass that does not yet read as the building (`Screenshots/README.md`, 0.1.6 gaps) can sit inside a "done" patch series. Realism is also the project's leading quality goal, which a patch cannot carry.
|
||||
|
||||
- **Decision**: Building realism gets its own minor milestone, `0.9.0 — Realistic buildings`, placed before the feature freeze. Its scope is the façade system (orders, profile library, storefront types, signage as decals), per-building photo-matched models, a measured material library with a texel-density rule, and LODs plus collision proxies. Patches keep fixes and refinements only. Renumbered by this decision: the former `0.9.0` freeze/beta/audit milestone becomes `0.10.0`, its patches `0.10.1`–`0.10.3`, and `1.0.0` moves to weeks 39–44.
|
||||
|
||||
- **Consequences**: Façade work now has a release gate — every building measured against its own photograph, every building with a collision proxy and a LOD, no placeholder materials — so "modelled" means verified, not merely present. The price is that realism work stops being sliced into patches, and the schedule shifts by one minor cycle. Blockouts without a photograph stay blockouts and are listed as such, rather than being dressed up to fill the milestone.
|
||||
@@ -33,3 +33,13 @@ Ordered by priority: realism first, performance last. The order decides design t
|
||||
| QS-9 | Game runs on dev workstation at 1440p | Frame rate holds the floor | ≥ 60 FPS, 99th percentile ≥ 55 FPS |
|
||||
|
||||
> QS-9 is last on purpose. Performance is a floor that every release must meet, not the goal that shapes the content: realism decides what gets built, the frame budget decides how much of it.
|
||||
|
||||
### Building realism gate (0.9.0)
|
||||
|
||||
| ID | Check | Metric |
|
||||
| --- | --- | --- |
|
||||
| BR-1 | A building with a reference photograph has a review shot compared against it | Silhouette and proportion deltas recorded per building in `Screenshots/README.md`; owner approval per shot |
|
||||
| BR-2 | Every building is either photo-matched or explicitly recorded as an unfounded blockout | List in `TODO.md`; no building in an in-between state |
|
||||
| BR-3 | Every building has a collision proxy and at least one LOD | Count of buildings missing either = 0 |
|
||||
| BR-4 | No surface carries a placeholder material | Material audit in `Tools/ue_audit_scene.py` reports 0 placeholder slots |
|
||||
| BR-5 | The realism pass does not break the floor | QS-9 still holds at 1440p, with the measured cost of the pass recorded |
|
||||
@@ -191,6 +191,7 @@ Static meshes and materials in `/Game/Hollywood/Site/`:
|
||||
| `SM_HeroFacade` | El Capitan Entertainment Centre detailed façade |
|
||||
| `SM_ElCapitanTheatre`, `SM_Budget_6822`, `SM_Hollywood_6806`, `SM_Hollywood_6902`, `SM_Hollywood_6922`, `SM_LeeDrug_6800` | South block frontage (6 buildings) |
|
||||
| `SM_Madame_Tussauds`, `SM_TCL_Chinese_Theatre` | North-side buildings (within 50 m) |
|
||||
| `SM_Retail_LeeDrug`, `SM_Retail_6806` | Retail trim over 6800 and 6806–6820, modelled from their own photograph (`Tools/retail_frontage_build.py`, staged as `RetailTrim_*`). `6822 Budget` stays a bare volume: no photograph exists. |
|
||||
|
||||
### Art Pipeline
|
||||
|
||||
@@ -198,13 +199,14 @@ Python scripts in `Tools/` drive the content pipeline:
|
||||
|
||||
| Script group | Purpose |
|
||||
| --- | --- |
|
||||
| `export_*`, `hero_facade_build` | Blender headless export (FBX) |
|
||||
| `ue_import_*`, `ue_hero_facade_import` | Unreal headless import + placement |
|
||||
| `export_*`, `hero_facade_build`, `retail_frontage_build`, `theatre_trim_build`, `facade_kit_build` | Blender headless export (FBX); trim builders work in world coordinates |
|
||||
| `ue_import_*`, `ue_hero_facade_import`, `ue_import_retail_detail` | Unreal headless import + placement |
|
||||
| `ue_build_demo_map`, `ue_build_review_map` | Map assembly (lighting, meshes, player start) |
|
||||
| `ue_create_input_assets` | Enhanced Input actions + mapping context |
|
||||
| `render_screenshots.sh` | Screenshot capture pipeline |
|
||||
| `judge_screenshot.py` | Vision-model pre-check (optional) |
|
||||
| `input_emulation.py`, `input_test.py` | OS-level virtual keyboard/mouse injection for input verification |
|
||||
| `Tools/_common.py` | Pure helper shared by Blender builders, importable without Blender (bay layout) |
|
||||
| `Tools/tests/` | Stdlib `unittest` tests for the pure parts of the tools (`requirements-tools.txt`) |
|
||||
| `Geo/tools/mapbox_buildings.py` | Second height source: Mapbox `mapbox-streets-v8` building data vs our OSM heights |
|
||||
|
||||
@@ -453,6 +455,14 @@ Rule: place a building only against a neighbour or street mesh, never in isolati
|
||||
|
||||
# Quality Requirements
|
||||
|
||||
## ADR-8: Realistic building façades are a minor milestone, not a patch
|
||||
|
||||
- **Context**: `0.1.4` carried the first façade pass as a _patch_, while the release policy says patches contain fixes and refinements and never new scope. The `0.1.6` retail pass then showed what that costs: a façade built in a patch has no release gate of its own, so a first pass that does not yet read as the building (`Screenshots/README.md`, 0.1.6 gaps) can sit inside a "done" patch series. Realism is also the project's leading quality goal, which a patch cannot carry.
|
||||
|
||||
- **Decision**: Building realism gets its own minor milestone, `0.9.0 — Realistic buildings`, placed before the feature freeze. Its scope is the façade system (orders, profile library, storefront types, signage as decals), per-building photo-matched models, a measured material library with a texel-density rule, and LODs plus collision proxies. Patches keep fixes and refinements only. Renumbered by this decision: the former `0.9.0` freeze/beta/audit milestone becomes `0.10.0`, its patches `0.10.1`–`0.10.3`, and `1.0.0` moves to weeks 39–44.
|
||||
|
||||
- **Consequences**: Façade work now has a release gate — every building measured against its own photograph, every building with a collision proxy and a LOD, no placeholder materials — so "modelled" means verified, not merely present. The price is that realism work stops being sliced into patches, and the schedule shifts by one minor cycle. Blockouts without a photograph stay blockouts and are listed as such, rather than being dressed up to fill the milestone.
|
||||
|
||||
## Quality Requirements Overview
|
||||
|
||||
Ordered by priority: realism first, performance last. The order decides design trade-offs; it does not license dropping a lower item silently (see the trade-off rule below the table).
|
||||
@@ -487,6 +497,16 @@ Ordered by priority: realism first, performance last. The order decides design t
|
||||
|
||||
> QS-9 is last on purpose. Performance is a floor that every release must meet, not the goal that shapes the content: realism decides what gets built, the frame budget decides how much of it.
|
||||
|
||||
### Building realism gate (0.9.0)
|
||||
|
||||
| ID | Check | Metric |
|
||||
| --- | --- | --- |
|
||||
| BR-1 | A building with a reference photograph has a review shot compared against it | Silhouette and proportion deltas recorded per building in `Screenshots/README.md`; owner approval per shot |
|
||||
| BR-2 | Every building is either photo-matched or explicitly recorded as an unfounded blockout | List in `TODO.md`; no building in an in-between state |
|
||||
| BR-3 | Every building has a collision proxy and at least one LOD | Count of buildings missing either = 0 |
|
||||
| BR-4 | No surface carries a placeholder material | Material audit in `Tools/ue_audit_scene.py` reports 0 placeholder slots |
|
||||
| BR-5 | The realism pass does not break the floor | QS-9 still holds at 1440p, with the measured cost of the pass recorded |
|
||||
|
||||
# Risks and Technical Debts
|
||||
|
||||
| ID | Risk / Debt | Priority | Mitigation |
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -523,3 +523,11 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO.
|
||||
- Precedent applied again: tag when the stated scope is done, carry blocked scope
|
||||
into the next patch rather than letting a release wait indefinitely on missing
|
||||
reference photography.
|
||||
|
||||
## 2026-11-09 — Retail frontage and two pipeline traps
|
||||
|
||||
- **`Tools/render_screenshots.sh` called `./Engine/...` for the capture step** while every commandlet above went through `run_cmdlet`'s `cd "$ENGINE"`. With `set -e` the script died at exit 127 *after* rebuilding the maps, so the PNGs silently stayed a day old and `--verify` never ran. Fixed to `"$ENGINE/Engine/Binaries/..."`. Symptom to remember: maps rebuild, screenshots do not move.
|
||||
- **The generic façade kit is applied to every actor labelled `Frontage_*` or `North_*`**, so staging a hand-modelled façade under those labels gets the kit pasted over it — two contradicting façade languages on one wall. `ue_apply_facade_kit.py` now skips `MODELLED` labels, and retail trim stages as `RetailTrim_*`.
|
||||
- **A Blender builder cannot be imported by a unit test** (`import bpy` at module scope). Pure layout arithmetic lives in `Tools/_common.py`; `Tools/tests/test_retail_frontage.py` tests it there, and the builder delegates. Same trick `fbx_export_preset.py` uses for its preset dicts.
|
||||
- **Joining 85 parts into one mesh per building** before export cut the Unreal import from 85 assets to 2, and the staging step to two spawns. Record per-part dimensions *before* the join: after it only the joined bounds exist.
|
||||
|
||||
@@ -108,8 +108,9 @@ Solo-development estimate: **approximately 30–40 weeks**, subject to asset qua
|
||||
| `0.6.0` | 22–24 | Visual/audio pass and evening lighting preset | `0.6.1` lighting fixes; `0.6.2` material corrections; `0.6.3` audio balancing |
|
||||
| `0.7.0` | 25–27 | Settings, control rebinding, save/load, accessibility basics | `0.7.1` save fixes; `0.7.2` UI fixes; `0.7.3` input persistence fixes |
|
||||
| `0.8.0` | 28–30 | Performance budgets and broader Linux hardware testing | `0.8.1` VRAM reductions; `0.8.2` frame-time fixes; `0.8.3` compatibility fixes |
|
||||
| `0.9.0` | 31–34 | Feature freeze, public beta, license and build audit | `0.9.1` blocker fixes; `0.9.2` regression fixes; `0.9.3` documentation fixes |
|
||||
| `1.0.0` | 35–40 | Stable neighborhood-scale walking/driving release | `1.0.x` maintenance and compatibility fixes |
|
||||
| `0.9.0` | 31–34 | Realistic buildings: façade system, per-building models, measured material library, LODs and collision proxies | `0.9.1` façade fixes |
|
||||
| `0.10.0` | 35–38 | Feature freeze, public beta, license and build audit | `0.10.1` blocker fixes; `0.10.2` regression fixes; `0.10.3` documentation fixes |
|
||||
| `1.0.0` | 39–44 | Stable neighborhood-scale walking/driving release | `1.0.x` maintenance and compatibility fixes |
|
||||
|
||||
Finish each milestone's release checks before expanding scope. Cut map size and simulation complexity before compromising the core walking/driving experience.
|
||||
|
||||
|
||||
+23
-7
@@ -8,7 +8,7 @@ Development roadmap for this project: milestone goals, scope, decisions, and rel
|
||||
|
||||
## Status at a glance — 2026-11-08 15:30
|
||||
|
||||
Workstation, geo pipeline, and engine validation are done. The art pipeline has a first blockout, terrain, and street in metres, in one shared coordinate frame. **Project bootstrap is complete**: `Game/GigLine` builds, packages, and both content-acceptance tests (static-mesh scale, MPFB skeletal character) pass. **`v0.1.0` released 2026-11-08**: the walkable demo, hero façade, verified block frontage, first-person controls, ODbL attribution, and every release check pass (traversal, placement, collision, 60 fps at 1440p, fresh-checkout build, packaged build with controls). Next: the `0.1.1`–`0.1.4` patch series (collision detail, scale/façade corrections, packaging fixes, real building façades), **Next:** `0.1.6` retail frontage (6822, 6806–6820, 6800 — blocked on reference photography for two of the three), then `0.2.0` (player body, third-person camera, animation).
|
||||
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), **`v0.1.6` released 2026-11-09**: retail frontage for 6800 Lee Drug and 6806–6820 modelled from their own photograph; 6822 Budget deferred (no reference) and the lettering/parapet detail moved to `0.9.0`. **Next:** `0.2.0` (player body, third-person camera, animation).
|
||||
|
||||
| Area | State |
|
||||
| --- | --- |
|
||||
@@ -168,22 +168,38 @@ Workstation, geo pipeline, and engine validation are done. The art pipeline has
|
||||
|
||||
**Goal:** fix compatibility issues from the hardware testing matrix.
|
||||
|
||||
## 0.9.0 — Feature freeze, public beta, license and build audit (weeks 31–34)
|
||||
## 0.9.0 — Realistic buildings (weeks 31–34)
|
||||
|
||||
**Goal:** every building in the demo area reads as the building it is, at walking distance, judged against its own photograph — and where no photograph exists it stays an honest blockout rather than an invented façade. This is the milestone that turns the façade system from "a kit applied to volumes" into architecture: a shared bay-and-profile vocabulary, a material library with measured albedo and roughness, modelled storefronts and signage, and per-building reference matching with a measured tolerance.
|
||||
|
||||
**Scope:**
|
||||
|
||||
- A façade system that carries more than one order: Art Deco, neoclassical and streamline-moderne variants on the same 4.96 m bay module, with a profile library for cornices, copings, string courses and storefront surrounds.
|
||||
- Per-building façades for the six south-side buildings and the two north-side buildings inside the 50 m buffer, each from its own reference set; the hero keeps its existing model and gains the detail levels this pass defines.
|
||||
- Storefronts and signage as first-class geometry plus decals: mullions, bulkheads, recessed entries, awning and canopy brackets, painted wall lettering, blade and projecting signs. Wall lettering is a texture/decal problem, not a block of geometry — the `0.1.6` attempt proved plates do not read.
|
||||
- A material library with measured values per surface type (stucco, painted render, ashlar, brick, terrazzo, glazing, bronze and steel trim) and a texel-density rule, replacing per-building ad-hoc colours.
|
||||
- LOD chain and collision proxies per building, so realism does not cost the 60 FPS floor.
|
||||
|
||||
**Out of scope:** interiors, night lighting and audio (the `0.6.0` evening preset owns those), vehicles, pedestrians, and any change to footprints or massing — OSM stays the source for those.
|
||||
|
||||
**Release gate:** each building has a review shot compared against its own photograph with the silhouette and proportion deltas recorded in `Screenshots/README.md`; no building carries an unverified façade; every building has a collision proxy and at least one LOD; 60 FPS at 1440p holds; no surface is left with a placeholder material; `THIRD_PARTY_NOTICES.md` covers every photographic reference used.
|
||||
|
||||
## 0.10.0 — Feature freeze, public beta, license and build audit (weeks 35–38)
|
||||
|
||||
**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.10.1 — Blocker fixes (patch)
|
||||
|
||||
**Goal:** fix blockers reported during the public beta.
|
||||
|
||||
## 0.9.2 — Regression fixes (patch)
|
||||
## 0.10.2 — Regression fixes (patch)
|
||||
|
||||
**Goal:** fix regressions introduced by `0.9.x` fixes.
|
||||
**Goal:** fix regressions introduced by `0.10.x` fixes.
|
||||
|
||||
## 0.9.3 — Documentation fixes (patch)
|
||||
## 0.10.3 — Documentation fixes (patch)
|
||||
|
||||
**Goal:** update all `Docs/` for accuracy before `1.0.0`.
|
||||
|
||||
## 1.0.0 — Stable neighborhood-scale walking/driving release (weeks 35–40)
|
||||
## 1.0.0 — Stable neighborhood-scale walking/driving release (weeks 39–44)
|
||||
|
||||
**Goal:** a final acceptance sweep across all `0.x` features, confirmation that the agreed scope (neighbourhood-scale walking and driving) is met, and final build, documentation, and licensing verification. `1.0.0` marks the stable scope, not completion of a whole city.
|
||||
+99
-14
@@ -12,18 +12,88 @@ Tools/render_screenshots.sh --verify
|
||||
|
||||
Stages the demo shot cameras, runs the game on `/Game/Hollywood/DemoMap`, copies the PNGs into `Screenshots/demo/` and then runs [`Tools/visual_loop.py`](../Tools/visual_loop.py) `--verify` to check that each shot still shows the content its expectation describes. About a minute.
|
||||
|
||||
## The shot list
|
||||
## Status at a glance
|
||||
|
||||
Each shot has an **expectation** in [`Tools/visual_targets.json`](../Tools/visual_targets.json) — the content the capture must show — and a verification state produced by `--verify`: it fails if the frame is blank, the wrong size, or has no tonal range.
|
||||
Every shot is embedded below, in order, so the whole review happens in this file.
|
||||
|
||||
| # | File | View | Expected content | Verified | Approved |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 1 | `demo/review-demo-spawn-view.png` | north sidewalk | The whole south block: the neoclassical hero façade between its neighbours, both sidewalks and the road in the foreground | ☑ 2026-11-09 | ☑ 2026-11-09 |
|
||||
| 2 | `demo/review-demo-street-level.png` | eye height, north side | Street level: the hero colonnade with the adjacent frontage buildings either side, shopfronts at pavement level | ☑ 2026-11-09 | ☑ 2026-11-09 |
|
||||
| 3 | `demo/review-demo-crossing.png` | along the crosswalk | The designated mid-block crossing (s=130 m, UE X −14956): zebra bars on the asphalt, south frontage and colonnade behind | ☑ 2026-11-09 | ☑ 2026-11-09 |
|
||||
| 4 | `demo/review-demo-north-buildings.png` | across the Boulevard | The two north-side buildings (TCL Chinese Theatre, Madame Tussaud's) from the south side, full height in frame | ☑ 2026-11-09 | ☑ 2026-11-09 |
|
||||
| # | Shot | Verified | Approved |
|
||||
| --- | ------------------------------------- | ------------ | ------------ |
|
||||
| 1 | [Spawn view](#1-spawn-view) | ☑ 2026-11-09 | ☑ 2026-11-09 |
|
||||
| 2 | [Street level](#2-street-level) | ☑ 2026-11-09 | ☑ 2026-11-09 |
|
||||
| 3 | [Crossing](#3-crossing) | ☑ 2026-11-09 | ☑ 2026-11-09 |
|
||||
| 4 | [North buildings](#4-north-buildings) | ☑ 2026-11-09 | ☑ 2026-11-09 |
|
||||
| 5 | [Retail frontage](#5-retail-frontage) | ☑ 2026-11-09 | ☑ 2026-11-09 |
|
||||
|
||||
Approval is per shot, in the table above (owner review of the `0.1.4` set, recorded in [`Docs/reviews/0.1.4-review.md`](../Docs/reviews/0.1.4-review.md)). Add a row when a shot is added; remove the file in the same change when a shot is retired.
|
||||
**Verified** is what `--verify` reports: the frame is not blank, is the right size, and has tonal range. **Approved** is the owner's verdict, recorded here. Each shot also has an **expectation** in [`Tools/visual_targets.json`](../Tools/visual_targets.json) describing the content it must show.
|
||||
|
||||
---
|
||||
|
||||
## 1. Spawn view
|
||||
|
||||

|
||||
|
||||
- **File:** `demo/review-demo-spawn-view.png`
|
||||
- **View:** north sidewalk, 14 m back, level horizon
|
||||
- **Expected content:** the whole south block — the neoclassical hero façade between its neighbouring buildings, both sidewalks and the road in the foreground
|
||||
- **Verified:** ☑ 2026-11-09 · **Approved:** ☑ 2026-11-09
|
||||
|
||||
---
|
||||
|
||||
## 2. Street level
|
||||
|
||||

|
||||
|
||||
- **File:** `demo/review-demo-street-level.png`
|
||||
- **View:** eye height, north side
|
||||
- **Expected content:** the hero colonnade with the adjacent frontage buildings either side, shopfronts at pavement level
|
||||
- **Verified:** ☑ 2026-11-09 · **Approved:** ☑ 2026-11-09
|
||||
|
||||
---
|
||||
|
||||
## 3. Crossing
|
||||
|
||||

|
||||
|
||||
- **File:** `demo/review-demo-crossing.png`
|
||||
- **View:** from the north sidewalk along the crosswalk
|
||||
- **Expected content:** the designated mid-block crossing (s=130 m, UE X −14956) — zebra bars on the asphalt, south frontage and colonnade behind
|
||||
- **Verified:** ☑ 2026-11-09 · **Approved:** ☑ 2026-11-09
|
||||
|
||||
---
|
||||
|
||||
## 4. North buildings
|
||||
|
||||

|
||||
|
||||
- **File:** `demo/review-demo-north-buildings.png`
|
||||
- **View:** across the Boulevard from the south side
|
||||
- **Expected content:** the two north-side buildings (TCL Chinese Theatre, Madame Tussaud's), full height in frame
|
||||
- **Verified:** ☑ 2026-11-09 · **Approved:** ☑ 2026-11-09
|
||||
|
||||
---
|
||||
|
||||
## 5. Retail frontage
|
||||
|
||||

|
||||
|
||||
- **File:** `demo/review-demo-retail-frontage.png`
|
||||
- **View:** north sidewalk, level horizon, 68° lens
|
||||
- **Expected content:** 6800 Lee Drug and 6806–6820 façades, full width in frame, showing the parapet, mural row, and lettering band
|
||||
- **Verified:** ☑ 2026-11-09 · **Approved:** ☑ 2026-11-09
|
||||
- **Expected content:** the retail run beside the hero — 6800 Lee Drug's bronze cornice, mural row and parapet next to 6806–6820's burgundy fascia, pilasters and window storey
|
||||
- **Verified:** ☑ 2026-11-09 · **Approved:** ☑ 2026-11-09
|
||||
|
||||
Read this one against the reference photograph it was modelled from: `Art/reference/retail-neighbours/01-lee-drug-and-neighbor.jpg` (Gb321, CC BY-SA 4.0).
|
||||
|
||||
Known gaps in this first pass, recorded rather than hidden:
|
||||
|
||||
- **The parapet still reads as a blank band.** It is now two tiers with three deeper reveals, and the step is visible, but there is no profile or shadow line to break the wall — a moulded coping and a modelled reveal need the `0.9.0` façade system.
|
||||
- **The mural row now reads** (framed plates standing 0.12–0.20 m off the wall), but **the lettering band does not**: it is a plate at 0.10 m and it disappears at 40 m. Wall lettering is a decal/texture problem, scoped in `0.9.0`; no amount of extra plate depth fixed it in the second pass.
|
||||
- **The corner tower is a plain cylinder with two cap rings** — the stepped Deco profile in the photograph is richer than that.
|
||||
- **The camera crops the east end** of Lee Drug at 68° from the north sidewalk.
|
||||
- **6822 Budget is untouched** and still a bare blockout volume: no photograph of it exists.
|
||||
|
||||
---
|
||||
|
||||
## The iteration loop (uncommitted)
|
||||
|
||||
@@ -40,12 +110,27 @@ Note the comparison is only meaningful when the shot and the reference frame the
|
||||
## Scene conventions
|
||||
|
||||
- **Demo map** (`/Game/Hollywood/DemoMap`): fixed daytime — sun pitch −62 / yaw 120 (intensity 4, warm), sky light 2.6, sky atmosphere, height fog, fixed exposure.
|
||||
- **Cameras**: elevation shots sit at their target height (pitch 0, so verticals stay parallel) and use a 42–55° lens to fill the frame from across the street.
|
||||
- **Cameras**: elevation shots sit at their target height (pitch 0, so verticals stay parallel) and use a 42–68° lens to fill the frame from across the street.
|
||||
- **Materials**: every surface carries a look-developed material from `Tools/ue_build_demo_map.py` — noise-blended base colour, masonry coursing, and a generated texture set (`Tools/generate_facade_textures.py`) sampled world-aligned. Explicit roughness and specular per surface type.
|
||||
|
||||
## Known cosmetic issues
|
||||
|
||||
- Hero façade renders pale: the stone material is a graph and a texture set, not a scanned surface (the `0.1.4` façade pass is in progress).
|
||||
- Banners sit on the column line rather than flush to the wall (they hang on the columns in the photographs, so this is acceptable).
|
||||
- Between kerb and building line the walkable surface is the terrain mesh, not a modelled pavement.
|
||||
- The El Capitan Theatre has no marquee or blade sign yet — the feature that makes it read as itself.
|
||||
Each issue is tracked in `TODO.md` under the version that will fix it; an issue with no ticket is a bug in the plan, not a note.
|
||||
|
||||
| Issue | Where it is tracked | State |
|
||||
| --- | --- | --- |
|
||||
| Hero stone renders pale — the material is a procedural graph and a generated texture set, not a measured surface | `0.9.0` → Materials, UVs and performance: **measured material library** | Open |
|
||||
| Hero façade has no doors at street level left and right of the central bay, and the central bay elements (columns, stairs, set-back entrance, door frames, signage) are not positioned as the photographs show | `0.9.0` → Per-building models: **hero façade (6840)** | Open |
|
||||
| Nothing should stand in front of the hero façade that blocks it — no awning, no temporary structure | `0.9.0` → Per-building models: **hero façade (6840)**, same item | Open |
|
||||
| Between kerb and building line the walkable surface is the terrain mesh, not a modelled pavement | `0.6.0` → Look and sound: **model the pavement between kerb and building line** | Open |
|
||||
| Retail parapet has no moulded profile, and wall lettering built as a plate does not read at 40 m | `0.9.0` → Per-building models: **6800 Lee Drug and 6806–6820, second pass** | Open (moved out of `0.1.6`) |
|
||||
| 6822 Budget is a bare blockout volume — no photograph exists to model it from | `0.9.0` → Per-building models: **photography gap** | Deferred (moved out of `0.1.6`) |
|
||||
|
||||
Accepted by the owner, not a defect:
|
||||
|
||||
- Banners hang on the column line rather than flush to the wall — that is how they hang in the photographs.
|
||||
|
||||
Resolved and removed from this list on 2026-11-09:
|
||||
|
||||
- "The El Capitan Theatre has no marquee or blade sign" — the trim has been staged since `Tools/ue_stage_theatre_trim.py` landed; the demo map carries all six trim actors (`/tmp/gigline/theatre-import.txt`).
|
||||
- "Materials are not yet created in Blender or fully integrated" — contradicted by the Scene conventions above: every surface carries a look-dev material built in `Tools/ue_build_demo_map.py`.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -9,16 +9,6 @@ Actionable task list for this project. This file is the single, central place fo
|
||||
- When all steps in a version section are done: write a `CHANGELOG.md` entry, then remove the version section from this file.
|
||||
- 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.6 — Retail frontage (patch)
|
||||
|
||||
Suggested blender skills: **references** (reference-image-match), **archviz**, **historical-worlds** for the 1920s–1990s retail mix on this run.
|
||||
|
||||
### Patch tasks
|
||||
|
||||
- [ ] **Model 6822 Budget, 6806–6820 and 6800 Lee Drug from their own references.** `Art/reference/retail-neighbours/` holds 6800 and its neighbour (Gb321, CC BY-SA 4.0); Commons has nothing usable for 6822 or 6806–6820, so those two need photography before they can be modelled honestly. Do not substitute the El Capitan Theatre's 18-frame set — it would misattribute the façades.
|
||||
- [ ] Review every façade change with `Tools/render_screenshots.sh --verify` before tagging.
|
||||
- [ ] Tag `v0.1.6` and write `CHANGELOG.md` entry; then remove this section.
|
||||
|
||||
## 0.2.0 — MPFB player body, third-person camera, basic animation (weeks 7–9)
|
||||
|
||||
### Character setup
|
||||
@@ -182,6 +172,7 @@ Suggested blender skills: **references** (reference-image-match), **archviz**, *
|
||||
|
||||
- [ ] Create an evening lighting preset.
|
||||
- [ ] Complete a visual polish pass across the neighborhood.
|
||||
- [ ] Model the pavement between kerb and building line; today that strip is the terrain mesh, which reads as bare ground under the shopfronts (`Screenshots/README.md`). Carried over 2026-11-09.
|
||||
- [ ] Add ambient audio (city ambience, traffic, basic SFX).
|
||||
- [ ] Verify licenses for all added audio files.
|
||||
|
||||
@@ -285,7 +276,49 @@ Suggested blender skills: **references** (reference-image-match), **archviz**, *
|
||||
- [ ] Fix compatibility issues from the hardware testing matrix.
|
||||
- [ ] Tag `v0.8.3` and write `CHANGELOG.md` entry; then remove this section.
|
||||
|
||||
## 0.9.0 — Feature freeze, public beta, license and build audit (weeks 31–34)
|
||||
## 0.9.0 — Realistic buildings (weeks 31–34)
|
||||
|
||||
Suggested blender skills: **archviz** (real-world scale, façade composition, elevation read), **references** (reference-image-match and visual-match checklists — every building is judged against its own photograph), **procedural-modeling** + **geometry-nodes** (the façades system: bay module, cornice and coping profiles, storefront types as reusable node groups), **materials** (measured albedo and roughness per surface type), **texture-workflow** (baked AO/curvature/normal, decals for painted lettering and signs), **uv-workflow** (texel density and modular UVs), **lod-pipeline** (LOD chain per building), **collision-proxy** (UCX proxies so detail geometry is not the collider), **lookdev** (screenshot comparison loop), **camera-cinematography** (reproducing each building's reference angle), **asset-optimization** + **unreal-export** (pre-export validation, `ue_metric_v1`), **qa-review** (the gate before every review shot).
|
||||
|
||||
### Façade system
|
||||
|
||||
- [ ] **Extend the module kit beyond one order.** Art Deco, neoclassical and streamline-moderne variants on the shared 4.96 m bay: alternate window proportions, spandrel and transom bands, and the way each order terminates at the roof.
|
||||
- [ ] **Build a profile library** for cornices, copings, string courses, sills and storefront surrounds, so a building is assembled from named profiles rather than ad-hoc boxes.
|
||||
- [ ] **Storefront types**: recessed entry, bulkhead and kickplate, mullion spacing, transom, awning/canopy brackets, and grille/shutter variants.
|
||||
- [ ] **Signage and lettering as decals.** Painted wall lettering, blade signs and fascia panels become texture and decal work with a documented projection; `0.1.6` proved that lettering built as geometry plates cannot resolve at review distance.
|
||||
- [ ] Document the system in `Art/blender/README.md` and the arc42 building block view.
|
||||
|
||||
### Per-building models
|
||||
|
||||
Each building needs its own reference set; where none exists the blockout stays and the item says so.
|
||||
|
||||
- [ ] Hero façade (6840): raise to this pass's detail level — capitals and fluting, window rhythm behind the colonnade, parapet motto band and medallions.
|
||||
- [ ] Hero façade (6840), entrance: **doors at street level left and right of the central bay**, as the photographs show, and correct positioning of the central-bay elements (column bases, entrance stairs, set-back entrance, door frames, banners and signage). Nothing may stand in front of the façade that blocks it — no awning, no temporary structure. Carried over from `Screenshots/README.md`, 2026-11-09.
|
||||
- [ ] El Capitan Theatre (6838): marquee, blade sign, entrance and the theatre's own elevation from its 18-frame set.
|
||||
- [ ] 6800 Lee Drug and 6806–6820: second pass on the `0.1.6` trim. Moved here from that patch because geometry cannot close them: **wall lettering as a decal** (a plate at 0.10 m still vanishes at 40 m), a **moulded parapet profile** instead of a two-tier box, richer **corner-tower stepping**, and the portrait panels reworked as textures rather than framed plates.
|
||||
- [ ] **Photography gap — 6822 Budget** (deferred out of `0.1.6`, blocked): no photograph exists; the single-storey 6.0 × 5.5 m volume cannot be read from the neighbour shot, so it stays a bare blockout until references are acquired. Record the sources checked in `Geo/`. Also re-shoot 6806–6820 and 6800 from the south sidewalk: the only reference is one oblique with a bus across Lee Drug's shopfront and a car across 6806's.
|
||||
- [ ] 6902, 6922 and the remaining south frontage.
|
||||
- [ ] North side: TCL Chinese Theatre and Madame Tussaud's façades from their reference sets.
|
||||
- [ ] Reconcile every modelled height against `Geo/tools/block_height_report.py` and record the source per building.
|
||||
|
||||
### Materials, UVs and performance
|
||||
|
||||
- [ ] **Measured material library**, including the hero's stone, which currently renders pale because it is a procedural graph and a generated texture set rather than a measured surface: stucco, painted render, ashlar, brick, terrazzo, glazing, bronze and steel trim, with albedo and roughness taken from the photographs rather than guessed; one Unreal material instance per surface type.
|
||||
- [ ] **Texel-density rule** per building class, applied through modular UVs; lightmap UVs where required.
|
||||
- [ ] **LOD chain and collision proxies** per building: UCX proxies for blocking volume, detail geometry non-colliding.
|
||||
- [ ] Re-validate the 60 FPS floor at 1440p once the detail lands, and record the cost of the pass.
|
||||
|
||||
### Release checks
|
||||
|
||||
- [ ] Every building with a photograph has a review shot measured against it, with silhouette and proportion deltas recorded in `Screenshots/README.md`.
|
||||
- [ ] No building carries an unverified façade; blockouts without references are listed as such in `TODO.md`.
|
||||
- [ ] Every building has a collision proxy and at least one LOD.
|
||||
- [ ] No surface left on a placeholder material.
|
||||
- [ ] `THIRD_PARTY_NOTICES.md` covers every photographic reference used in this pass.
|
||||
- [ ] Performance target still met at 1440p.
|
||||
- [ ] Tag `v0.9.0` and write `CHANGELOG.md` entry; then remove this section.
|
||||
|
||||
## 0.10.0 — Feature freeze, public beta, license and build audit (weeks 35–38)
|
||||
|
||||
### Freeze and audit
|
||||
|
||||
@@ -297,29 +330,29 @@ Suggested blender skills: **references** (reference-image-match), **archviz**, *
|
||||
|
||||
### Release checks
|
||||
|
||||
- [ ] Beta feedback triaged; blockers logged for `0.9.x`.
|
||||
- [ ] Tag `v0.9.0` and write `CHANGELOG.md` entry; then remove this section.
|
||||
- [ ] Beta feedback triaged; blockers logged for `0.10.x`.
|
||||
- [ ] Tag `v0.10.0` and write `CHANGELOG.md` entry; then remove this section.
|
||||
|
||||
## 0.9.1 — Blocker fixes (patch)
|
||||
## 0.10.1 — Blocker fixes (patch)
|
||||
|
||||
### Patch tasks
|
||||
|
||||
- [ ] Fix blockers reported during the public beta.
|
||||
- [ ] Tag `v0.9.1` and write `CHANGELOG.md` entry; then remove this section.
|
||||
- [ ] Tag `v0.10.1` and write `CHANGELOG.md` entry; then remove this section.
|
||||
|
||||
## 0.9.2 — Regression fixes (patch)
|
||||
## 0.10.2 — Regression fixes (patch)
|
||||
|
||||
### Patch tasks
|
||||
|
||||
- [ ] Fix regressions introduced by `0.9.x` fixes.
|
||||
- [ ] Tag `v0.9.2` and write `CHANGELOG.md` entry; then remove this section.
|
||||
- [ ] Tag `v0.10.2` and write `CHANGELOG.md` entry; then remove this section.
|
||||
|
||||
## 0.9.3 — Documentation fixes (patch)
|
||||
## 0.10.3 — Documentation fixes (patch)
|
||||
|
||||
### Patch tasks
|
||||
|
||||
- [ ] Update all `Docs/` for accuracy before `1.0.0`.
|
||||
- [ ] Tag `v0.9.3` and write `CHANGELOG.md` entry; then remove this section.
|
||||
- [ ] Tag `v0.10.3` and write `CHANGELOG.md` entry; then remove this section.
|
||||
|
||||
## 1.0.0 — Stable neighborhood-scale walking/driving release (weeks 35–40)
|
||||
|
||||
|
||||
@@ -17,7 +17,10 @@ Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side b
|
||||
| --- | --- |
|
||||
| `check-lfs.sh` | Fails if the `lfs` filter is missing or an LFS-tracked file was committed as a plain blob |
|
||||
| `facade_kit_build.py` | Builds the reusable façade **module kit** at the measured 4.96 m bay: pilaster, sill, lintel, window bay (surround + glazing), shopfront bay (piers, glass, fascia), door bay (jambs, door, threshold), cornice and parapet + coping. Exports each piece with the shared preset plus a JSON part report (dims + evidence per piece). |
|
||||
| `ue_import_retail_detail.py` | Imports the two retail trim meshes into `/Game/Hollywood/Frontage` with complex-as-simple collision — the trim decorates a solid the player already collides with, so it must not add snag points. Staged by the demo builder as `RetailTrim_*`. |
|
||||
| `ue_import_facade_kit.py` | Imports the kit into `/Game/Hollywood/Kit/*` as static meshes with complex-as-simple collision; writes `/tmp/gigline/kit-import.txt`. |
|
||||
| `retail_frontage_build.py` | Retail frontage trim for **6800 Lee Drug** and **6806–6820** (0.1.6): bronze cornice, mural row, lettering band, stepped Deco parapet and rounded corner tower on Lee Drug; burgundy fascia, pilasters, white-framed windows and banded parapet on 6806–6820. Built in world coordinates from the blockout bounds, one joined mesh per building, JSON part report with the evidence behind every piece. `6822 Budget` is **not** built — no photograph exists. |
|
||||
| `retail_frontage_build.py` | Retail frontage trim for **6800 Lee Drug** and **6806–6820** (0.1.6): bronze cornice, mural row, lettering band, stepped Deco parapet and rounded corner tower on Lee Drug; burgundy fascia, pilasters, white-framed windows and banded parapet on 6806–6820. Built in world coordinates from the blockout bounds, one joined mesh per building, JSON part report with the evidence behind every piece. `6822 Budget` is **not** built — no photograph exists. |
|
||||
| `theatre_trim_build.py` | El Capitan Theatre trim: projecting marquee canopy, illuminated fascia, recessed soffit, vertical blade sign with frame and bracket. Built **in world coordinates** (span and pavement height baked in) with a JSON part report, so staging is a single spawn at the origin. |
|
||||
| `ue_stage_theatre_trim.py` | Imports the trim to `/Game/Hollywood/Theatre` and stages one actor per piece in the DemoMap with site materials; clears a previous run first. |
|
||||
| `ue_apply_facade_kit.py` | Lays the kit over a building's street face from its own bounding box: bay count = round(width / 4.96), storeys from the height, shopfronts on the ground floor, windows above, pilasters on bay boundaries, cornice and parapet at the top. Clears a previous application first; unique labels per building. |
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"""Shared helpers that must stay importable outside Blender and Unreal.
|
||||
|
||||
Blender-side builders import `bpy` at module scope, so nothing in them can be unit
|
||||
tested. Pure layout arithmetic lives here instead: the builders import it, and
|
||||
`Tools/tests/` imports it without a 3D application.
|
||||
|
||||
Standard library only.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def bay_centres(x0, x1, margin=0.9, bay=2.2):
|
||||
"""Centres of equal bays across a frontage, inset by `margin` at each end.
|
||||
|
||||
Used by the façade kit and the retail frontage so both use one rhythm. The count
|
||||
is the span divided by the bay module and rounded, so a 33 m frontage at a 2.2 m
|
||||
module gets 14 bays rather than one bay per 2.2 m of wall with a stub at the end.
|
||||
|
||||
Returns [] when the margins consume the whole frontage.
|
||||
"""
|
||||
span = (x1 - x0) - 2.0 * margin
|
||||
if span <= 0:
|
||||
return []
|
||||
count = max(1, int(round(span / bay)))
|
||||
step = span / count
|
||||
return [x0 + margin + step * (i + 0.5) for i in range(count)]
|
||||
@@ -39,7 +39,10 @@ run_cmdlet ue_build_review_map.py "5/6 stage the demo shot cameras"
|
||||
tail -3 /tmp/ue-review-map-result.txt 2>/dev/null || true
|
||||
|
||||
echo "== 6/6 capture the demo map =="
|
||||
./Engine/Binaries/Linux/UnrealEditor-Cmd "$PROJ" /Game/Hollywood/DemoMap \
|
||||
# The engine path is absolute: this step runs from the repo, not from $ENGINE, and a
|
||||
# bare ./Engine/... failed with "No such file or directory" while every commandlet
|
||||
# above succeeded through run_cmdlet's cd.
|
||||
"$ENGINE/Engine/Binaries/Linux/UnrealEditor-Cmd" "$PROJ" /Game/Hollywood/DemoMap \
|
||||
-game -windowed -ResX=1920 -ResY=1080 -nopause -nosplash -stdout
|
||||
|
||||
echo "== collect the demo screenshots =="
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Retail frontage trim for 6800 Lee Drug and 6806–6820 (0.1.6).
|
||||
|
||||
Both buildings are bare OSM extrusions with a flat stucco material; what makes them
|
||||
read as *the* retail run beside the hero is the façade layer: the bronze cornice and
|
||||
stepped Deco parapet on Lee Drug, and the pilaster-and-fascia rhythm on 6806–6820.
|
||||
|
||||
The trim is built **in world coordinates** in the shared frame (metres, X east,
|
||||
Y north, Z from the project datum), so the export carries its position and the
|
||||
Unreal staging step is a single spawn at the origin — the same convention as
|
||||
`Tools/theatre_trim_build.py` and `Tools/export_block_frontage.py`.
|
||||
|
||||
blender -b --factory-startup --python Tools/retail_frontage_build.py -- \
|
||||
--out /tmp/gigline/retail --report /tmp/gigline/retail-report.json
|
||||
|
||||
Bounds are **measured**, not assumed: they are the blockout objects' world bounds
|
||||
(`Lee Drug Building`, `element.019`), which sit at Z 0 with the volume top at 12.10 m.
|
||||
Vertical proportions come from the single reference photograph and are recorded with
|
||||
the assumption in `Art/blender/retail/ref-analysis.md`.
|
||||
|
||||
6822 Budget is deliberately **not** built: there is no photograph of it, and the TODO
|
||||
item says so. Guessing its façade would be the misattribution the item warns against.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import bpy
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import _common # noqa: E402
|
||||
import fbx_export_preset as fbx_preset # noqa: E402
|
||||
|
||||
# --- measured bounds, from the blockout (world metres) ------------------------
|
||||
# Lee Drug 6800, OSM way 424024149. Front face is the north edge (max Y).
|
||||
LD_X0, LD_X1 = -33.20, -14.37
|
||||
LD_FACE_Y = -14.63
|
||||
LD_H = 12.10
|
||||
|
||||
# 6806-6820, OSM way 424024150.
|
||||
B6_X0, B6_X1 = -66.08, -33.07
|
||||
B6_FACE_Y = -12.52
|
||||
B6_H = 12.10
|
||||
|
||||
# --- proportions, read off the reference photograph ---------------------------
|
||||
# Fractions of the 12.10 m height; see Art/blender/retail/ref-analysis.md for how
|
||||
# they were read and why they are assumptions at ±0.03 H.
|
||||
LD_SHOP_TOP = 0.36 * LD_H # 4.36 m: shopfront glazing / cornice soffit
|
||||
LD_CORNICE_TOP = 0.43 * LD_H # 5.20 m: top of the bronze band
|
||||
LD_MURAL_Z = (0.49 * LD_H, 0.61 * LD_H)
|
||||
LD_LETTER_Z = (0.65 * LD_H, 0.76 * LD_H)
|
||||
LD_PARAPET_Z = 0.82 * LD_H # 9.92 m: bottom of the stepped parapet
|
||||
|
||||
B6_SHOP_TOP = 0.32 * B6_H # 3.87 m
|
||||
B6_FASCIA_TOP = 0.38 * B6_H # 4.60 m
|
||||
B6_WINDOW_Z = (0.47 * B6_H, 0.64 * B6_H)
|
||||
B6_PARAPET_Z = 0.74 * B6_H # 8.95 m (first pass used 0.62 and left a 4.6 m blank band)
|
||||
|
||||
BAY = 2.20 # mural / bay rhythm on Lee Drug, assumed
|
||||
PROJECT = 0.55 # bronze cornice projection, assumed
|
||||
TOWER_R = 2.10 # Lee Drug corner tower radius, assumed
|
||||
|
||||
PARTS: list[dict] = []
|
||||
|
||||
|
||||
def make_material(name, rgb, rough=0.6, metallic=0.0):
|
||||
"""Principled material with an explicit colour; the export preset carries it as a slot."""
|
||||
mat = bpy.data.materials.new(name)
|
||||
mat.use_nodes = True
|
||||
bsdf = mat.node_tree.nodes.get("Principled BSDF")
|
||||
bsdf.inputs["Base Color"].default_value = (rgb[0], rgb[1], rgb[2], 1.0)
|
||||
bsdf.inputs["Roughness"].default_value = rough
|
||||
bsdf.inputs["Metallic"].default_value = metallic
|
||||
return mat
|
||||
|
||||
|
||||
def clear_scene():
|
||||
bpy.ops.object.select_all(action="SELECT")
|
||||
bpy.ops.object.delete(use_global=False)
|
||||
for mesh in list(bpy.data.meshes):
|
||||
if mesh.users == 0:
|
||||
bpy.data.meshes.remove(mesh)
|
||||
|
||||
|
||||
def cube(name, x, y, z, dx, dy, dz, material):
|
||||
bpy.ops.mesh.primitive_cube_add(size=1.0, location=(x, y, z))
|
||||
obj = bpy.context.active_object
|
||||
obj.name = name
|
||||
obj.scale = (dx, dy, dz)
|
||||
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
|
||||
obj.data.materials.append(material)
|
||||
return obj
|
||||
|
||||
|
||||
def cylinder(name, x, y, z, radius, height, material, vertices=28):
|
||||
bpy.ops.mesh.primitive_cylinder_add(vertices=vertices, radius=radius, depth=height,
|
||||
location=(x, y, z))
|
||||
obj = bpy.context.active_object
|
||||
obj.name = name
|
||||
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
|
||||
obj.data.materials.append(material)
|
||||
return obj
|
||||
|
||||
|
||||
def bay_centres(x0, x1, margin=0.9, bay=BAY):
|
||||
"""Bay centres; the arithmetic lives in `_common` so it is testable without Blender."""
|
||||
return _common.bay_centres(x0, x1, margin=margin, bay=bay)
|
||||
|
||||
|
||||
def note(obj, evidence):
|
||||
PARTS.append({
|
||||
"object": obj.name,
|
||||
"asset": "SM_" + obj.name,
|
||||
"dims_m": [round(d, 3) for d in obj.dimensions],
|
||||
"location_m": [round(c, 3) for c in obj.location],
|
||||
"evidence": evidence,
|
||||
})
|
||||
print(f"[retail] {obj.name:26} {obj.dimensions.x:6.2f} x {obj.dimensions.y:5.2f} "
|
||||
f"x {obj.dimensions.z:5.2f} m ({evidence})")
|
||||
|
||||
|
||||
def build_lee_drug(mats):
|
||||
"""6800 Lee Drug: bronze cornice, mural row, lettering band, stepped Deco parapet,
|
||||
rounded corner tower."""
|
||||
stucco, bronze, mural, glass, white = (mats["stucco"], mats["bronze"], mats["mural"],
|
||||
mats["glass"], mats["white"])
|
||||
lettering, reveal = mats["lettering"], mats["reveal"]
|
||||
parts = []
|
||||
cx = (LD_X0 + LD_X1) / 2.0
|
||||
width = LD_X1 - LD_X0
|
||||
|
||||
# Bronze cornice: the strongest horizontal line on the building.
|
||||
cornice = cube("LD_Cornice", cx, LD_FACE_Y + PROJECT / 2.0,
|
||||
(LD_SHOP_TOP + LD_CORNICE_TOP) / 2.0,
|
||||
width + 0.4, PROJECT, LD_CORNICE_TOP - LD_SHOP_TOP, bronze)
|
||||
parts.append((cornice, "photographs: projecting bronze cornice over the shopfront; "
|
||||
f"{PROJECT:.2f} m projection assumed"))
|
||||
|
||||
# Shopfront glazing, recessed behind the face, one panel per bay.
|
||||
for i, bx in enumerate(bay_centres(LD_X0, LD_X1, margin=1.2), start=1):
|
||||
panel = cube(f"LD_ShopGlass_{i}", bx, LD_FACE_Y + 0.12, LD_SHOP_TOP / 2.0,
|
||||
BAY - 0.30, 0.10, LD_SHOP_TOP - 0.55, glass)
|
||||
parts.append((panel, "photographs: full-height shopfront glazing; bay width and "
|
||||
"the 0.55 m kickplate are assumed (a bus hides this zone)"))
|
||||
mullion = cube(f"LD_Mullion_{i}", bx - (BAY - 0.30) / 2.0, LD_FACE_Y - 0.02,
|
||||
LD_SHOP_TOP / 2.0, 0.10, 0.16, LD_SHOP_TOP - 0.55, bronze)
|
||||
parts.append((mullion, "assumption: slim mullion at each bay boundary"))
|
||||
|
||||
# Celebrity-portrait panels, one per bay, in the band above the cornice.
|
||||
#
|
||||
# First pass modelled these as a shallow plate on the wall plane and they vanished
|
||||
# at review distance: no shadow, no edge. Now each panel is a near-black plate set
|
||||
# 0.14 m back inside a stucco frame that stands 0.10 m proud, so the sun casts a
|
||||
# shadow line down one side and the row reads as a row of pictures.
|
||||
z0, z1 = LD_MURAL_Z
|
||||
for i, bx in enumerate(bay_centres(LD_X0, LD_X1, margin=1.2), start=1):
|
||||
frame = cube(f"LD_MuralFrame_{i}", bx, LD_FACE_Y + 0.20, (z0 + z1) / 2.0,
|
||||
BAY - 0.55, 0.12, (z1 - z0) + 0.30, white)
|
||||
parts.append((frame, "photographs: stucco frame around each portrait panel; "
|
||||
"0.30 m border assumed"))
|
||||
panel = cube(f"LD_Mural_{i}", bx, LD_FACE_Y + 0.12, (z0 + z1) / 2.0,
|
||||
BAY - 0.85, 0.06, (z1 - z0), mural)
|
||||
parts.append((panel, "photographs: row of black-framed portrait panels; count and "
|
||||
"size assumed from the visible row"))
|
||||
|
||||
# Lettering band: the photographs show dark letters painted straight on the stucco,
|
||||
# so this is a slightly proud plate in a mid-dark tone rather than a black recess.
|
||||
# The words themselves remain a decal job; the plate only reserves the band.
|
||||
lz0, lz1 = LD_LETTER_Z
|
||||
letter = cube("LD_LetterBand", cx, LD_FACE_Y + 0.10, (lz0 + lz1) / 2.0,
|
||||
width - 3.6, 0.10, lz1 - lz0, lettering)
|
||||
parts.append((letter, "photographs: large dark lettering across the upper wall; "
|
||||
"modelled as a painted band, the text itself is a decal job"))
|
||||
|
||||
# Stepped Deco parapet. A single 2.18 m wall with 0.10 m reveals read as one blank
|
||||
# white band, so it is now two tiers with a 0.10 m step between them, and the
|
||||
# reveals are 0.22 m tall and cut 0.16 m deep - deep enough to hold a shadow.
|
||||
tier_h = (LD_H - LD_PARAPET_Z) / 2.0
|
||||
lower = cube("LD_ParapetLower", cx, LD_FACE_Y + 0.14, LD_PARAPET_Z + tier_h / 2.0,
|
||||
width + 0.2, 0.34, tier_h, stucco)
|
||||
parts.append((lower, "photographs: parapet lower tier above the frontage wall; "
|
||||
"thickness assumed"))
|
||||
upper = cube("LD_ParapetUpper", cx, LD_FACE_Y + 0.24, LD_PARAPET_Z + tier_h * 1.5,
|
||||
width - 3.2, 0.30, tier_h, stucco)
|
||||
parts.append((upper, "photographs: parapet upper tier, stepped back from the lower "
|
||||
"one; setback and length assumed"))
|
||||
for i, (z, span) in enumerate(((LD_PARAPET_Z + tier_h * 0.45, width + 0.2),
|
||||
(LD_PARAPET_Z + tier_h * 1.30, width + 0.2),
|
||||
(LD_H - 0.14, width - 3.2)), start=1):
|
||||
groove = cube(f"LD_ParapetReveal_{i}", cx, LD_FACE_Y - 0.02, z,
|
||||
span, 0.16, 0.22, reveal)
|
||||
parts.append((groove, "photographs: horizontal reveal lines in the parapet; "
|
||||
"count read off the frame"))
|
||||
|
||||
# Rounded corner tower at the east end, stepped twice at the top. The centre
|
||||
# sits INSIDE the footprint so the tower projects 0.35 m from the face instead of
|
||||
# bulging 4 m into the pavement, which the first run did.
|
||||
tower_y = LD_FACE_Y - TOWER_R + 0.35
|
||||
tower = cylinder("LD_CornerTower", LD_X1 - TOWER_R, tower_y,
|
||||
(LD_H - 0.9) / 2.0, TOWER_R, LD_H - 0.9, stucco)
|
||||
parts.append((tower, "photographs: rounded Deco corner tower rising above the "
|
||||
f"frontage; {TOWER_R:.2f} m radius assumed"))
|
||||
# Vertical fins around the curve: the photograph's tower is fluted, not a smooth
|
||||
# drum. Twelve ribs at this radius are legible from the street.
|
||||
for i in range(12):
|
||||
ang = math.pi * (i + 0.5) / 12.0
|
||||
fx = (LD_X1 - TOWER_R) + math.cos(ang) * (TOWER_R + 0.06)
|
||||
fy = tower_y + math.sin(ang) * (TOWER_R + 0.06)
|
||||
fin = cube(f"LD_TowerFin_{i + 1}", fx, fy, (LD_H - 0.9) / 2.0,
|
||||
0.16, 0.16, LD_H - 0.9, white)
|
||||
parts.append((fin, "photographs: fluted fins on the corner tower; count assumed"))
|
||||
|
||||
# Cap rings stack exactly to the measured 12.10 m top: 11.20 -> 11.75 -> 12.10.
|
||||
cap_z = LD_H - 0.9
|
||||
for i, (dh, radius) in enumerate(((0.55, TOWER_R - 0.35), (0.35, TOWER_R - 0.70)), start=1):
|
||||
cap = cylinder(f"LD_TowerCap_{i}", LD_X1 - TOWER_R, tower_y,
|
||||
cap_z + dh / 2.0, radius, dh, white)
|
||||
parts.append((cap, "photographs: two stepped cap rings on the tower"))
|
||||
cap_z += dh
|
||||
return parts
|
||||
|
||||
|
||||
def build_6806(mats):
|
||||
"""6806–6820: burgundy fascia, brown pilasters, white-framed windows, banded parapet."""
|
||||
stucco, bronze, glass, white, burgundy = (mats["stucco"], mats["bronze"], mats["glass"],
|
||||
mats["white"], mats["burgundy"])
|
||||
reveal = mats["reveal"]
|
||||
parts = []
|
||||
cx = (B6_X0 + B6_X1) / 2.0
|
||||
width = B6_X1 - B6_X0
|
||||
|
||||
fascia = cube("B6_Fascia", cx, B6_FACE_Y - 0.02,
|
||||
(B6_SHOP_TOP + B6_FASCIA_TOP) / 2.0, width, 0.22,
|
||||
B6_FASCIA_TOP - B6_SHOP_TOP, burgundy)
|
||||
parts.append((fascia, "photographs: dark fascia band over the shopfront; colour "
|
||||
"sampled, height assumed"))
|
||||
|
||||
centres = bay_centres(B6_X0, B6_X1, margin=1.0)
|
||||
for i, bx in enumerate(centres, start=1):
|
||||
panel = cube(f"B6_ShopGlass_{i}", bx, B6_FACE_Y + 0.12, B6_SHOP_TOP / 2.0,
|
||||
BAY - 0.25, 0.10, B6_SHOP_TOP - 0.45, glass)
|
||||
parts.append((panel, "photographs: framed shopfront glazing behind the fascia"))
|
||||
# Brown pilaster on every second bay boundary, as the photo shows.
|
||||
if i % 2 == 1:
|
||||
pil = cube(f"B6_Pilaster_{i}", bx, B6_FACE_Y - 0.10,
|
||||
(B6_FASCIA_TOP + B6_PARAPET_Z) / 2.0, 0.40, 0.22,
|
||||
B6_PARAPET_Z - B6_FASCIA_TOP, bronze)
|
||||
parts.append((pil, "photographs: brown pilaster strips dividing the frontage"))
|
||||
|
||||
wz0, wz1 = B6_WINDOW_Z
|
||||
for i, bx in enumerate(centres, start=1):
|
||||
frame = cube(f"B6_WindowFrame_{i}", bx, B6_FACE_Y - 0.08, (wz0 + wz1) / 2.0,
|
||||
1.70, 0.18, wz1 - wz0, white)
|
||||
parts.append((frame, "photographs: white-framed upper windows with security bars; "
|
||||
"count and size assumed"))
|
||||
pane = cube(f"B6_WindowGlass_{i}", bx, B6_FACE_Y + 0.04, (wz0 + wz1) / 2.0,
|
||||
1.46, 0.08, wz1 - wz0 - 0.24, glass)
|
||||
parts.append((pane, "photographs: dark glazing inside each frame"))
|
||||
# Security bars: three thin verticals per window, the detail that stops the
|
||||
# upper storey reading as a row of black rectangles.
|
||||
for b in (-1, 0, 1):
|
||||
bar = cube(f"B6_WindowBar_{i}_{b + 2}", bx + b * 0.40, B6_FACE_Y - 0.15,
|
||||
(wz0 + wz1) / 2.0, 0.06, 0.06, wz1 - wz0 - 0.30, white)
|
||||
parts.append((bar, "photographs: vertical security bars on the upper windows"))
|
||||
|
||||
# Two-tier parapet with deep reveals, as on Lee Drug: one flat band with hairlines
|
||||
# disappeared at review distance.
|
||||
tier_h = (B6_H - B6_PARAPET_Z) / 2.0
|
||||
para = cube("B6_Parapet", cx, B6_FACE_Y + 0.12, B6_PARAPET_Z + tier_h / 2.0,
|
||||
width + 0.2, 0.30, tier_h, stucco)
|
||||
parts.append((para, "photographs: parapet lower tier above the window storey; "
|
||||
"thickness assumed"))
|
||||
upper = cube("B6_ParapetUpper", cx, B6_FACE_Y + 0.20, B6_PARAPET_Z + tier_h * 1.5,
|
||||
width - 2.4, 0.26, tier_h, stucco)
|
||||
parts.append((upper, "photographs: parapet upper tier stepped back; length assumed"))
|
||||
for i, (z, span) in enumerate(((B6_PARAPET_Z + tier_h * 0.45, width + 0.2),
|
||||
(B6_PARAPET_Z + tier_h * 1.30, width + 0.2),
|
||||
(B6_H - 0.12, width - 2.4)), start=1):
|
||||
groove = cube(f"B6_ParapetReveal_{i}", cx, B6_FACE_Y - 0.02, z,
|
||||
span, 0.14, 0.18, reveal)
|
||||
parts.append((groove, "photographs: reveal lines in the parapet; count assumed"))
|
||||
return parts
|
||||
|
||||
|
||||
def join_parts(parts, name):
|
||||
"""Join one building's parts into a single mesh so Unreal imports one asset.
|
||||
|
||||
The per-part dimensions and evidence are recorded before this runs, so joining
|
||||
loses nothing from the report. Materials survive as separate slots.
|
||||
"""
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
for obj, _text in parts:
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = parts[0][0]
|
||||
bpy.ops.object.join()
|
||||
joined = bpy.context.active_object
|
||||
joined.name = name
|
||||
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
|
||||
return joined
|
||||
|
||||
|
||||
def export_objects(objects, out_dir):
|
||||
count = 0
|
||||
for obj in objects:
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
fbx_preset.export_static(os.path.join(out_dir, "SM_" + obj.name + ".fbx"),
|
||||
selection=True)
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--out", default="/tmp/gigline/retail")
|
||||
ap.add_argument("--report", default=None)
|
||||
args = ap.parse_args(argv)
|
||||
os.makedirs(args.out, exist_ok=True)
|
||||
|
||||
clear_scene()
|
||||
mats = {
|
||||
"stucco": make_material("M_Retail_Stucco", (0.86, 0.83, 0.76), rough=0.72),
|
||||
"bronze": make_material("M_Retail_Bronze", (0.20, 0.11, 0.06), rough=0.45, metallic=0.35),
|
||||
"mural": make_material("M_Retail_Mural", (0.035, 0.032, 0.030), rough=0.55),
|
||||
"glass": make_material("M_Retail_Glass", (0.035, 0.045, 0.055), rough=0.15),
|
||||
"white": make_material("M_Retail_White", (0.90, 0.88, 0.84), rough=0.60),
|
||||
"burgundy": make_material("M_Retail_Burgundy", (0.20, 0.09, 0.09), rough=0.55),
|
||||
# Painted lettering and shadow reveals: same value family as the wall so they
|
||||
# read as paint and shadow, not as black holes.
|
||||
"lettering": make_material("M_Retail_Lettering", (0.30, 0.27, 0.24), rough=0.65),
|
||||
"reveal": make_material("M_Retail_Reveal", (0.42, 0.38, 0.34), rough=0.70),
|
||||
}
|
||||
|
||||
ld_parts = build_lee_drug(mats)
|
||||
b6_parts = build_6806(mats)
|
||||
for obj, text in ld_parts + b6_parts:
|
||||
note(obj, text)
|
||||
|
||||
# One mesh per building: 85 separate actors over the frontage would cost more
|
||||
# draw calls than the trim is worth, and the staging step stays two spawns.
|
||||
joined = [join_parts(ld_parts, "Retail_LeeDrug"), join_parts(b6_parts, "Retail_6806")]
|
||||
for obj in joined:
|
||||
print(f"[retail] joined {obj.name}: {len(obj.data.vertices)} verts, "
|
||||
f"{len(obj.data.materials)} material slot(s), "
|
||||
f"X {min(v.co.x for v in obj.data.vertices):.2f}.."
|
||||
f"{max(v.co.x for v in obj.data.vertices):.2f}")
|
||||
exported = export_objects(joined, args.out)
|
||||
print(f"[retail] exported {exported} object(s) to {args.out}")
|
||||
|
||||
if args.report:
|
||||
with open(args.report, "w", encoding="utf-8") as handle:
|
||||
json.dump({
|
||||
"scope": ["6800 Lee Drug (way 424024149)", "6806-6820 (way 424024150)"],
|
||||
"deferred": ["6822 Budget (way 424024144): no photograph, not modelled"],
|
||||
"bounds_m": {
|
||||
"lee_drug": [LD_X0, LD_X1, LD_FACE_Y, LD_H],
|
||||
"b6806": [B6_X0, B6_X1, B6_FACE_Y, B6_H],
|
||||
},
|
||||
"reference": "Art/reference/retail-neighbours/01-lee-drug-and-neighbor.jpg "
|
||||
"(Gb321, CC BY-SA 4.0)",
|
||||
"analysis": "Art/blender/retail/ref-analysis.md",
|
||||
"parts": PARTS,
|
||||
}, handle, indent=2)
|
||||
print(f"[retail] report -> {args.report}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Tests for the pure layout helper in Tools/retail_frontage_build.py.
|
||||
|
||||
The builder itself needs Blender; `bay_centres` does not, so the bay rhythm the
|
||||
façade depends on is checkable without starting Blender or Unreal.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
from Tools._common import bay_centres
|
||||
|
||||
|
||||
class BayCentreTests(unittest.TestCase):
|
||||
def test_centres_stay_inside_the_margins(self):
|
||||
centres = bay_centres(-33.20, -14.37, margin=1.2)
|
||||
self.assertGreaterEqual(min(centres), -33.20 + 1.2 - 1e-9)
|
||||
self.assertLessEqual(max(centres), -14.37 - 1.2 + 1e-9)
|
||||
|
||||
def test_bays_are_evenly_spaced(self):
|
||||
centres = bay_centres(-66.08, -33.07, margin=1.0)
|
||||
steps = {round(b - a, 6) for a, b in zip(centres, centres[1:])}
|
||||
self.assertEqual(len(steps), 1, f"uneven bay spacing: {steps}")
|
||||
|
||||
def test_bay_count_is_the_round_of_span_over_bay(self):
|
||||
# 33.01 m frontage, 1.0 m margin each end -> 31.01 m -> 14 bays at 2.2 m.
|
||||
self.assertEqual(len(bay_centres(-66.08, -33.07, margin=1.0)), 14)
|
||||
|
||||
def test_degenerate_frontage_returns_nothing(self):
|
||||
self.assertEqual(bay_centres(0.0, 1.0, margin=1.0), [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -235,9 +235,17 @@ def main() -> None:
|
||||
removed += 1
|
||||
log(f"cleared {removed} previous kit actors")
|
||||
|
||||
# The kit is the fallback for buildings that have nothing but a blockout. These
|
||||
# two are modelled from their own photograph in 0.1.6
|
||||
# (Tools/retail_frontage_build.py); pasting the generic kit over them put a
|
||||
# second, contradicting facade language on the same wall.
|
||||
MODELLED = ("Frontage_SM_LeeDrug_6800", "Frontage_SM_Hollywood_6806")
|
||||
targets = [a for a in unreal.EditorLevelLibrary.get_all_level_actors()
|
||||
if a.get_actor_label().startswith(("Frontage_", "North_"))]
|
||||
log(f"frontage actors: {len(targets)}")
|
||||
if a.get_actor_label().startswith(("Frontage_", "North_"))
|
||||
and a.get_actor_label() not in MODELLED]
|
||||
skipped = [a.get_actor_label() for a in unreal.EditorLevelLibrary.get_all_level_actors()
|
||||
if a.get_actor_label() in MODELLED]
|
||||
log(f"frontage actors: {len(targets)} (kit skipped on modelled facades: {skipped})")
|
||||
total = 0
|
||||
for actor in targets:
|
||||
total += apply_to_actor(actor, kit, materials)
|
||||
|
||||
@@ -305,7 +305,8 @@ def build():
|
||||
log(f"could not create {MAP}")
|
||||
return
|
||||
|
||||
removed = clear(("Demo_", "PlayerStart", "Hero_Facade", "Frontage_", "ShotSequencer"))
|
||||
removed = clear(("Demo_", "PlayerStart", "Hero_Facade", "Frontage_", "RetailTrim_",
|
||||
"ShotSequencer"))
|
||||
log(f"cleared {removed} previous demo actors")
|
||||
|
||||
# --- lighting: fixed daytime ------------------------------------------
|
||||
@@ -597,6 +598,37 @@ def build():
|
||||
staged_frontage += 1
|
||||
log(f"staged {staged_frontage} frontage buildings")
|
||||
|
||||
# --- retail frontage trim (0.1.6) -------------------------------------
|
||||
# The two retail buildings beside the hero are modelled from their own
|
||||
# photograph (Art/reference/retail-neighbours/); the trim is laid over the
|
||||
# blockout volume, so it keeps the trim's own sampled materials rather than a
|
||||
# look-dev override. 6822 Budget has no photograph and stays a bare volume.
|
||||
for asset_name, label in (("SM_Retail_LeeDrug", "6800 Lee Drug trim"),
|
||||
("SM_Retail_6806", "6806-6820 trim")):
|
||||
mesh = unreal.EditorAssetLibrary.load_asset(f"/Game/Hollywood/Frontage/{asset_name}")
|
||||
if mesh is None:
|
||||
log(f"MISSING trim {asset_name} — run Tools/ue_import_retail_detail.py")
|
||||
continue
|
||||
# RetailTrim_ rather than Frontage_ so the kit assembler leaves it alone.
|
||||
actor = spawn(unreal.StaticMeshActor, unreal.Vector(0, 0, 0), f"RetailTrim_{asset_name}")
|
||||
if actor:
|
||||
comp = actor.static_mesh_component
|
||||
comp.set_static_mesh(mesh)
|
||||
comp.set_mobility(unreal.ComponentMobility.MOVABLE)
|
||||
# Map the trim's own slots onto the look-dev materials where the surface
|
||||
# type matches, so the walls sit in the same noise/texture family as the
|
||||
# rest of the block. The trim's colour slots (bronze, burgundy, mural,
|
||||
# lettering) stay as authored: those tones came from the photograph.
|
||||
TRIM_LOOK = {"M_Retail_Stucco": "M_Stucco", "M_Retail_White": "M_Stucco",
|
||||
"M_Retail_Glass": "M_FacadeGlass"}
|
||||
mapped = 0
|
||||
for index, slot in enumerate(mesh.get_editor_property("static_materials")):
|
||||
target = TRIM_LOOK.get(str(slot.get_editor_property("material_slot_name")))
|
||||
if target and target in built:
|
||||
comp.set_material(index, built[target])
|
||||
mapped += 1
|
||||
log(f"staged: {label} ({mapped} slot(s) onto look materials)")
|
||||
|
||||
# --- player start: north sidewalk, facing the hero frontage -------------
|
||||
# Signs were measured from the imported meshes rather than assumed: the FBX
|
||||
# preset flips north, so in Unreal the road spans Y -1382..+1047, the NORTH
|
||||
|
||||
@@ -300,6 +300,12 @@ try:
|
||||
# on the north sidewalk looking along the crosswalk itself, which is
|
||||
# the pedestrian's view and shows the zebra bars as bars.
|
||||
("demo-crossing", unreal.Vector(-15250, -2500, 400), unreal.Vector(-14956, 400, 150)),
|
||||
# 0.1.6 retail frontage: 6800 Lee Drug (X -3340..-1417, face Y +1467)
|
||||
# and 6806-6820 (X -6618..-3297, face Y +1252). Stand on the north
|
||||
# sidewalk at 2 m, look south with a level horizon so the parapets and
|
||||
# the cornice line read as horizontals. 68 deg covers both frontages at
|
||||
# this distance; the ends crop rather than the elevation leaning.
|
||||
("demo-retail-frontage", unreal.Vector(-4700, -2600, 210), unreal.Vector(-4700, 1400, 620), 66.0),
|
||||
# the two north-side background buildings (TCL, Tussaud's) seen from
|
||||
# the south across Hollywood Blvd. Probed world-space (2026-11-08):
|
||||
# TCL bbox X -22889..-18442, Y -9431..-1802; Tussaud's X -27071..
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# Import the retail frontage trim (6800 Lee Drug, 6806–6820) into Unreal.
|
||||
#
|
||||
# blender -b --factory-startup --python Tools/retail_frontage_build.py -- \
|
||||
# --out /tmp/gigline/retail --report /tmp/gigline/retail-report.json
|
||||
# <engine>/Engine/Binaries/Linux/UnrealEditor-Cmd <uproject> -run=pythonscript \
|
||||
# -script=<repo>/Tools/ue_import_retail_detail.py -nullrhi -unattended -nopause -stdout
|
||||
#
|
||||
# Two meshes arrive, one per building, each already at its world position in the
|
||||
# project frame (the FBX carries the offset), so staging spawns at the origin.
|
||||
# Destination: /Game/Hollywood/Frontage/<name> — same folder as the block frontage
|
||||
# so the demo builder stages them together.
|
||||
#
|
||||
# Result file: /tmp/gigline/retail-import.txt
|
||||
|
||||
import glob
|
||||
import os
|
||||
|
||||
import unreal
|
||||
|
||||
SRC = "/tmp/gigline/retail"
|
||||
DEST = "/Game/Hollywood/Frontage"
|
||||
RESULT = "/tmp/gigline/retail-import.txt"
|
||||
lines = []
|
||||
|
||||
|
||||
def log(msg):
|
||||
lines.append(str(msg))
|
||||
unreal.log(str(msg))
|
||||
|
||||
|
||||
def import_fbx(path, name):
|
||||
task = unreal.AssetImportTask()
|
||||
task.filename = path
|
||||
task.destination_path = DEST
|
||||
task.destination_name = name
|
||||
task.automated = True
|
||||
task.save = True
|
||||
task.replace_existing = True
|
||||
|
||||
ui = unreal.FbxImportUI()
|
||||
ui.import_mesh = True
|
||||
ui.import_materials = True
|
||||
ui.import_textures = False
|
||||
ui.import_as_skeletal = False
|
||||
smd = ui.static_mesh_import_data
|
||||
smd.import_uniform_scale = 1.0
|
||||
smd.combine_meshes = True
|
||||
smd.convert_scene_unit = False
|
||||
smd.force_front_x_axis = False
|
||||
smd.normal_import_method = unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS
|
||||
task.options = ui
|
||||
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
|
||||
|
||||
mesh = unreal.EditorAssetLibrary.load_asset(f"{DEST}/{name}")
|
||||
if mesh is None:
|
||||
log(f"MISSING after import: {DEST}/{name}")
|
||||
return None
|
||||
|
||||
# Trim is decoration on a solid the player already collides with: no collision
|
||||
# of its own, otherwise the cornice and mullions become snag points.
|
||||
try:
|
||||
body = mesh.get_editor_property("body_setup")
|
||||
body.set_editor_property("collision_trace_flag",
|
||||
unreal.CollisionTraceFlag.CTF_USE_COMPLEX_AS_SIMPLE)
|
||||
mesh.set_editor_property("body_setup", body)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
log(f"{name}: collision flag not set ({exc!r})")
|
||||
|
||||
box = mesh.get_bounding_box()
|
||||
mn, mx = box.min, box.max
|
||||
log(f"{name:22} X {mn.x:8.0f}..{mx.x:8.0f} Y {mn.y:7.0f}..{mx.y:7.0f} "
|
||||
f"Z {mn.z:5.0f}..{mx.z:5.0f} slots {len(mesh.get_editor_property('static_materials'))}")
|
||||
return mesh
|
||||
|
||||
|
||||
def main():
|
||||
files = sorted(glob.glob(os.path.join(SRC, "*.fbx")))
|
||||
if not files:
|
||||
log(f"no FBX in {SRC}; run Tools/retail_frontage_build.py first")
|
||||
return
|
||||
for path in files:
|
||||
import_fbx(path, os.path.splitext(os.path.basename(path))[0])
|
||||
unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True)
|
||||
|
||||
|
||||
try:
|
||||
main()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
import traceback
|
||||
log(f"FAILED: {type(exc).__name__}: {exc}")
|
||||
log(traceback.format_exc())
|
||||
|
||||
with open(RESULT, "w") as f:
|
||||
f.write("\n".join(lines) + "\n")
|
||||
@@ -48,6 +48,18 @@
|
||||
"Art/reference/north-chinese/01-grauman-s-chinese-theatre-by-carol-highsmith.jpg"
|
||||
],
|
||||
"expect": "The two north-side buildings (TCL Chinese Theatre, Madame Tussaud's) seen from across Hollywood Boulevard, full height in frame, daylight"
|
||||
},
|
||||
{
|
||||
"name": "demo-retail-frontage",
|
||||
"shot": "Screenshots/demo/review-demo-retail-frontage.png",
|
||||
"size": [
|
||||
1920,
|
||||
1080
|
||||
],
|
||||
"references": [
|
||||
"Art/reference/retail-neighbours/01-lee-drug-and-neighbor.jpg"
|
||||
],
|
||||
"expect": "The retail run beside the hero from across the street: 6800 Lee Drug's bronze cornice, mural row and stepped parapet next to 6806-6820's pilasters and window storey, daylight"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user