diff --git a/Art/blender/site/site-2026-11-06.blend b/Art/blender/site/site-2026-11-06.blend
index 83aa233..cb34036 100644
--- a/Art/blender/site/site-2026-11-06.blend
+++ b/Art/blender/site/site-2026-11-06.blend
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7d6d303913de1a2c24b3e04ceb6c43a84a46355470b14ebab6acf146f7dbb636
-size 991800
+oid sha256:2a54ae747e4990399c8f269b8ebe5a3f1bf0d6cca4ac2dd9ca2562f66f5eedfb
+size 994709
diff --git a/Art/blender/street/street-2026-11-06.blend b/Art/blender/street/street-2026-11-06.blend
index 71028dc..ae518a9 100644
--- a/Art/blender/street/street-2026-11-06.blend
+++ b/Art/blender/street/street-2026-11-06.blend
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b6f546fcf86cd08a5e37571a7f42b2e6615925f71f7ca5ed850e801313591f70
-size 514043
+oid sha256:5744a2138e894a485f026d92b9311f7ecde12c280dd096bbbde5776694b32ae2
+size 514185
diff --git a/Art/blender/street/street_mesh.py b/Art/blender/street/street_mesh.py
index 201b616..368a18c 100644
--- a/Art/blender/street/street_mesh.py
+++ b/Art/blender/street/street_mesh.py
@@ -261,20 +261,24 @@ def main():
paint = bpy.data.materials.new("Road_Paint_White")
paint.diffuse_color = (0.85, 0.85, 0.85, 1.0)
- def crossing_band(s_c):
+ # All three bands live in ONE object. Creating one object per crossing used
+ # to produce Crossing_Markings, .001 and .002; site assembly appends by the
+ # exact name, so only the first crossing ever reached the game.
+ band_verts, band_faces = [], []
+
+ def add_crossing_band(s_c):
# Stripes are emitted as two triangles each: the road crown makes the
# quad non-planar and the FBX SDK refuses to triangulate it on import
# ("unable to triangulate mesh" — the mesh is omitted entirely).
zone = 3.0 # band length along the road, m
w, g = 0.5, 0.5 # stripe width / gap, m
span = (SOUTH_GUTTER + 0.2, NORTH_GUTTER - 0.2)
- verts, faces = [], []
n = int(zone // (w + g))
covered = n * w + (n - 1) * g # actual stripe coverage, centred on s_c
for k in range(n):
s0 = s_c - covered / 2 + k * (w + g)
- corners = []
- base_idx = len(verts) # verts grows only after the stripe is sampled
+ base_idx = len(band_verts) # sampled positions, then vertices
+ pts = []
for s_edge in (s0, s0 + w):
base, normal, _ = at(s_edge)
for off in span:
@@ -284,23 +288,23 @@ def main():
# interpolates the DEM along 1 m rows while the markings sample
# only their own corners, so a 1 cm lift left parts of the
# stripes sunk under the road and invisible in game.
- corners.append((base_idx + len(corners), (x, y, (dem_z(x, y) or 0.0) + 0.06)))
- # corners order: (s0,off_s), (s0,off_n), (s1,off_s), (s1,off_n)
- idx = [c[0] for c in corners]
- verts.extend(c[1] for c in corners)
- faces.append((idx[0], idx[3], idx[1]))
- faces.append((idx[0], idx[2], idx[3]))
- mesh = bpy.data.meshes.new("Crossing_Markings")
- mesh.from_pydata(verts, [], faces)
- mesh.update()
- mesh.materials.append(paint)
- obj = bpy.data.objects.new("Crossing_Markings", mesh)
- bpy.context.scene.collection.objects.link(obj)
- return obj
+ pts.append((x, y, (dem_z(x, y) or 0.0) + 0.06))
+ # pts order: (s0,off_s), (s0,off_n), (s1,off_s), (s1,off_n)
+ band_verts.extend(pts)
+ band_faces.append((base_idx + 0, base_idx + 3, base_idx + 1))
+ band_faces.append((base_idx + 0, base_idx + 2, base_idx + 3))
for s_c in CROSSINGS:
- band = crossing_band(s_c)
- print(f"crossing band at s={s_c:.1f}: {len(band.data.polygons)} stripes")
+ add_crossing_band(s_c)
+
+ mesh = bpy.data.meshes.new("Crossing_Markings")
+ mesh.from_pydata(band_verts, [], band_faces)
+ mesh.update()
+ mesh.materials.append(paint)
+ markings = bpy.data.objects.new("Crossing_Markings", mesh)
+ bpy.context.scene.collection.objects.link(markings)
+ print(f"crossing markings: {len(CROSSINGS)} bands, "
+ f"{len(markings.data.vertices)} verts, {len(markings.data.polygons)} faces")
for obj in (road, sw_s, sw_n):
print(f"{obj.name:16} verts {len(obj.data.vertices):5} faces {len(obj.data.polygons):5}")
diff --git a/Art/blender/terrain/terrain-2026-11-06.blend b/Art/blender/terrain/terrain-2026-11-06.blend
index c294b9b..fd74a93 100644
--- a/Art/blender/terrain/terrain-2026-11-06.blend
+++ b/Art/blender/terrain/terrain-2026-11-06.blend
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9707f1256667ce156f8104cf84c461a1ee074a2a62c932fd377c9bb39e8f41d8
-size 431130
+oid sha256:bf33bfa11b78194c3dce1f061aaf904af4d82d7294f291772073e8d7d31ac016
+size 433475
diff --git a/Art/blender/terrain/terrain_from_dem.py b/Art/blender/terrain/terrain_from_dem.py
index aa007cf..1ecfde7 100644
--- a/Art/blender/terrain/terrain_from_dem.py
+++ b/Art/blender/terrain/terrain_from_dem.py
@@ -224,6 +224,12 @@ def main():
mesh = bpy.data.meshes.new("Terrain_DEM")
mesh.from_pydata(verts, [], faces)
mesh.update()
+ # Without a material the mesh imports into Unreal with the default white
+ # material and blows out under the demo sun. A muted earth tone keeps the
+ # ground reading as ground.
+ ground = bpy.data.materials.new("Ground_Earth")
+ ground.diffuse_color = (0.30, 0.28, 0.25, 1.0)
+ mesh.materials.append(ground)
obj = bpy.data.objects.new("Terrain_DEM", mesh)
bpy.context.scene.collection.objects.link(obj)
diff --git a/Game/GigLine/Content/Hollywood/DemoMap.umap b/Game/GigLine/Content/Hollywood/DemoMap.umap
index 97dc8de..adfeb64 100644
--- a/Game/GigLine/Content/Hollywood/DemoMap.umap
+++ b/Game/GigLine/Content/Hollywood/DemoMap.umap
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2a2b7a61aa8b3c86c3944849107aa00d3ab6fa0d35056d50b02a65acbd4854a1
-size 33816
+oid sha256:3f3925238751bd1cfd9b145a7bc3a12945392434b7ea3f345bc822116f4f5a51
+size 44553
diff --git a/Game/GigLine/Content/Hollywood/Site/Crossing_Markings.uasset b/Game/GigLine/Content/Hollywood/Site/Crossing_Markings.uasset
index b85f6c7..4c3e2c1 100644
--- a/Game/GigLine/Content/Hollywood/Site/Crossing_Markings.uasset
+++ b/Game/GigLine/Content/Hollywood/Site/Crossing_Markings.uasset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:27a75622ca7ea90976cd88677961ca6d195b61e965a0ff17d6c8909b012918af
-size 74929
+oid sha256:70c729bb152a8bbd7cfe2f457e0d3fa1b1bac455d696bb27d23644aecff7cecd
+size 75501
diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_North.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_North.uasset
index 10a7d05..88cb645 100644
--- a/Game/GigLine/Content/Hollywood/Site/Kerb_North.uasset
+++ b/Game/GigLine/Content/Hollywood/Site/Kerb_North.uasset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ddadef2aaa688c7525f87d4498949ea64bf9f0e463bc0118bd86a3989a2f5acd
+oid sha256:7893216176a7977d4784234bf10e6b49a219ab31dd61497f8db7958c5a04b7ce
size 83550
diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_South.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_South.uasset
index 3a001b0..7f5ec1b 100644
--- a/Game/GigLine/Content/Hollywood/Site/Kerb_South.uasset
+++ b/Game/GigLine/Content/Hollywood/Site/Kerb_South.uasset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8e13d22b4ae7ed978c6755adfd5b8c02d98157177976f792794c083b95797270
+oid sha256:fd5493759b160992ebec5073e59d67a6ea8ceeb1fbb35b0624223668934cebc3
size 83797
diff --git a/Game/GigLine/Content/Hollywood/Site/M_Asphalt.uasset b/Game/GigLine/Content/Hollywood/Site/M_Asphalt.uasset
new file mode 100644
index 0000000..bb6d30d
--- /dev/null
+++ b/Game/GigLine/Content/Hollywood/Site/M_Asphalt.uasset
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fc0d523f0fb587306a9e946e27b10113eb89df1218e6fdb8553b29f083384e49
+size 4605
diff --git a/Game/GigLine/Content/Hollywood/Site/M_Concrete.uasset b/Game/GigLine/Content/Hollywood/Site/M_Concrete.uasset
new file mode 100644
index 0000000..fa49e20
--- /dev/null
+++ b/Game/GigLine/Content/Hollywood/Site/M_Concrete.uasset
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c765035f6c076c4016831884b167fa93e162bab7d0038f55cd09d240105dfa4c
+size 4610
diff --git a/Game/GigLine/Content/Hollywood/Site/M_Ground.uasset b/Game/GigLine/Content/Hollywood/Site/M_Ground.uasset
new file mode 100644
index 0000000..5948d47
--- /dev/null
+++ b/Game/GigLine/Content/Hollywood/Site/M_Ground.uasset
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:21454ed393d70dc103e0ad7ca00b06aed6fded381062ddcde87a424c05ff580d
+size 4600
diff --git a/Game/GigLine/Content/Hollywood/Site/M_RoadPaint.uasset b/Game/GigLine/Content/Hollywood/Site/M_RoadPaint.uasset
new file mode 100644
index 0000000..7aa4d2f
--- /dev/null
+++ b/Game/GigLine/Content/Hollywood/Site/M_RoadPaint.uasset
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:891ee60d2feac3e741509e9e03756e9941b4561994e1a77732f565b7aae5bb52
+size 4615
diff --git a/Game/GigLine/Content/Hollywood/Site/Road_Hollywood.uasset b/Game/GigLine/Content/Hollywood/Site/Road_Hollywood.uasset
index 3809334..908fab3 100644
--- a/Game/GigLine/Content/Hollywood/Site/Road_Hollywood.uasset
+++ b/Game/GigLine/Content/Hollywood/Site/Road_Hollywood.uasset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:20165ed97f13fb5fba8391ca8735d9cf965eb97d676282d549afafb1a274b0ca
+oid sha256:5b2c133f3dc163f937cc7f73de9ee35a31659451e43171abdaa1b1fc5b357e1a
size 309810
diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North.uasset
index 0ad1590..1416015 100644
--- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North.uasset
+++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North.uasset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e32a1f9590b943a40f05efbd531ec4e1a6c6933c579a5dec4a3dca8753e408c0
+oid sha256:a3fa880ce24eff71415cf8039b9e2347181087c2fa42ac217b022a55e1cc68a8
size 150676
diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South.uasset
index 5b0956e..9b7eae9 100644
--- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South.uasset
+++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South.uasset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d66130107a5d5d996d8367cc1cbb55c7ef506a91df419f0c9ad5ee74d652b915
+oid sha256:bf71bb2cd8feebb44a102f9a4f44179a5d26d7fb125cf5fbec31d5084acbd9c4
size 164347
diff --git a/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset b/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset
index bd1a338..7d7ac7d 100644
--- a/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset
+++ b/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6d5d3a6fb6646ebeef426b7c07a58b872060779e6ffe0c8b073939ee93299ab8
-size 333138
+oid sha256:aa5407698175d4ca2a8e0ee7e3bdde23654337445260c9b314a62667d8edcdc1
+size 335528
diff --git a/Game/GigLine/Content/Test/ReviewMap.umap b/Game/GigLine/Content/Test/ReviewMap.umap
index 8db2af7..1f0d7e8 100644
--- a/Game/GigLine/Content/Test/ReviewMap.umap
+++ b/Game/GigLine/Content/Test/ReviewMap.umap
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4d399b3e1b28616e58f445fca8a7de82e9bf1df7c4308500473fc0c63d6d597d
+oid sha256:18fafef3a73b195fdbc21ff1c6e9a8d55f691ae3626e2f588d64871554297b31
size 28613
diff --git a/MEMORY.md b/MEMORY.md
index 0e5e9b1..e19febc 100644
--- a/MEMORY.md
+++ b/MEMORY.md
@@ -174,3 +174,12 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO.
- **`EditorLevelLibrary.load_level`/`new_level` are deprecated no-ops in `-nullrhi` commandlets** (they return without switching worlds, so staged actors land in whatever world is current and get saved over the wrong map). Use the `LevelEditorSubsystem`: `unreal.get_editor_subsystem(unreal.LevelEditorSubsystem).load_level(path)` actually switches. Symptom was ReviewMap containing Demo actors + `ShotSequencer`.
- **Map-save ordering bug in `Tools/ue_build_review_map.py`**: the script saved ReviewMap *after* the DemoMap section, but the switch back to ReviewMap fails silently because the map only exists in memory at that point — so the final `save_map` wrote the demo world over ReviewMap. Fix: save ReviewMap first, then stage the demo cameras, and never switch back. Both maps verified by probe (ReviewMap lists Review actors; DemoMap lists Demo actors).
- Rebuilt ReviewMap after the fixes: character, both cubes (A grounded, B floating at 2 m), pawn, NPC prefab, hero facade all present; user re-approved the shots. README cube naming corrected (README had A/B swapped vs the script).
+
+## 2026-11-07 — Materials and lighting in the demo map
+
+- **Blender flat diffuse colours do not survive FBX -> Unreal.** Every site mesh (road, sidewalks, kerbs, crossing paint) imported with a material asset that had a default *white* base colour, and the terrain imported with `WorldGridMaterial` (UE's bright grey grid). That — not the sun — is what made the demo look blown out and washed white. Fix: create the colours in UE (`M_Asphalt`, `M_Concrete`, `M_Ground`, `M_RoadPaint` via `Constant3Vector`) and assign them per actor in `Tools/ue_build_demo_map.py`.
+- **Auto-exposure makes review shots non-comparable**: the same road rendered washed-out in one framing and near-black in another because each camera saw a different amount of sky. Fix: an unbound `PostProcessVolume` with `AEM_HISTOGRAM` and `auto_exposure_min_brightness == auto_exposure_max_brightness` (fixed exposure). Do not use `AEM_MANUAL` + `auto_exposure_bias` — a bias of 11 blew the whole frame out.
+- **`DirectionalLightComponent.light_color` takes a `Color` struct, not `LinearColor`** (`unreal.Color(r=255,g=245,b=230,a=255)`); passing `LinearColor` raises a nativize error.
+- **One band per object loses geometry downstream.** `crossing_band()` created `Crossing_Markings` each call, so Blender renamed the second and third to `.001`/`.002`; `site_assembly.py` appends by exact name, so only one crossing ever reached the game. Build all bands into a single mesh instead of one object per band.
+- **Street arc length -> UE X:** s=0 is the west end of the block (UE X −27966 cm) and s increases eastward, so the designated mid-block crossing at s=130.1 m is UE X ≈ −14956, not the −24291 used by an earlier camera (that is the s=36.6 crossing).
+- **Camera placement in the demo map needs the mesh bounds, not intuition.** The hero façade body spans UE Y −1457..−200, so a "street level" camera at Y −400 is *inside* the building and renders its unlit back face. Read `get_bounding_box()` on the staged meshes before choosing a camera.
diff --git a/Screenshots/README.md b/Screenshots/README.md
index a022080..c2e6285 100644
--- a/Screenshots/README.md
+++ b/Screenshots/README.md
@@ -22,8 +22,14 @@ and exits — about a minute. The camera list lives in
- **Floor**: 160 m square, top face at Z 0. The character measures Z 0..83 cm.
- **Cubes**: `Cube_ScaleTest_A` rests on the floor, `Cube_ScaleTest_B` floats at
Z 2 m; both are exactly 1 m — the scale reference.
-- **Lighting**: one warm key light plus sky fill (review rig, not the `0.1.0`
- daytime look). Beyond the floor is open: the horizon is a black band.
+- **Lighting (review map)**: one warm key light plus sky fill, neutral exposure.
+- **Lighting (demo map)**: fixed daytime — sun at pitch -50 / yaw 120 (intensity
+ 4, warm), sky light 2.6, sky atmosphere, height fog, and a manual-exposure
+ post-process volume (min = max brightness 1.0) so a shot cannot wash out or
+ crush to black depending on how much sky it frames. Surface colours are
+ assigned explicitly in `Tools/ue_build_demo_map.py` (`M_Asphalt`, `M_Concrete`,
+ `M_Ground`, `M_RoadPaint`) because Blender's flat diffuse colours do not
+ survive the FBX → Unreal import.
## Review-map shots
@@ -53,10 +59,10 @@ Rendered from `/Game/Hollywood/DemoMap`, the walkable level. Click an image for
| File | Preview | Approval | What it shows |
| --- | --- | --- | --- |
-| `demo/review-demo-spawn-view.png` | [
](demo/review-demo-spawn-view.png) | ☑ | Spawn view: north sidewalk, the hero façade across the road |
-| `demo/review-demo-street-level.png` | [
](demo/review-demo-street-level.png) | ☑ | Street level: portals, flights, banners, colonnade |
-| `demo/review-demo-crossing.png` | [
](demo/review-demo-crossing.png) | ☑ | The designated mid-block crossing (s=130 m): zebra markings on the road, kerbs flushed to ramps. Camera on the north sidewalk looking south-west. Note: the road renders near-black in shadow — the fixed-daytime-lighting item, not the markings |
-| `demo/review-demo-north-buildings.png` | [
](demo/review-demo-north-buildings.png) | ☑ | North-side background buildings (TCL, Tussaud's) across the Blvd |
+| `demo/review-demo-spawn-view.png` | [
](demo/review-demo-spawn-view.png) | needs review | Spawn view from the north sidewalk: the hero façade across the road under fixed daytime light |
+| `demo/review-demo-street-level.png` | [
](demo/review-demo-street-level.png) | needs review | Street level from the north sidewalk: the façade colonnade and columns at eye height, now sunlit with cast shadows (was backlit and near-black) |
+| `demo/review-demo-crossing.png` | [
](demo/review-demo-crossing.png) | needs review | The designated mid-block crossing at s=130 m (UE X -14956): three continental zebra bars on the dark asphalt, kerbs flushed to ramps, camera on the north sidewalk looking along the crosswalk |
+| `demo/review-demo-north-buildings.png` | [
](demo/review-demo-north-buildings.png) | needs review | North-side background buildings (TCL, Tussaud's) across the Blvd, lit against the sky |
## What to look for
diff --git a/Screenshots/demo/review-demo-crossing.png b/Screenshots/demo/review-demo-crossing.png
index 041f7ed..f9523d8 100644
--- a/Screenshots/demo/review-demo-crossing.png
+++ b/Screenshots/demo/review-demo-crossing.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b6bd66678c132a8fa09004f2f2e3d160b1c99122e1d1365932d7e31ed0933199
-size 1628670
+oid sha256:90b3f9e5aa44b2bd5f446c0532e1ecc7947e005188e909748f1d6520abf1648d
+size 1568849
diff --git a/Screenshots/demo/review-demo-north-buildings.png b/Screenshots/demo/review-demo-north-buildings.png
index 137a99a..4ffe045 100644
--- a/Screenshots/demo/review-demo-north-buildings.png
+++ b/Screenshots/demo/review-demo-north-buildings.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5eaa60227e5f6e307e1bf878b33d3f52634e0121dcaa6af73afd7fff5728499b
-size 2304425
+oid sha256:1b3eb0d7831dcd3fe83cf2e0b65dc6b8f20b21ebf3d03ae538ad60565f5a5037
+size 1390196
diff --git a/Screenshots/demo/review-demo-spawn-view.png b/Screenshots/demo/review-demo-spawn-view.png
index 9132d32..9635de1 100644
--- a/Screenshots/demo/review-demo-spawn-view.png
+++ b/Screenshots/demo/review-demo-spawn-view.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a312b5ee86cd5c9fef8b686d2aae429199f88caae7276d6fb714d1d73fed996a
-size 2571043
+oid sha256:a9106c7c915119e4df4bedc22404028ed060a71e309ff1662931890b55469644
+size 1573928
diff --git a/Screenshots/demo/review-demo-street-level.png b/Screenshots/demo/review-demo-street-level.png
index 86a9834..22d99fc 100644
--- a/Screenshots/demo/review-demo-street-level.png
+++ b/Screenshots/demo/review-demo-street-level.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2675eb353573003961c8d81ba24c2bbbe1f1052c11c4ee38cd786cab7115b686
-size 1975605
+oid sha256:fd5e854bfce63b51d573817bd86ca897b06cbf231b02c0302f7e99b1bb7c6870
+size 1783060
diff --git a/Screenshots/review-character.png b/Screenshots/review-character.png
index b550695..7aed33b 100644
--- a/Screenshots/review-character.png
+++ b/Screenshots/review-character.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ea60ed98f688bab482160386d851b39a10868c64bf1371b3628427cc25a3de97
-size 2498425
+oid sha256:f314dcc5211758e70c0fde67ece08eaeef863cc27596eefb15fd706ed20d6ce8
+size 2476490
diff --git a/Screenshots/review-ground-contact.png b/Screenshots/review-ground-contact.png
index 2c74a02..04fba20 100644
--- a/Screenshots/review-ground-contact.png
+++ b/Screenshots/review-ground-contact.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:03b47847338f6d840e1e2dd370fce585c599cd72db0cf1393b82cc91964619c0
-size 1778455
+oid sha256:ef80c5c6eb63b79e97ba41d8a79793b2215b71c854966c093904016aa3eb47da
+size 1793304
diff --git a/Screenshots/review-hero-facade-front.png b/Screenshots/review-hero-facade-front.png
index 564b65c..24fbb72 100644
--- a/Screenshots/review-hero-facade-front.png
+++ b/Screenshots/review-hero-facade-front.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fabcc154b139f35e0c6910f87a042c6cc51b9e263993cbfb49a15fa3817736d3
-size 2052242
+oid sha256:6a6b384911d4f07d1e165545ee6c78338869df8a8d30231d1194e7b89a6349df
+size 2051886
diff --git a/Screenshots/review-hero-facade-oblique.png b/Screenshots/review-hero-facade-oblique.png
index 07d778c..1da0b73 100644
--- a/Screenshots/review-hero-facade-oblique.png
+++ b/Screenshots/review-hero-facade-oblique.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:20a85e17ab952b5f8de52556bbe9bf7b4dfb03c7e430a371eafa625b0874b4d7
-size 1917582
+oid sha256:9d47147cd5b8c4959ce61da5b7ea72c6bf4dab21bebfd9a2426809f873e72938
+size 1932092
diff --git a/Screenshots/review-hero-portals.png b/Screenshots/review-hero-portals.png
index 69194a1..87dcc4a 100644
--- a/Screenshots/review-hero-portals.png
+++ b/Screenshots/review-hero-portals.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8fa065612a714476d37f5cc4bb28182b30dc9b10cfcfd37cc04257d177b41f33
-size 1774919
+oid sha256:ce2496abd2848ae5d73c17e4eb7c2ccb9bd39437f0f47ee1f2e81b75a64974c6
+size 1778660
diff --git a/Screenshots/review-npc-prefab.png b/Screenshots/review-npc-prefab.png
index b10ff0f..3003951 100644
--- a/Screenshots/review-npc-prefab.png
+++ b/Screenshots/review-npc-prefab.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c1522a746881f69e8fb2e327610ad9fd7d98e673c200ea8d781daab1f23317cc
-size 1721860
+oid sha256:8de11af89103a6ab42531adbcab2985e29f239e0a2122972d1308d1ca98edba3
+size 1721002
diff --git a/Screenshots/review-overview.png b/Screenshots/review-overview.png
index d518bc4..76dcd69 100644
--- a/Screenshots/review-overview.png
+++ b/Screenshots/review-overview.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:78b318299a7633fb267c961ecfd61f5fe7a0a3278d28103b634d21f47bc01c49
-size 2580045
+oid sha256:50c31d72e9149efbd61e4dd454f2671342162a1a3aa58140c1427d523e2091a5
+size 2608909
diff --git a/Screenshots/review-player-placement.png b/Screenshots/review-player-placement.png
index f39f1df..d86b002 100644
--- a/Screenshots/review-player-placement.png
+++ b/Screenshots/review-player-placement.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:605fc1ee8ef167a57d119ef83e741ab1e6d8fbba923bfe5aa162f0dda1069830
-size 1969709
+oid sha256:4c21d2f5f92612cca6963b3217ae9f2ebf45c94837a09b6f0e8398622a9bc040
+size 1964636
diff --git a/Screenshots/review-scale-cubes.png b/Screenshots/review-scale-cubes.png
index 8fc47eb..9ca844c 100644
--- a/Screenshots/review-scale-cubes.png
+++ b/Screenshots/review-scale-cubes.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:26c64adad9f64789c85882a224d402ae4e8ca20eb73736731925972275fc560d
-size 1906660
+oid sha256:daad42906dd5ee802051d09ca813a293a4b73927dd68adc876fc6612ce8ef951
+size 1902016
diff --git a/Screenshots/review-shadow-compare.png b/Screenshots/review-shadow-compare.png
index 2d975fa..f5555bc 100644
--- a/Screenshots/review-shadow-compare.png
+++ b/Screenshots/review-shadow-compare.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:bab0f21cc8a6c90889e0003510b7df68f6f23d16e69e9ba08faec40deef77c8b
-size 1735259
+oid sha256:79123a8f37fd95b617decda35469f7c629f89bbe5f64c063f8b087b27f2615ce
+size 1731317
diff --git a/TODO.md b/TODO.md
index e5d3374..7061a9d 100644
--- a/TODO.md
+++ b/TODO.md
@@ -95,7 +95,7 @@ The MPFB character asset is planned as the NPC template (see `ROADMAP.md`). Keep
- [~] Collision, reset, and pause functionality. Reset (**R**) and pause (**Escape**, a real game pause that releases the cursor) are implemented in `AGigLineCharacter` with `/Game/Input/IA_Reset` and `IA_Pause`. Collision now exists — terrain, road and sidewalks import as complex-as-simple, so a simple hull cannot ramp over the kerbs. **Not verified by hand**: walk, reset and pause need a human at the keyboard. No pause menu widget yet, only the pause itself.
- [x] Build the walkable demo level: terrain, street, and hero facade assembled into one map with a start point on the north sidewalk, so the demo is something a player can walk rather than a review staging. **Built 2026-11-07** by `Tools/ue_build_demo_map.py` → `/Game/Hollywood/DemoMap`: sun/sky, `Hero_Facade`, six site meshes imported with collision (`Terrain_DEM`, `Road_Hollywood`, both sidewalks and both kerbs), and a `Demo_PlayerStart` on the north sidewalk 20 m back from the frontage. Demo views in `Screenshots/demo/` re-rendered and ☑ approved 2026-11-07.
- [x] Walkable sidewalks and a designated street crossing. Crossing chosen: mid-block signalised crossing, OSM way 385648319, at 130 m along the block (`Geo/verification/2026-11-06-street-profile.md`). Done 2026-11-07: continental zebra markings (0.5 m bars / 0.5 m gaps, 3 m band) at all three signalised crossings, no collision on the paint; kerbs were already flushed at the crossings. Enablers found on the way: the street surfaces were 100 % down-facing (invisible in engine) — winding flipped; the terrain buried the street — 10.7 m corridor cut around the centreline (sidewalks cover the transition, walk-check 0 holes); LFS-lockable `.blend`/`.umap` check out read-only — builders now chmod before saving and fail loudly. Screenshot ☑ approved 2026-11-07 (`Screenshots/demo/review-demo-crossing`); the corridor renders near-black in shadow — that is the still-open **Fixed daytime lighting** item, not the crossing.
-- [-] Fixed daytime lighting and basic ambience. Current state: `Demo_Sun` at pitch −35, yaw 200 puts the street corridor in shadow — the crossing shot renders near-black. Plan: rotate sun yaw so light comes down the street axis (e/w), raise sun intensity, boost skylight fill, set fixed daytime (no time-of-day motion).
+- [-] Fixed daytime lighting and basic ambience. **Implemented 2026-11-07**, pending screenshot approval. Demo map now has a fixed daytime rig in `Tools/ue_build_demo_map.py`: sun pitch −50 / yaw 120 (intensity 4, warm 255/245/230, atmosphere sun light), sky light 2.6 with real-time capture, sky atmosphere, height fog (density 0.008), and a manual-exposure post-process volume (min = max brightness 1.0) so a shot cannot wash out or crush to black. Two root causes found while doing it: (a) **Blender flat diffuse colours do not survive the FBX→UE import** — every site mesh arrived white and the terrain arrived with `WorldGridMaterial`, which is what actually made the scene look blown out; explicit `M_Asphalt` / `M_Concrete` / `M_Ground` / `M_RoadPaint` materials are now created and assigned in the demo builder. (b) only **one of the three crossings** was reaching the game: each band was its own object and Blender renamed the duplicates `.001`/`.002`, which site assembly skips; all bands are now one `Crossing_Markings` object. Demo cameras also fixed: street-level sat inside the façade body, and the crossing camera aimed at the wrong crossing (s=36.6 instead of the designated s=130.1 at UE X −14956).
- [ ] Geographic attribution display ("© OpenStreetMap contributors" + license link).
### Repository publication
diff --git a/Tools/ue_build_demo_map.py b/Tools/ue_build_demo_map.py
index a79107d..cbbde45 100644
--- a/Tools/ue_build_demo_map.py
+++ b/Tools/ue_build_demo_map.py
@@ -50,6 +50,29 @@ def spawn(cls, loc, label, rot=None):
return actor
+def ensure_material(asset_name, rgb):
+ """Create/reuse a flat-colour material under /Game/Hollywood/Site.
+
+ Used where the FBX material round-trip does not survive import: the terrain
+ arrives with WorldGridMaterial (UE's bright default grid), which reads as a
+ blown-out white field under the demo sun.
+ """
+ path = f"/Game/Hollywood/Site/{asset_name}"
+ if unreal.EditorAssetLibrary.does_asset_exist(path):
+ return unreal.EditorAssetLibrary.load_asset(path)
+ mat = unreal.AssetToolsHelpers.get_asset_tools().create_asset(
+ asset_name, "/Game/Hollywood/Site", unreal.Material, unreal.MaterialFactoryNew())
+ node = unreal.MaterialEditingLibrary.create_material_expression(
+ mat, unreal.MaterialExpressionConstant3Vector)
+ node.set_editor_property("constant", unreal.LinearColor(rgb[0], rgb[1], rgb[2], 1.0))
+ unreal.MaterialEditingLibrary.connect_material_property(
+ node, "", unreal.MaterialProperty.MP_BASE_COLOR)
+ unreal.MaterialEditingLibrary.recompile_material(mat)
+ unreal.EditorAssetLibrary.save_loaded_asset(mat)
+ log(f"created material {asset_name} rgb={rgb}")
+ return mat
+
+
def clear(prefixes=()):
removed = 0
for actor in unreal.EditorLevelLibrary.get_all_level_actors():
@@ -74,12 +97,87 @@ def build():
removed = clear(("Demo_", "PlayerStart", "Hero_Facade"))
log(f"cleared {removed} previous demo actors")
- # --- lighting ---------------------------------------------------------
- spawn(unreal.DirectionalLight, unreal.Vector(0, 0, 2000), "Demo_Sun",
- unreal.Rotator(pitch=-35.0, yaw=200.0, roll=0.0))
- spawn(unreal.SkyLight, unreal.Vector(0, 0, 2000), "Demo_SkyLight")
+ # --- lighting: fixed daytime ------------------------------------------
+ # The demo is a fixed daytime scene, not a time-of-day simulation. Three
+ # things matter and each was wrong before:
+ #
+ # 1. Sun angle. The hero facade front faces -Y and the road faces +Z. A sun
+ # at yaw 120 / pitch -50 shines toward (-X, +Y, down), so it lights both
+ # the facade front and the carriageway instead of raking across them.
+ # 2. Exposure. Auto-exposure made the same road washed-out in one shot and
+ # near-black in the next, because each camera framed a different amount
+ # of sky. A manual-exposure post-process volume pins it.
+ # 3. Ambience. A height fog gives the street depth and hides the terrain
+ # edge that otherwise reads as a black band at the horizon.
+ sun = spawn(unreal.DirectionalLight, unreal.Vector(0, 0, 2000), "Demo_Sun",
+ unreal.Rotator(pitch=-50.0, yaw=120.0, roll=0.0))
+ if sun:
+ comp = sun.get_component_by_class(unreal.DirectionalLightComponent)
+ if comp:
+ for prop, value in (
+ ("intensity", 4.0),
+ ("light_color", unreal.Color(r=255, g=245, b=230, a=255)),
+ ("atmosphere_sun_light", True),
+ ):
+ try:
+ comp.set_editor_property(prop, value)
+ except Exception as exc: # noqa: BLE001
+ log(f"sun.{prop} not set: {exc!r}")
+ log("staged: Demo_Sun (pitch -50, yaw 120, intensity 4, warm)")
+
+ sky = spawn(unreal.SkyLight, unreal.Vector(0, 0, 2000), "Demo_SkyLight")
+ if sky:
+ comp = sky.get_component_by_class(unreal.SkyLightComponent)
+ if comp:
+ for prop, value in (("intensity", 2.6), ("real_time_capture", True)):
+ try:
+ comp.set_editor_property(prop, value)
+ except Exception as exc: # noqa: BLE001
+ log(f"sky.{prop} not set: {exc!r}")
+ log("staged: Demo_SkyLight (intensity 2.6, real-time capture)")
+
spawn(unreal.SkyAtmosphere, unreal.Vector(0, 0, 0), "Demo_Sky")
- log("staged: sun, sky light, sky atmosphere")
+ log("staged: Demo_Sky (sky atmosphere)")
+
+ fog = spawn(unreal.ExponentialHeightFog, unreal.Vector(0, 0, 0), "Demo_Fog")
+ if fog:
+ comp = fog.get_component_by_class(unreal.ExponentialHeightFogComponent)
+ if comp:
+ for prop, value in (
+ ("fog_density", 0.008),
+ ("fog_height_falloff", 0.15),
+ ("start_distance", 3000.0),
+ ):
+ try:
+ comp.set_editor_property(prop, value)
+ except Exception as exc: # noqa: BLE001
+ log(f"fog.{prop} not set: {exc!r}")
+ log("staged: Demo_Fog (height fog, density 0.008)")
+
+ # Fixed exposure: without this the same road renders washed out or black
+ # depending on how much sky the camera frames. Pinning min == max brightness
+ # freezes the exposure at one value (the classic fixed-exposure trick);
+ # AEM_MANUAL with a bias was tried first and blew the frame out.
+ ppv = spawn(unreal.PostProcessVolume, unreal.Vector(0, 0, 0), "Demo_PostProcess")
+ if ppv:
+ ppv.set_editor_property("unbound", True)
+ settings = ppv.get_editor_property("settings")
+ for prop, value in (
+ ("override_auto_exposure_method", True),
+ ("auto_exposure_method", unreal.AutoExposureMethod.AEM_HISTOGRAM),
+ ("override_auto_exposure_min_brightness", True),
+ ("auto_exposure_min_brightness", 1.0),
+ ("override_auto_exposure_max_brightness", True),
+ ("auto_exposure_max_brightness", 1.0),
+ ("override_auto_exposure_bias", True),
+ ("auto_exposure_bias", 0.0),
+ ):
+ try:
+ settings.set_editor_property(prop, value)
+ except Exception as exc: # noqa: BLE001
+ log(f"pp.{prop} not set: {exc!r}")
+ ppv.set_editor_property("settings", settings)
+ log("staged: Demo_PostProcess (fixed exposure, min=max brightness 1.0)")
# --- hero facade on the measured building line ------------------------
hero = unreal.EditorAssetLibrary.load_asset("/Game/Hollywood/ElCapitan/SM_HeroFacade")
@@ -93,9 +191,21 @@ def build():
log("MISSING: SM_HeroFacade — run Tools/ue_hero_facade_import.py")
# --- site meshes, if they have been imported --------------------------
+ # Blender's flat diffuse colours do not survive FBX -> Unreal: Interchange
+ # builds a material with a default white base colour, and the terrain even
+ # lands with WorldGridMaterial. Every site surface therefore rendered white,
+ # which is what washed out the demo. Assign explicit colours per mesh here.
+ SURFACE_MATERIALS = {
+ "Terrain_DEM": ("M_Ground", (0.30, 0.28, 0.25)),
+ "Road_Hollywood": ("M_Asphalt", (0.055, 0.055, 0.06)),
+ "Sidewalk_South": ("M_Concrete", (0.52, 0.50, 0.47)),
+ "Sidewalk_North": ("M_Concrete", (0.52, 0.50, 0.47)),
+ "Kerb_South": ("M_Concrete", (0.52, 0.50, 0.47)),
+ "Kerb_North": ("M_Concrete", (0.52, 0.50, 0.47)),
+ "Crossing_Markings": ("M_RoadPaint", (0.80, 0.80, 0.78)),
+ }
imported = 0
- for name in ("Terrain_DEM", "Road_Hollywood", "Sidewalk_South", "Sidewalk_North",
- "Kerb_South", "Kerb_North", "Crossing_Markings"):
+ for name, (mat_name, rgb) in SURFACE_MATERIALS.items():
mesh = unreal.EditorAssetLibrary.load_asset(f"/Game/Hollywood/Site/{name}")
if mesh is None:
continue
@@ -103,8 +213,11 @@ def build():
if actor:
actor.static_mesh_component.set_static_mesh(mesh)
actor.static_mesh_component.set_mobility(unreal.ComponentMobility.STATIC)
+ mat = ensure_material(mat_name, rgb)
+ if mat:
+ actor.static_mesh_component.set_material(0, mat)
imported += 1
- log(f"staged {imported} site meshes from /Game/Hollywood/Site")
+ log(f"staged {imported} site meshes with explicit surface materials")
# --- player start: north sidewalk, facing the hero frontage -------------
# Signs were measured from the imported meshes rather than assumed: the FBX
diff --git a/Tools/ue_build_review_map.py b/Tools/ue_build_review_map.py
index bd48a94..edd3ce9 100644
--- a/Tools/ue_build_review_map.py
+++ b/Tools/ue_build_review_map.py
@@ -258,8 +258,17 @@ try:
# 14 m back so the 14.5 m elevation fits: at 2 m the camera sees only
# the colonnade's underside.
("demo-spawn-view", unreal.Vector(-10722.5, -3500, 165), unreal.Vector(-10722.5, -1450, 700)),
- ("demo-street-level", unreal.Vector(-11000, -400, 165), unreal.Vector(-10722.5, -1450, 500)),
- ("demo-crossing", unreal.Vector(-23750, -1800, 480), unreal.Vector(-24291, -1400, 300)),
+ # Street level, from the north sidewalk: the old camera sat at Y -400,
+ # inside the facade body (which spans Y -1457..-200), so it looked at
+ # the unlit back face. Y -2600 is on the sidewalk in front of the lit
+ # elevation.
+ ("demo-street-level", unreal.Vector(-10850, -2600, 170), unreal.Vector(-10722.5, -1450, 400)),
+ # The designated mid-block crossing. Street arc length runs from the
+ # west end (s=0 at X -27966 cm), so s=130.1 m is UE X -14956, not the
+ # -24291 the old camera used (that is the s=36.6 crossing). Camera sits
+ # 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)),
# the two north-side background buildings (TCL, Tussaud's) seen from
# the south sidewalk, across Hollywood Blvd: their frontage and mass.
("demo-north-buildings", unreal.Vector(-22800, -1000, 400), unreal.Vector(-22800, 3000, 300)),