3 Commits
Author SHA1 Message Date
zwitschi c53386fcf4 docs(review): embed every screenshot inline so the review happens in the file
Each shot now has its own section with the image embedded, its view, expected
content and both verdicts, plus a status table at the top that links to them.
A path alone was not a review: the owner had to open five PNGs to judge a pass.

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

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

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

Known gaps recorded in Screenshots/README.md, not hidden: the parapet reads as a
blank band, murals and lettering do not resolve at review distance, the tower is a
plain cylinder, and the review camera crops the east end.
2026-11-09 09:10:49 +01:00
zwitschi 4e703fcfd9 todo: claim 0.1.6 retail frontage on branch facade-retail-0.1.6 2026-11-09 08:59:07 +01:00
57 changed files with 772 additions and 67 deletions

No files matched your search

+2 -2
View File
@@ -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** (`![alt](demo/<shot>.png)`) 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`.
+64
View File
@@ -0,0 +1,64 @@
# Reference analysis — retail frontage, 0.1.6
Phase 0 of the `references` pack (reference-image-match). Written before any geometry, because the pass that produced the hero façade skipped it and cost two rebuilds.
## Source
| | |
| --- | --- |
| Image | `Art/reference/retail-neighbours/01-lee-drug-and-neighbor.jpg` (1920×953) |
| Author / licence | Gb321, own work, **CC BY-SA 4.0** — attribution required |
| Captured | 2024-07-19 11:11 local |
| Description | "Lee Drug and 6806 Hollywood Buildings in Hollywood" |
| Coverage | Both target buildings in one frame, mid-morning sun from the south-east |
**One photograph is the whole evidence base for 6800 and 6806–6820.** It is a street-level oblique, not an elevation: verticals converge, the ground floor of 6806–6820 is partly behind a parked car, and the Lee Drug shopfront is behind a bus. Everything below that the photo does not resolve is marked _assumed_ and is carried into the part report.
**6822 Budget has no photograph.** It is a 6.0 m × 5.5 m single-storey sliver west of 6806–6820, and the TODO item already records that Commons has nothing usable. It stays a blockout extrusion and the item is deferred rather than guessed.
## Measured (not from the photo)
From `Geo/raw/osm-api-map-2026-11-06.osm` and the blockout, in the shared frame (metres, X east, Y north, Z from the project datum):
| Building | OSM way | X | Front face Y | Height | Depth |
| --- | --- | --- | --- | --- | --- |
| 6800 Lee Drug | 424024149 | −33.20 … −14.37 | −14.63 | 12.10 m | 33.5 m |
| 6806–6820 | 424024150 | −66.08 … −33.07 | −12.52 | 12.10 m | 30.8 m |
| 6822 Budget | 424024144 | −78.54 … −72.52 | −12.92 | 5.50 m | 15.4 m |
Both target buildings are 12.10 m tall, which makes the _relative_ proportions in the photo directly usable: the parapet line, the cornice and the shopfront line can be read as fractions of the frame height.
## 6800 Lee Drug — reading
| Category | Observation |
| --- | --- |
| Subject | Corner retail block at Hollywood × Highland; the street face and the corner return are both visible |
| Silhouette | Long low box, broken by a **rounded corner tower** that rises above the frontage parapet; the parapet is **stepped**, not flat |
| Storeys | Two zones, not storeys: a tall shopfront zone and a single tall display wall above it |
| Proportion (vertical) | Shopfront ≈ 0 → 0.36 H; bronze cornice ≈ 0.36 → 0.43 H; mural + lettering wall ≈ 0.43 → 0.82 H; stepped parapet ≈ 0.82 → 1.0 H (_assumed split; read off the frame, ±0.03 H_) |
| Proportion (horizontal) | Corner tower occupies ≈ 4 m of the east end; eight mural panels at roughly 2.2 m centres along the frontage |
| Colour | Cream stucco wall `#EDE6D8`, bronze/brown cornice `#6B4A2F`, near-black mural plates `#1C1A18`, dark glazing `#20262A` (_sampled from the frame_) |
| Unique features | Stepped Deco parapet with 4 horizontal reveal lines; projecting bronze cornice; row of black celebrity-portrait panels; large dark "SOUVENIRS OF …" lettering on the upper wall; shopfront glazing with slim mullions |
| Mood | 1930s Deco retail, freshly painted, bright midday |
## 6806–6820 — reading
| Category | Observation |
| --- | --- |
| Silhouette | Flat-fronted two-storey block; parapet steps up in fins at the west end |
| Proportion (vertical) | Shopfront ≈ 0 → 0.32 H; burgundy fascia band ≈ 0.32 → 0.38 H; window storey ≈ 0.38 → 0.62 H; blank wall + parapet ≈ 0.62 → 1.0 H (_assumed_) |
| Horizontal | Brown pilaster strips divide the frontage into roughly 3 m bays; seven upper windows across 33 m |
| Colour | Same cream stucco; brown pilasters `#6B4A2F`; **burgundy-green** fascia `#5B3A3A`; white window frames |
| Unique features | Banded parapet with 3 reveal lines; white-framed upper windows with security bars; shopfront glazing behind a burgundy fascia |
## Camera match (for the review shot)
The photo is a street-level oblique from the **north** side looking south-east, subject filling ~80 % of the frame width. The demo review shot must reproduce that: camera at 1.7 m on the north sidewalk, yaw toward the south frontage, 35–42° lens, both buildings in frame. A wide block view will not compare usefully against this crop.
## Assumptions to carry into the model report
1. Vertical proportions, as above — the photo's convergence makes a pixel-perfect read impossible.
2. Mural panel size and spacing; the lettering is modelled as a recessed plate, not as text.
3. Shopfront bay spacing on both buildings; the bus hides most of Lee Drug's ground floor.
4. Parapet reveal line count (4 on Lee Drug, 3 on 6806–6820).
5. Corner tower radius; only the outer curve is visible.
+4 -2
View File
@@ -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 |
+4 -2
View File
@@ -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 |
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.
+8
View File
@@ -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.
+78 -10
View File
@@ -12,18 +12,86 @@ 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 | ☐ pending |
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
![Spawn view: the whole south block from the north sidewalk](demo/review-demo-spawn-view.png)
- **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
![Street level: the hero colonnade at eye height](demo/review-demo-street-level.png)
- **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
![Crossing: the designated mid-block crossing with zebra bars](demo/review-demo-crossing.png)
- **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
![North buildings: TCL Chinese Theatre and Madame Tussaud's from the south](demo/review-demo-north-buildings.png)
- **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
![Retail frontage: 6800 Lee Drug and 6806–6820 from across the street](demo/review-demo-retail-frontage.png)
- **File:** `demo/review-demo-retail-frontage.png`
- **View:** north sidewalk, level horizon, 68° lens
- **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:** ☐ pending
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 reads as a blank white band.** It is modelled as one 2.18 m wall with four 0.10 m reveal strips, and at review distance the strips disappear into the wash.
- **The mural row and lettering band barely read.** They are shallow plates (0.06–0.08 m) on the same plane as the wall; they need either depth or a decal before they read as the photographs' black-framed panels.
- **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,7 +108,7 @@ 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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -2
View File
@@ -13,9 +13,10 @@ Actionable task list for this project. This file is the single, central place fo
Suggested blender skills: **references** (reference-image-match), **archviz**, **historical-worlds** for the 1920s–1990s retail mix on this run.
### Patch tasks
### Patch tasks - active - session-5598ce7e-27e3-4c64-bb49-c04a4a31ee58 (branch `facade-retail-0.1.6`)
- [ ] **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.
- [A] **Model 6800 Lee Drug and 6806–6820 from their own reference.** First pass 2026-11-09 on branch `facade-retail-0.1.6`: `Tools/retail_frontage_build.py` builds both façades from the one photograph (`Art/reference/retail-neighbours/`, Gb321 CC BY-SA 4.0) with the analysis in `Art/blender/retail/ref-analysis.md`; `Tools/ue_import_retail_detail.py` imports them and the demo builder stages them as `RetailTrim_*`. The generic façade kit is now skipped on these two, so their walls carry one façade language instead of two. **Open on this item:** the parapet reads as a blank band, the mural row and lettering band do not resolve at review distance, the corner tower is a plain cylinder, and the review camera crops the east end — all listed in `Screenshots/README.md`.
- [D] **Model 6822 Budget.** Deferred: no photograph exists for it (Commons has nothing usable) and the single-storey 6.0 × 5.5 m volume cannot be read from the neighbour shot. Needs photography before it can be modelled honestly; do not substitute the El Capitan Theatre's set or the 6806–6820 shot.
- [ ] 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.
+3
View File
@@ -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. |
+27
View File
@@ -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)]
+4 -1
View File
@@ -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 =="
+329
View File
@@ -0,0 +1,329 @@
#!/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.45 * B6_H, 0.60 * B6_H)
B6_PARAPET_Z = 0.62 * B6_H # 7.50 m
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"])
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.
z0, z1 = LD_MURAL_Z
for i, bx in enumerate(bay_centres(LD_X0, LD_X1, margin=1.2), start=1):
panel = cube(f"LD_Mural_{i}", bx, LD_FACE_Y - 0.04, (z0 + z1) / 2.0,
BAY - 0.75, 0.08, z1 - z0, mural)
parts.append((panel, "photographs: row of black-framed portrait panels; count and "
"size assumed from the visible row"))
# Lettering band: a recessed plate, not text. Reproducing the lettering needs a
# decal, which is a materials-pass job.
lz0, lz1 = LD_LETTER_Z
letter = cube("LD_LetterBand", cx, LD_FACE_Y - 0.03, (lz0 + lz1) / 2.0,
width - 3.0, 0.06, lz1 - lz0, mural)
parts.append((letter, "photographs: large dark lettering across the upper wall; "
"modelled as a recessed plate, the text itself is a decal job"))
# Stepped Deco parapet: wall band plus four horizontal reveals.
para = cube("LD_Parapet", cx, LD_FACE_Y + 0.10, (LD_PARAPET_Z + LD_H) / 2.0,
width + 0.2, 0.30, LD_H - LD_PARAPET_Z, stucco)
parts.append((para, "photographs: parapet steps above the frontage wall; "
"thickness assumed"))
for i in range(4):
z = LD_PARAPET_Z + (LD_H - LD_PARAPET_Z) * (i + 1) / 5.0
groove = cube(f"LD_ParapetReveal_{i + 1}", cx, LD_FACE_Y - 0.03, z,
width + 0.2, 0.07, 0.10, mural)
parts.append((groove, "photographs: four 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"))
# 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"])
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.06, (wz0 + wz1) / 2.0,
1.70, 0.14, 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.02, (wz0 + wz1) / 2.0,
1.46, 0.08, wz1 - wz0 - 0.24, glass)
parts.append((pane, "photographs: dark glazing inside each frame"))
para = cube("B6_Parapet", cx, B6_FACE_Y + 0.08, (B6_PARAPET_Z + B6_H) / 2.0,
width + 0.2, 0.26, B6_H - B6_PARAPET_Z, stucco)
parts.append((para, "photographs: parapet band above the window storey; thickness assumed"))
for i in range(3):
z = B6_PARAPET_Z + (B6_H - B6_PARAPET_Z) * (i + 1) / 4.0
groove = cube(f"B6_ParapetReveal_{i + 1}", cx, B6_FACE_Y - 0.03, z,
width + 0.2, 0.07, 0.09, bronze)
parts.append((groove, "photographs: three 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),
}
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())
+32
View File
@@ -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()
+10 -2
View File
@@ -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)
+19
View File
@@ -597,6 +597,25 @@ 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)
log(f"staged: {label}")
# --- 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
+6
View File
@@ -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, -2000, 200), unreal.Vector(-4700, 1400, 560), 68.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..
+94
View File
@@ -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")
+12
View File
@@ -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"
}
]
}