feat(facades): 0.1.5 — hero detail, scene audit, material orphaning fixed #32
No files matched your search
@@ -86,8 +86,13 @@ The hero block and the demo level are built by a fixed sequence. Each step write
|
||||
| North import | `Tools/ue_import_north_buildings.py` | `/Game/Hollywood/North/*` |
|
||||
| Hero import | `Tools/ue_hero_facade_import.py` | `/Game/Hollywood/ElCapitan/SM_HeroFacade` |
|
||||
| Demo map | `Tools/ue_build_demo_map.py` | `/Game/Hollywood/DemoMap` |
|
||||
| Façade kit assembly | `Tools/ue_apply_facade_kit.py` | kit actors in `DemoMap` |
|
||||
| Theatre trim | `Tools/ue_stage_theatre_trim.py` | marquee + blade sign actors |
|
||||
| Scene audit | `Tools/ue_audit_scene.py` | materials, placement spans, labels (gate) |
|
||||
| Review shots | `Tools/ue_build_review_map.py` + `Tools/render_screenshots.sh` | `Screenshots/` |
|
||||
|
||||
**Order matters.** `ue_build_demo_map.py` rebuilds the site materials, so any actor staged by another tool must be (re)staged _after_ it, or it keeps a reference to a material that no longer exists — that is how 604 actors once ended up untethered from their materials while the map only looked slightly flat. `Tools/render_screenshots.sh` runs the full chain in the right order and gates on `ue_audit_scene.py`.
|
||||
|
||||
Every Blender exporter calls [`Tools/fbx_export_preset.py`](../Tools/fbx_export_preset.py) (`ue_metric_v1` / `ue_character_v1`) instead of repeating the FBX argument block — one place for the scale conversion, the axis mapping and `mesh_smooth_type="FACE"` (without which every Unreal import warns about missing smoothing groups and round geometry arrives faceted).
|
||||
|
||||
Two rules that the pipeline enforces rather than documents:
|
||||
|
||||
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.
@@ -487,3 +487,24 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO.
|
||||
twice on a clean run. Always count instances before believing a perf number.
|
||||
- Series committed at `Docs/perf/0.1.5-kit-walk-frametimes.csv`; comparison table in
|
||||
`Docs/release-checks.md`.
|
||||
|
||||
## 2026-11-09 — 604 unmaterialed actors: material rebuild was delete+recreate
|
||||
|
||||
- The "blockout cleaning pass" turned out to need **material and placement hygiene,
|
||||
not more geometry**. `Tools/ue_audit_scene.py` (new) found **604 of 620 mesh actors
|
||||
with no material**: kit pieces and theatre trim all untextured.
|
||||
- **Cause:** `ensure_material` in `ue_build_demo_map.py` deleted and recreated each
|
||||
`/Game/Hollywood/Site/M_*` asset every run ("so a look change cannot be masked by a
|
||||
stale asset"). That orphane every reference: actors staged by other tools pointed at
|
||||
deleted assets and fell back to the default material. The map looked only *slightly*
|
||||
flat — no error, no warning.
|
||||
- **Fix:** update the material **in place** — load it, `MaterialEditingLibrary.
|
||||
delete_all_material_expressions(mat)`, rebuild the graph, recompile, save. Note
|
||||
`Material.expressions` is protected from Python; use the library's deleter.
|
||||
- **Ordering rule now documented and enforced:** the demo builder rebuilds materials,
|
||||
so kit and theatre staging must run *after* it. `render_screenshots.sh` now runs
|
||||
demo map → kit assembly → theatre trim → scene audit → shot cameras → capture, and
|
||||
**fails on a non-zero audit count**.
|
||||
- Second find: 53 duplicated kit labels (both shop piers of a bay shared one name).
|
||||
- Scene audit after the fixes: **0 failures** (all materialed, all spans verified, no
|
||||
duplicate labels). Packaged QA still PASS; spawn-view reference score 0.72.
|
||||
@@ -19,7 +19,7 @@ The `0.1.4` kit, materials and theatre trim are signed off; these are the geomet
|
||||
|
||||
- [A] **Retail neighbours (6822 Budget, 6806–6820, 6800 Lee Drug) from their own references.** **Started 2026-11-09.** Fetched the one usable reference Commons holds for this run — `Art/reference/retail-neighbours/01-lee-drug-and-neighbor.jpg` (Gb321, CC BY-SA 4.0, 1920×953) — which shows 6800 **and** its neighbour at street level; the vision pre-check confirmed shopfronts and parapets are visible. The 18-frame theatre set cannot stand in for these façades, and Commons has nothing else for 6822 or 6806–6820, so per-building references for those two remain the blocker. **Kit layout fixed while here:** bay count was `round(width / 4.96)`, which gave 6922 thirteen bays across 65.8 m (a window every 4.96 m on a 37 m block) and left bare wall under the cornice; bays are now capped at 9 with the module widened, and the storey spread fills the available height (6922 9 × 11, 6806 7 × 3, 6800 4 × 3, theatre 8 × 6).
|
||||
- [x] **Hero façade: fluting and the motto band as geometry.** **Done 2026-11-09.** Fluting: 20 grooves per column (thin shadow-tone cylinders set into the shaft, 120 pieces) instead of boolean cuts — it breaks the flat highlight at a fraction of the poly cost. Motto band: a real recess now, with a moulded surround standing 6 cm proud and the panel set 12 cm behind it, replacing the 8 cm dark stripe. Hero is 271 objects / 5 492 tris (from 148 / 3 044) and still 14.50 m. **En route it exposed a placement bug:** `ue_hero_facade_import.py` carried its own stale hero position `(-12024.5, -1450)` from before the `0.1.0` frontage fix, so running the importer alone moved the hero 13 m east and onto the north side — invisible in a render, and it dropped the reference score from 0.69 to 0.56. Placement now lives in `Tools/ue_placement.py` with span validators, asserted after every import (`span ok: X -14644..-12028`), and the spawn-view score recovered to **0.72**.
|
||||
- [ ] **Blosm blockout cleaning pass.** Roofs and facades remain untextured volumes and the `setupScript` styles stay unused; with the kit carrying the elevations, this is now cleanup rather than a second façade system.
|
||||
- [x] **Blosm blockout cleaning pass.** **Done 2026-11-09 — the cleaning it needed was material and placement hygiene, not more geometry.** Audit found **604 of 620 mesh actors with no material at all**: `ensure_material` deleted and recreated each site material on every run, orphaning every reference held by the 598 kit actors and the theatre trim, while the map looked merely slightly flat. Fixed by updating materials **in place** (`delete_all_material_expressions` then rebuild the graph) instead of delete+create. Also fixed 53 duplicated kit actor labels (both shop piers of a bay shared one name). New `Tools/ue_audit_scene.py` gates the chain on: every mesh actor has a material, every verified frontage span is within ±1 m, no duplicate labels — wired into `render_screenshots.sh`, which now runs the staging chain in the correct order and fails on an audit failure. Scene audit: **0 failures**.
|
||||
- [x] **Re-measure 1440p performance** with 625 kit actors and 8 generated textures in the map. **Done 2026-11-09:** 2.76 ms average (363 fps), p99 3.64 ms at 2560×1440, Epic preset, TSR, VSync off, no frame generation — about **12 %** more frame time than `0.1.0` (2.42 ms / 414 fps) and still ~6× the 60 fps floor. Recorded in `Docs/release-checks.md`; series at `Docs/perf/0.1.5-kit-walk-frametimes.csv`. Note: runs showing ~105 fps were two overlapping game instances, not the build.
|
||||
- [ ] Tag `v0.1.5` and write `CHANGELOG.md` entry; then remove this section.
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side b
|
||||
| `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. |
|
||||
| `generate_facade_textures.py` | Deterministic stdlib PNG generator for the facade maps (`T_StoneAshlar`, `T_Stucco`, `T_Asphalt`, `T_Concrete`, each `_BC` base colour + `_R` roughness). Pure-zlib writer, hash-based value noise, identical bytes on every run; `--size` and `--out` (`/tmp/gigline/textures` by default). `ue_build_demo_map.py` imports them into `/Game/Hollywood/Textures` and samples them world-aligned. |
|
||||
| `ue_audit_scene.py` | Gate on the DemoMap: no unmaterialed actor, every verified frontage span within tolerance (`ue_placement`), no duplicate actor labels. Writes `/tmp/gigline/scene-audit.txt`; `render_screenshots.sh --verify` fails on a non-zero failure count. Catches the class of defect that still renders plausibly. |
|
||||
| `ue_placement.py` | Single source of truth for where staged actors belong in the DemoMap (hero position, verified frontage spans) plus `span_error`/`describe_span_failure` validators. Exists because the hero position was written twice and drifted — the importer kept the pre-`0.1.0` value and moved the hero 13 m east onto the wrong side of the boulevard. No `unreal` import, so it is unit-tested. |
|
||||
| `ue_common.py` | Shared helpers for the Unreal commandlet scripts: `make_writable()` clears the read-only bit an LFS `.umap` checks out with (otherwise `save_map()` returns False silently), and `require_saved()` makes that failure explicit. |
|
||||
| `fbx_export_preset.py` | The **single** FBX export preset (`ue_metric_v1` / `ue_character_v1`): metres→centimetres via `FBX_SCALE_NONE`, axis mapping east +X / north −Y / up +Z, `mesh_smooth_type="FACE"` (smoothing groups survive the import). Every Blender exporter imports it instead of copying the argument block; `preset_kwargs()` is bpy-free and unit-tested. |
|
||||
|
||||
@@ -20,18 +20,29 @@ PROJ="$REPO/Game/GigLine/GigLine.uproject"
|
||||
VERIFY="no"
|
||||
[ "${1:-}" = "--verify" ] && VERIFY="yes"
|
||||
|
||||
echo "== 1/3 stage the demo shot cameras =="
|
||||
cd "$ENGINE"
|
||||
./Engine/Binaries/Linux/UnrealEditor-Cmd "$PROJ" \
|
||||
-run=pythonscript -script="$REPO/Tools/ue_build_review_map.py" \
|
||||
-nullrhi -unattended -nopause -stdout
|
||||
run_cmdlet() {
|
||||
local script="$1" label="$2"
|
||||
echo "== $label =="
|
||||
( cd "$ENGINE" && ./Engine/Binaries/Linux/UnrealEditor-Cmd "$PROJ" \
|
||||
-run=pythonscript -script="$REPO/Tools/$script" \
|
||||
-nullrhi -unattended -nopause -stdout )
|
||||
}
|
||||
|
||||
# Order matters. The demo builder rebuilds the site materials, so any actor staged
|
||||
# by another tool must be (re)staged after it or it keeps a reference to a material
|
||||
# that no longer exists - that is how 604 actors ended up untextured once.
|
||||
run_cmdlet ue_build_demo_map.py "1/5 rebuild the demo map (materials, site, frontage)"
|
||||
run_cmdlet ue_apply_facade_kit.py "2/5 assemble the facade kit"
|
||||
run_cmdlet ue_stage_theatre_trim.py "3/5 stage the theatre trim"
|
||||
run_cmdlet ue_audit_scene.py "4/6 audit the scene (materials, placement, labels)"
|
||||
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 "== 2/3 capture the demo map =="
|
||||
echo "== 6/6 capture the demo map =="
|
||||
./Engine/Binaries/Linux/UnrealEditor-Cmd "$PROJ" /Game/Hollywood/DemoMap \
|
||||
-game -windowed -ResX=1920 -ResY=1080 -nopause -nosplash -stdout
|
||||
|
||||
echo "== 3/3 collect the demo screenshots =="
|
||||
echo "== collect the demo screenshots =="
|
||||
mkdir -p "$REPO/Screenshots/demo"
|
||||
rm -f "$REPO"/Screenshots/demo/review-*.png
|
||||
found=0
|
||||
@@ -47,6 +58,14 @@ done
|
||||
[ "$found" = "1" ] || { echo "ERROR: no demo screenshots found"; exit 1; }
|
||||
ls -1 "$REPO/Screenshots/demo"
|
||||
|
||||
# The scene audit is a gate: a wrong-material or misplaced actor is exactly the
|
||||
# kind of failure that still renders plausibly.
|
||||
if grep -q "failure(s) ==" /tmp/gigline/scene-audit.txt 2>/dev/null; then
|
||||
grep -E "FAIL|failure\(s\)" /tmp/gigline/scene-audit.txt | tail -5
|
||||
grep -q "== 0 failure(s) ==" /tmp/gigline/scene-audit.txt || {
|
||||
echo "ERROR: scene audit failed — see /tmp/gigline/scene-audit.txt"; exit 1; }
|
||||
fi
|
||||
|
||||
if [ "$VERIFY" = "yes" ]; then
|
||||
echo "== verify: does each shot show what it should? =="
|
||||
cd "$REPO"
|
||||
|
||||
@@ -156,11 +156,13 @@ def apply_to_actor(actor, kit, materials=None, prefix=None):
|
||||
for bay in range(bays):
|
||||
cx = mn.x + bay_w * (bay + 0.5)
|
||||
# ground floor: shopfront
|
||||
for piece, dx in (("Kit_ShopPier_L", -bay_w / 2 + 30.0),
|
||||
("Kit_ShopPier_R", bay_w / 2 - 30.0)):
|
||||
for piece, dx, side in (("Kit_ShopPier_L", -bay_w / 2 + 30.0, "L"),
|
||||
("Kit_ShopPier_R", bay_w / 2 - 30.0, "R")):
|
||||
if piece in kit:
|
||||
# The side belongs in the label: both piers of a bay shared one
|
||||
# label, which left 53 ambiguous pairs in the outliner.
|
||||
if _spawn(kit[piece], unreal.Vector(cx + dx, face_y, base_z),
|
||||
f"{prefix}shop_pier_{bay}", kit_material(piece)):
|
||||
f"{prefix}shop_pier_{side}_{bay}", kit_material(piece)):
|
||||
placed += 1
|
||||
if "Kit_ShopGlass" in kit:
|
||||
if _spawn(kit["Kit_ShopGlass"], unreal.Vector(cx, face_y, base_z),
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Audit the DemoMap for staged actors that would render wrong.
|
||||
|
||||
Checks that are cheap and catch the failures this project actually had:
|
||||
|
||||
1. **No unmaterialed actors.** 604 actors once rendered with the default material
|
||||
because the demo builder deleted and recreated the site materials, orphaning
|
||||
every reference; the map looked merely "a bit flat".
|
||||
2. **Placement spans.** Each verified frontage building is probed against
|
||||
`Tools/ue_placement.py`; a building 13 m off its footprint still renders fine,
|
||||
which is exactly why it needs a number, not an eye.
|
||||
3. **No duplicate actor labels.** Two buildings' kit pieces sharing a label made
|
||||
the outliner ambiguous and hid one of them.
|
||||
|
||||
UnrealEditor-Cmd <uproject> -run=pythonscript \
|
||||
-script=Tools/ue_audit_scene.py -nullrhi -unattended -nopause -stdout
|
||||
|
||||
Result file: `/tmp/gigline/scene-audit.txt`. Exit code from the commandlet is
|
||||
always 0, so **read the file** — `visual_loop.py --verify` does not depend on it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import os
|
||||
import sys
|
||||
|
||||
import unreal
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import ue_placement # noqa: E402
|
||||
|
||||
MAP = "/Game/Hollywood/DemoMap"
|
||||
RESULT = "/tmp/gigline/scene-audit.txt"
|
||||
|
||||
lines: list[str] = []
|
||||
failures = 0
|
||||
|
||||
|
||||
def log(message: str) -> None:
|
||||
lines.append(message)
|
||||
print(message)
|
||||
|
||||
|
||||
def fail(message: str) -> None:
|
||||
global failures
|
||||
failures += 1
|
||||
log("FAIL " + message)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not unreal.EditorLevelLibrary.load_level(MAP):
|
||||
log(f"could not load {MAP}")
|
||||
return
|
||||
|
||||
actors = unreal.EditorLevelLibrary.get_all_level_actors()
|
||||
|
||||
# 1. materials
|
||||
unmaterialed = []
|
||||
for actor in actors:
|
||||
comp = actor.get_component_by_class(unreal.StaticMeshComponent)
|
||||
if not comp or not comp.static_mesh:
|
||||
continue
|
||||
if comp.get_material(0) is None:
|
||||
unmaterialed.append(actor.get_actor_label())
|
||||
log(f"mesh actors: {sum(1 for a in actors if a.get_component_by_class(unreal.StaticMeshComponent))}")
|
||||
if unmaterialed:
|
||||
fail(f"{len(unmaterialed)} actor(s) with no material, e.g. {unmaterialed[:6]}")
|
||||
else:
|
||||
log("OK every staged mesh has a material")
|
||||
|
||||
# 2. placement spans
|
||||
for actor in actors:
|
||||
label = actor.get_actor_label()
|
||||
key = None
|
||||
if label.startswith("Frontage_SM_"):
|
||||
name = label[len("Frontage_SM_"):]
|
||||
key = {"Hollywood_6922": "6922", "Hollywood_6902": "6902",
|
||||
"ElCapitanTheatre": "6838", "Budget_6822": "6822",
|
||||
"Hollywood_6806": "6806", "LeeDrug_6800": "6800"}.get(name)
|
||||
elif label == ue_placement.HERO_ACTOR:
|
||||
key = "hero"
|
||||
if not key:
|
||||
continue
|
||||
origin, extent = actor.get_actor_bounds(False)
|
||||
lo, hi = origin.x - extent.x, origin.x + extent.x
|
||||
problem = ue_placement.describe_span_failure(key, lo, hi)
|
||||
if problem:
|
||||
fail(problem)
|
||||
else:
|
||||
log(f"OK {key}: X {lo:.0f}..{hi:.0f}")
|
||||
|
||||
# 3. duplicate labels
|
||||
counts = collections.Counter(a.get_actor_label() for a in actors)
|
||||
duplicates = {k: v for k, v in counts.items() if v > 1}
|
||||
if duplicates:
|
||||
sample = list(duplicates.items())[:6]
|
||||
fail(f"{len(duplicates)} duplicated actor label(s), e.g. {sample}")
|
||||
else:
|
||||
log("OK no duplicate actor labels")
|
||||
|
||||
log(f"== {failures} failure(s) ==")
|
||||
|
||||
|
||||
try:
|
||||
main()
|
||||
except Exception as exc: # noqa: BLE001 - surface in the result file
|
||||
import traceback
|
||||
log(f"FAILED: {type(exc).__name__}: {exc}")
|
||||
log(traceback.format_exc())
|
||||
|
||||
os.makedirs(os.path.dirname(RESULT), exist_ok=True)
|
||||
with open(RESULT, "w", encoding="utf-8") as handle:
|
||||
handle.write("\n".join(lines) + "\n")
|
||||
@@ -118,14 +118,23 @@ def ensure_material(mat_name, spec, textures=None):
|
||||
does not read as one flat specular value.
|
||||
"""
|
||||
path = f"/Game/Hollywood/Site/{mat_name}"
|
||||
if unreal.EditorAssetLibrary.does_asset_exist(path):
|
||||
# Rebuilt every run so a look change cannot be masked by a stale asset.
|
||||
unreal.EditorAssetLibrary.delete_asset(path)
|
||||
|
||||
mat = unreal.AssetToolsHelpers.get_asset_tools().create_asset(
|
||||
mat_name, "/Game/Hollywood/Site", unreal.Material, unreal.MaterialFactoryNew())
|
||||
lib = unreal.MaterialEditingLibrary
|
||||
|
||||
# Update the existing material IN PLACE rather than deleting and recreating it.
|
||||
# Deleting the asset to force a look rebuild silently orphaned every reference:
|
||||
# after one demo-builder run, all 598 kit actors and the theatre trim pointed at
|
||||
# deleted materials and rendered with the default one (604 actors with no
|
||||
# material), while the map itself looked merely "a bit flat".
|
||||
mat = unreal.EditorAssetLibrary.load_asset(path)
|
||||
if mat is None:
|
||||
mat = unreal.AssetToolsHelpers.get_asset_tools().create_asset(
|
||||
mat_name, "/Game/Hollywood/Site", unreal.Material, unreal.MaterialFactoryNew())
|
||||
else:
|
||||
# Clear the old graph so a removed node cannot survive a look change.
|
||||
# `Material.expressions` is protected from Python, so use the editing
|
||||
# library's own deleter rather than reading the array.
|
||||
lib.delete_all_material_expressions(mat)
|
||||
|
||||
def node(cls):
|
||||
return lib.create_material_expression(mat, cls)
|
||||
|
||||
|
||||
Reference in new issue
Block a user