lighting: fixed daytime rig, real surface materials, dynamic lights

Completes "Fixed daytime lighting and basic ambience" for 0.1.0. All 14
screenshots approved by the owner.

Lighting rig (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)
- Unbound post-process volume with fixed exposure (min == max brightness) so a
  shot cannot wash out or crush to black depending on how much sky it frames
- Dynamic-only: r.AllowStaticLighting=False plus MOVABLE lights and mesh
  components, so the editor's "LIGHTING NEEDS TO BE REBUILT" warning no longer
  fires and nothing needs baking

Bugs found and fixed on the way:
- Blender diffuse colours do not survive the FBX -> Unreal import: every site
  material arrived white and the terrain arrived with WorldGridMaterial, which
  is what actually made the scene look blown out. The demo builder now creates
  and assigns M_Asphalt, M_Concrete, M_Ground and M_RoadPaint.
- Only one of the three crossings reached the game: each zebra band was its own
  object, so Blender renamed the duplicates .001/.002 and exact-name site
  assembly skipped them. All bands are now one Crossing_Markings mesh.
- Demo cameras: street-level sat inside the facade body (rendering its unlit
  back face) and the crossing camera aimed at the s=36.6 crossing instead of
  the designated s=130.1 (UE X -14956).
- The terrain had no material and imported as a bright default grid.
- Cleared a committed Android File Server SecurityToken (plugin disabled and
  shipping-excluded; the value must stay empty).

Docs:
- Screenshots/README.md: one consolidated table for all 14 shots with map,
  camera view and description; all approved.
- MEMORY.md: records the material-import, exposure, light_color struct,
  band-naming, camera-bounds and lighting-build traps.
- TODO.md: lighting item marked done.

Squash of branch gameplay-lighting-0.1.0 (PR #3).
This commit is contained in:
zwitschi committed 2026-11-07 18:22:19 +01:00
1 parent 1dbe737b85
commit accf3a5306
38 files changed
+318 -126

No files matched your search

Binary file not shown.
Binary file not shown.
+23 -19
View File
@@ -261,20 +261,24 @@ def main():
paint = bpy.data.materials.new("Road_Paint_White") paint = bpy.data.materials.new("Road_Paint_White")
paint.diffuse_color = (0.85, 0.85, 0.85, 1.0) 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 # 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 # quad non-planar and the FBX SDK refuses to triangulate it on import
# ("unable to triangulate mesh" — the mesh is omitted entirely). # ("unable to triangulate mesh" — the mesh is omitted entirely).
zone = 3.0 # band length along the road, m zone = 3.0 # band length along the road, m
w, g = 0.5, 0.5 # stripe width / gap, m w, g = 0.5, 0.5 # stripe width / gap, m
span = (SOUTH_GUTTER + 0.2, NORTH_GUTTER - 0.2) span = (SOUTH_GUTTER + 0.2, NORTH_GUTTER - 0.2)
verts, faces = [], []
n = int(zone // (w + g)) n = int(zone // (w + g))
covered = n * w + (n - 1) * g # actual stripe coverage, centred on s_c covered = n * w + (n - 1) * g # actual stripe coverage, centred on s_c
for k in range(n): for k in range(n):
s0 = s_c - covered / 2 + k * (w + g) s0 = s_c - covered / 2 + k * (w + g)
corners = [] base_idx = len(band_verts) # sampled positions, then vertices
base_idx = len(verts) # verts grows only after the stripe is sampled pts = []
for s_edge in (s0, s0 + w): for s_edge in (s0, s0 + w):
base, normal, _ = at(s_edge) base, normal, _ = at(s_edge)
for off in span: for off in span:
@@ -284,23 +288,23 @@ def main():
# interpolates the DEM along 1 m rows while the markings sample # interpolates the DEM along 1 m rows while the markings sample
# only their own corners, so a 1 cm lift left parts of the # only their own corners, so a 1 cm lift left parts of the
# stripes sunk under the road and invisible in game. # 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))) pts.append((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) # pts order: (s0,off_s), (s0,off_n), (s1,off_s), (s1,off_n)
idx = [c[0] for c in corners] band_verts.extend(pts)
verts.extend(c[1] for c in corners) band_faces.append((base_idx + 0, base_idx + 3, base_idx + 1))
faces.append((idx[0], idx[3], idx[1])) band_faces.append((base_idx + 0, base_idx + 2, base_idx + 3))
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
for s_c in CROSSINGS: for s_c in CROSSINGS:
band = crossing_band(s_c) add_crossing_band(s_c)
print(f"crossing band at s={s_c:.1f}: {len(band.data.polygons)} stripes")
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): 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}") print(f"{obj.name:16} verts {len(obj.data.vertices):5} faces {len(obj.data.polygons):5}")
Binary file not shown.
+6
View File
@@ -224,6 +224,12 @@ def main():
mesh = bpy.data.meshes.new("Terrain_DEM") mesh = bpy.data.meshes.new("Terrain_DEM")
mesh.from_pydata(verts, [], faces) mesh.from_pydata(verts, [], faces)
mesh.update() 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) obj = bpy.data.objects.new("Terrain_DEM", mesh)
bpy.context.scene.collection.objects.link(obj) bpy.context.scene.collection.objects.link(obj)
+9 -2
View File
@@ -2,7 +2,12 @@
GlobalDefaultGameMode="/Script/GigLine.GigLineGameModeBase" GlobalDefaultGameMode="/Script/GigLine.GigLineGameModeBase"
[/Script/Engine.RendererSettings] [/Script/Engine.RendererSettings]
; Leave defaults for 0.1.0; quality preset is documented at release time. ; Dynamic-only lighting. With static lighting allowed, the stationary sun and
; sky light demanded a baked lighting build and the editor warned
; "LIGHTING NEEDS TO BE REBUILT" (Build Lighting Only / BuildLighting) on every
; map. The demo is a fixed daytime scene lit entirely dynamically, so disable
; static lighting outright — nothing to build, no warning.
r.AllowStaticLighting=False
[URL] [URL]
GameName=GigLine GameName=GigLine
@@ -10,7 +15,9 @@ GameName=GigLine
[/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings] [/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings]
bEnablePlugin=False bEnablePlugin=False
bAllowNetworkConnection=False bAllowNetworkConnection=False
SecurityToken=01A1B5CDAFDE7AAEB5DFBB48880D25DE ; SecurityToken intentionally empty: it is a credential, and the plugin is
; disabled and excluded from shipping. Never commit a real token here.
SecurityToken=
bIncludeInShipping=False bIncludeInShipping=False
bAllowExternalStartInShipping=False bAllowExternalStartInShipping=False
bCompileAFSProject=False bCompileAFSProject=False
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.
+11
View File
@@ -174,3 +174,14 @@ 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`. - **`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). - **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). - 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.
- **"LIGHTING NEEDS TO BE REBUILT" (Build Lighting Only / `BuildLighting`) appears whenever a map has Stationary/Static lights and no baked lightmap.** For a fully dynamic scene the clean fix is `r.AllowStaticLighting=False` in `Config/DefaultEngine.ini` plus `set_mobility(ComponentMobility.MOVABLE)` on the directional light, sky light and height fog (and MOVABLE on the staged mesh components). Nothing to build, warning gone. Note the cvar is read at startup, so each new commandlet/game run picks it up.
- The Android File Server `SecurityToken` kept reappearing in `Config/DefaultEngine.ini`; the plugin is disabled and excluded from shipping, so the value is cleared and must stay empty — it is a credential and must not be committed.
+36 -37
View File
@@ -17,46 +17,45 @@ camera list via `AGigLineShotActor`, captures each shot, copies the PNGs here
and exits — about a minute. The camera list lives in and exits — about a minute. The camera list lives in
`Tools/ue_build_review_map.py`; add new shots there, remove retired ones here. `Tools/ue_build_review_map.py`; add new shots there, remove retired ones here.
> Naming: shots use their stable camera name (no index prefix), so adding or
> reordering cameras never renumbers files or orphans the old ones. Click an
> image for the full-size PNG.
## Shot list
All ☑ approved 2026-11-07.
| # | File | Preview | Map | View | What it shows |
| --- | --- | --- | --- | --- | --- |
| 1 | `review-overview.png` | [<img src="review-overview.png" alt="overview" width="180">](review-overview.png) | Review | 7 m up, NE | Whole review layout: character + both cubes on the floor |
| 2 | `review-character.png` | [<img src="review-character.png" alt="character" width="180">](review-character.png) | Review | eye height, south | MPFB mannequin, idle pose + warm skin; feet meet the plane |
| 3 | `review-scale-cubes.png` | [<img src="review-scale-cubes.png" alt="scale cubes" width="180">](review-scale-cubes.png) | Review | south | Floating vs grounded cube: cast vs contact shadow |
| 4 | `review-player-placement.png` | [<img src="review-player-placement.png" alt="player placement" width="180">](review-player-placement.png) | Review | spawn | The `AGigLineCharacter` pawn, capsule height and landing |
| 5 | `review-npc-prefab.png` | [<img src="review-npc-prefab.png" alt="npc prefab" width="180">](review-npc-prefab.png) | Review | mid, west | The `AGigLineNPCPerson` prefab: collision capsule + visible MPFB body, feet on Z 0 |
| 6 | `review-ground-contact.png` | [<img src="review-ground-contact.png" alt="ground contact" width="180">](review-ground-contact.png) | Review | knee height, feet | Floating/sinking character is obvious here |
| 7 | `review-shadow-compare.png` | [<img src="review-shadow-compare.png" alt="shadow compare" width="180">](review-shadow-compare.png) | Review | low angle | Detached (floating) vs touching (grounded) shadows |
| 8 | `review-hero-portals.png` | [<img src="review-hero-portals.png" alt="hero portals" width="180">](review-hero-portals.png) | Review | 8 m, eye height | Three genuine 1.2 m portal recesses with flights and plinth |
| 9 | `review-hero-facade-front.png` | [<img src="review-hero-facade-front.png" alt="hero facade front" width="180">](review-hero-facade-front.png) | Review | road side, straight-on | Hero façade elevation and column rhythm |
| 10 | `review-hero-facade-oblique.png` | [<img src="review-hero-facade-oblique.png" alt="hero facade oblique" width="180">](review-hero-facade-oblique.png) | Review | north-west | Body depth and column line standing proud of the mass |
| 11 | `demo/review-demo-spawn-view.png` | [<img src="demo/review-demo-spawn-view.png" alt="spawn view" width="180">](demo/review-demo-spawn-view.png) | Demo | spawn point | Spawn view from the north sidewalk: the hero façade across the road under fixed daytime light |
| 12 | `demo/review-demo-street-level.png` | [<img src="demo/review-demo-street-level.png" alt="street level" width="180">](demo/review-demo-street-level.png) | Demo | eye height, north side | The façade colonnade and columns, sunlit with cast shadows |
| 13 | `demo/review-demo-crossing.png` | [<img src="demo/review-demo-crossing.png" alt="crossing" width="180">](demo/review-demo-crossing.png) | Demo | along the crosswalk | The designated mid-block crossing at s=130 m (UE X -14956): three continental zebra bars on the asphalt, kerbs flushed to ramps |
| 14 | `demo/review-demo-north-buildings.png` | [<img src="demo/review-demo-north-buildings.png" alt="north buildings" width="180">](demo/review-demo-north-buildings.png) | Demo | across the Blvd | North-side background buildings (TCL, Tussaud's), lit against the sky |
## Scene conventions ## Scene conventions
- **Floor**: 160 m square, top face at Z 0. The character measures Z 0..83 cm. - **Review map** (`/Game/Test/ReviewMap`): 160 m floor, top face at Z 0; the
character measures Z 0..83 cm. One warm key light plus sky fill.
- **Cubes**: `Cube_ScaleTest_A` rests on the floor, `Cube_ScaleTest_B` floats at - **Cubes**: `Cube_ScaleTest_A` rests on the floor, `Cube_ScaleTest_B` floats at
Z 2 m; both are exactly 1 m — the scale reference. 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` - **Demo map** (`/Game/Hollywood/DemoMap`): fixed daytime — sun at pitch −50 /
daytime look). Beyond the floor is open: the horizon is a black band. yaw 120 (intensity 4, warm), sky light 2.6, sky atmosphere, height fog, fixed
exposure. Surface colours are assigned explicitly in
## Review-map shots `Tools/ue_build_demo_map.py` (`M_Asphalt`, `M_Concrete`, `M_Ground`,
`M_RoadPaint`) because Blender's flat diffuse colours do not survive the
All ☑ approved 2026-11-07 (re-rendered with the character and cubes restored; FBX → Unreal import.
user visual check passed). - **Lighting is dynamic-only** (`r.AllowStaticLighting=False`, all lights
Movable), so no lighting build is ever required.
> Naming: shots use their stable `Tools/ue_build_review_map.py` camera name (no
> index prefix), so adding or reordering cameras never renumbers files or
> orphans the old ones. Click an image for the full-size PNG.
| File | Preview | View | What it shows |
| --- | --- | --- | --- |
| `review-overview.png` | [<img src="review-overview.png" alt="overview" width="200">](review-overview.png) | 7 m up, NE | Whole review layout: character + both cubes on the floor |
| `review-character.png` | [<img src="review-character.png" alt="character" width="200">](review-character.png) | eye height, south | MPFB mannequin, idle pose + warm skin; feet meet the plane |
| `review-scale-cubes.png` | [<img src="review-scale-cubes.png" alt="scale cubes" width="200">](review-scale-cubes.png) | south | Floating vs grounded cube: cast vs contact shadow |
| `review-player-placement.png` | [<img src="review-player-placement.png" alt="player placement" width="200">](review-player-placement.png) | spawn | The `AGigLineCharacter` pawn, capsule height and landing |
| `review-npc-prefab.png` | [<img src="review-npc-prefab.png" alt="npc prefab" width="200">](review-npc-prefab.png) | mid, west | The `AGigLineNPCPerson` prefab: collision capsule + visible MPFB body, feet on Z 0 (**new 2026-11-07**) |
| `review-ground-contact.png` | [<img src="review-ground-contact.png" alt="ground contact" width="200">](review-ground-contact.png) | knee height, feet | Floating/sinking character is obvious here |
| `review-shadow-compare.png` | [<img src="review-shadow-compare.png" alt="shadow compare" width="200">](review-shadow-compare.png) | low angle | Detached (floating) vs touching (grounded) shadows |
| `review-hero-portals.png` | [<img src="review-hero-portals.png" alt="hero portals" width="200">](review-hero-portals.png) | 8 m, eye height | Three genuine 1.2 m portal recesses with flights and plinth |
| `review-hero-facade-front.png` | [<img src="review-hero-facade-front.png" alt="hero facade front" width="200">](review-hero-facade-front.png) | road side, straight-on | Hero façade elevation and column rhythm |
| `review-hero-facade-oblique.png` | [<img src="review-hero-facade-oblique.png" alt="hero facade oblique" width="200">](review-hero-facade-oblique.png) | north-west | Body depth and column line standing proud of the mass |
## Demo-map views
Rendered from `/Game/Hollywood/DemoMap`, the walkable level. Click an image for the full-size PNG.
| File | Preview | Approval | What it shows |
| --- | --- | --- | --- |
| `demo/review-demo-spawn-view.png` | [<img src="demo/review-demo-spawn-view.png" alt="spawn view" width="200">](demo/review-demo-spawn-view.png) | ☑ | Spawn view: north sidewalk, the hero façade across the road |
| `demo/review-demo-street-level.png` | [<img src="demo/review-demo-street-level.png" alt="street level" width="200">](demo/review-demo-street-level.png) | ☑ | Street level: portals, flights, banners, colonnade |
| `demo/review-demo-crossing.png` | [<img src="demo/review-demo-crossing.png" alt="crossing" width="200">](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` | [<img src="demo/review-demo-north-buildings.png" alt="north buildings" width="200">](demo/review-demo-north-buildings.png) | ☑ | North-side background buildings (TCL, Tussaud's) across the Blvd |
## What to look for ## What to look for
@@ -73,4 +72,4 @@ Rendered from `/Game/Hollywood/DemoMap`, the walkable level. Click an image for
- Banners and window panels float detached from the colonnade wall (next - Banners and window panels float detached from the colonnade wall (next
modelling pass). modelling pass).
- Between kerb and building line, the walkable surface is the terrain mesh, not - Between kerb and building line, the walkable surface is the terrain mesh, not
a modelled pavement. a modelled pavement.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+4 -4
View File
@@ -89,13 +89,13 @@ The MPFB character asset is planned as the NPC template (see `ROADMAP.md`). Keep
- [-] Build sidewalks, roads, and collision meshes. Terrain, road, sidewalks and kerbs exist (`Art/blender/terrain/`, `street/`) and are assembled in `site/site-2026-11-06.blend`; collision and details (crossing markings, kerb radii, aprons) are not built. Ground must follow the DEM: 3.94 m fall along the block, ~2 % crown between the gutters at ±10–11 m from the centreline, north sidewalk higher than south. Kerb height is an unresolved assumption — the DEM does not resolve it (`Geo/verification/2026-11-06-cross-sections.md`). - [-] Build sidewalks, roads, and collision meshes. Terrain, road, sidewalks and kerbs exist (`Art/blender/terrain/`, `street/`) and are assembled in `site/site-2026-11-06.blend`; collision and details (crossing markings, kerb radii, aprons) are not built. Ground must follow the DEM: 3.94 m fall along the block, ~2 % crown between the gutters at ±10–11 m from the centreline, north sidewalk higher than south. Kerb height is an unresolved assumption — the DEM does not resolve it (`Geo/verification/2026-11-06-cross-sections.md`).
- [ ] Materials and look development for all exported assets. - [ ] Materials and look development for all exported assets.
### Gameplay - active - 01a1b26b-3c94-707f-9665-2f6b4a79b564 (branch `gameplay-crossing-0.1.0`) ### Gameplay - active - 01a1b26b-3c94-707f-9665-2f6b4a79b564 (branch `gameplay-lighting-0.1.0`)
- [x] First-person movement, mouse look, and Enhanced Input bindings. Implemented 2026-11-07: `AGigLineCharacter` (C++, 1.75 m capsule, 1.60 m eye camera, 4 m/s walk, mouse look in deg/s, jump) as the default pawn, with five actions and an 11-entry context in `/Game/Input` built by `Tools/ue_create_input_assets.py`. **Not yet confirmed interactively** — launch PIE on `Test/ReviewMap` and walk. Screenshot review pending (`Screenshots/README.md`). - [x] First-person movement, mouse look, and Enhanced Input bindings. Implemented 2026-11-07: `AGigLineCharacter` (C++, 1.75 m capsule, 1.60 m eye camera, 4 m/s walk, mouse look in deg/s, jump) as the default pawn, with five actions and an 11-entry context in `/Game/Input` built by `Tools/ue_create_input_assets.py`. **Not yet confirmed interactively** — launch PIE on `Test/ReviewMap` and walk. Screenshot review pending (`Screenshots/README.md`).
- [~] 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. - [~] 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.
- [-] 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] 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. - [x] Walkable sidewalks and a designated street crossing. Crossing chosen: mid-block signalised crossing, OSM way 385648319, at 130 m along the block (`Geo/verification/2026-11-06-street-profile.md`). Done 2026-11-07: continental zebra markings (0.5 m bars / 0.5 m gaps, 3 m band) at all three signalised crossings, no collision on the paint; kerbs were already flushed at the crossings. Enablers found on the way: the street surfaces were 100 % down-facing (invisible in engine) — winding flipped; the terrain buried the street — 10.7 m corridor cut around the centreline (sidewalks cover the transition, walk-check 0 holes); LFS-lockable `.blend`/`.umap` check out read-only — builders now chmod before saving and fail loudly. Screenshot ☑ approved 2026-11-07 (`Screenshots/demo/review-demo-crossing`); the corridor renders near-black in shadow — that was the **Fixed daytime lighting** item, now also done.
- [ ] Fixed daytime lighting and basic ambience. - [x] Fixed daytime lighting and basic ambience. **Done 2026-11-07, screenshots approved.** Demo map now has a fixed daytime rig in `Tools/ue_build_demo_map.py`: sun pitch −50 / yaw 120 (intensity 4, warm 255/245/230, atmosphere sun light), sky light 2.6 with real-time capture, sky atmosphere, height fog (density 0.008), and a manual-exposure post-process volume (min = max brightness 1.0) so a shot cannot wash out or crush to black. Two root causes found while doing it: (a) **Blender flat diffuse colours do not survive the FBX→UE import** — every site mesh arrived white and the terrain arrived with `WorldGridMaterial`, which is what actually made the scene look blown out; explicit `M_Asphalt` / `M_Concrete` / `M_Ground` / `M_RoadPaint` materials are now created and assigned in the demo builder. (b) only **one of the three crossings** was reaching the game: each band was its own object and Blender renamed the duplicates `.001`/`.002`, which site assembly skips; all bands are now one `Crossing_Markings` object. Demo cameras also fixed: street-level sat inside the façade body, and the crossing camera aimed at the wrong crossing (s=36.6 instead of the designated s=130.1 at UE X −14956). Lighting is dynamic-only (`r.AllowStaticLighting=False`, all lights Movable) so the editor's "LIGHTING NEEDS TO BE REBUILT" warning no longer fires. All four demo views approved by the owner.
- [ ] Geographic attribution display ("© OpenStreetMap contributors" + license link). - [ ] Geographic attribution display ("© OpenStreetMap contributors" + license link).
### Repository publication ### Repository publication
+137 -10
View File
@@ -50,6 +50,29 @@ def spawn(cls, loc, label, rot=None):
return actor 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=()): def clear(prefixes=()):
removed = 0 removed = 0
for actor in unreal.EditorLevelLibrary.get_all_level_actors(): for actor in unreal.EditorLevelLibrary.get_all_level_actors():
@@ -74,12 +97,101 @@ def build():
removed = clear(("Demo_", "PlayerStart", "Hero_Facade")) removed = clear(("Demo_", "PlayerStart", "Hero_Facade"))
log(f"cleared {removed} previous demo actors") log(f"cleared {removed} previous demo actors")
# --- lighting --------------------------------------------------------- # --- lighting: fixed daytime ------------------------------------------
spawn(unreal.DirectionalLight, unreal.Vector(0, 0, 2000), "Demo_Sun", # The demo is a fixed daytime scene, not a time-of-day simulation. Three
unreal.Rotator(pitch=-35.0, yaw=200.0, roll=0.0)) # things matter and each was wrong before:
spawn(unreal.SkyLight, unreal.Vector(0, 0, 2000), "Demo_SkyLight") #
# 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}")
# Movable: static lighting is disabled (r.AllowStaticLighting=False),
# so nothing needs a lighting build.
try:
comp.set_mobility(unreal.ComponentMobility.MOVABLE)
except Exception as exc: # noqa: BLE001
log(f"sun mobility 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}")
try:
comp.set_mobility(unreal.ComponentMobility.MOVABLE)
except Exception as exc: # noqa: BLE001
log(f"sky mobility not set: {exc!r}")
log("staged: Demo_SkyLight (intensity 2.6, real-time capture)")
spawn(unreal.SkyAtmosphere, unreal.Vector(0, 0, 0), "Demo_Sky") 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}")
try:
comp.set_mobility(unreal.ComponentMobility.MOVABLE)
except Exception as exc: # noqa: BLE001
log(f"fog mobility 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 facade on the measured building line ------------------------
hero = unreal.EditorAssetLibrary.load_asset("/Game/Hollywood/ElCapitan/SM_HeroFacade") hero = unreal.EditorAssetLibrary.load_asset("/Game/Hollywood/ElCapitan/SM_HeroFacade")
@@ -87,24 +199,39 @@ def build():
actor = spawn(unreal.StaticMeshActor, unreal.Vector(-10722.5, -1443.0, 0.0), "Hero_Facade") actor = spawn(unreal.StaticMeshActor, unreal.Vector(-10722.5, -1443.0, 0.0), "Hero_Facade")
if actor: if actor:
actor.static_mesh_component.set_static_mesh(hero) actor.static_mesh_component.set_static_mesh(hero)
actor.static_mesh_component.set_mobility(unreal.ComponentMobility.STATIC) actor.static_mesh_component.set_mobility(unreal.ComponentMobility.MOVABLE)
log("staged: Hero_Facade at the building line") log("staged: Hero_Facade at the building line")
else: else:
log("MISSING: SM_HeroFacade — run Tools/ue_hero_facade_import.py") log("MISSING: SM_HeroFacade — run Tools/ue_hero_facade_import.py")
# --- site meshes, if they have been imported -------------------------- # --- 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 imported = 0
for name in ("Terrain_DEM", "Road_Hollywood", "Sidewalk_South", "Sidewalk_North", for name, (mat_name, rgb) in SURFACE_MATERIALS.items():
"Kerb_South", "Kerb_North", "Crossing_Markings"):
mesh = unreal.EditorAssetLibrary.load_asset(f"/Game/Hollywood/Site/{name}") mesh = unreal.EditorAssetLibrary.load_asset(f"/Game/Hollywood/Site/{name}")
if mesh is None: if mesh is None:
continue continue
actor = spawn(unreal.StaticMeshActor, unreal.Vector(0, 0, 0), f"Demo_{name}") actor = spawn(unreal.StaticMeshActor, unreal.Vector(0, 0, 0), f"Demo_{name}")
if actor: if actor:
actor.static_mesh_component.set_static_mesh(mesh) actor.static_mesh_component.set_static_mesh(mesh)
actor.static_mesh_component.set_mobility(unreal.ComponentMobility.STATIC) actor.static_mesh_component.set_mobility(unreal.ComponentMobility.MOVABLE)
mat = ensure_material(mat_name, rgb)
if mat:
actor.static_mesh_component.set_material(0, mat)
imported += 1 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 ------------- # --- player start: north sidewalk, facing the hero frontage -------------
# Signs were measured from the imported meshes rather than assumed: the FBX # Signs were measured from the imported meshes rather than assumed: the FBX
+33 -7
View File
@@ -96,11 +96,28 @@ try:
else: else:
log("floor spawn failed") log("floor spawn failed")
if spawn(unreal.DirectionalLight, unreal.Vector(0, 0, 900), "RenderKeyLight", # Lights are Movable and the project disables static lighting
rot=unreal.Rotator(pitch=-38.0, yaw=35.0, roll=0.0)): # (r.AllowStaticLighting=False), so no baked lighting build is needed and
log("staged: DirectionalLight") # the editor stops asking to rebuild lighting.
if spawn(unreal.SkyLight, unreal.Vector(0, 0, 1200), "RenderFillSky"): sun = spawn(unreal.DirectionalLight, unreal.Vector(0, 0, 900), "RenderKeyLight",
log("staged: SkyLight") rot=unreal.Rotator(pitch=-38.0, yaw=35.0, roll=0.0))
if sun:
comp = sun.get_component_by_class(unreal.DirectionalLightComponent)
if comp:
try:
comp.set_mobility(unreal.ComponentMobility.MOVABLE)
except Exception as exc: # noqa: BLE001
log(f"key light mobility not set: {exc!r}")
log("staged: DirectionalLight (movable)")
sky = spawn(unreal.SkyLight, unreal.Vector(0, 0, 1200), "RenderFillSky")
if sky:
comp = sky.get_component_by_class(unreal.SkyLightComponent)
if comp:
try:
comp.set_mobility(unreal.ComponentMobility.MOVABLE)
except Exception as exc: # noqa: BLE001
log(f"fill sky mobility not set: {exc!r}")
log("staged: SkyLight (movable)")
if spawn(unreal.SkyAtmosphere, unreal.Vector(0, 0, 0), "RenderSky"): if spawn(unreal.SkyAtmosphere, unreal.Vector(0, 0, 0), "RenderSky"):
log("staged: SkyAtmosphere") log("staged: SkyAtmosphere")
@@ -258,8 +275,17 @@ try:
# 14 m back so the 14.5 m elevation fits: at 2 m the camera sees only # 14 m back so the 14.5 m elevation fits: at 2 m the camera sees only
# the colonnade's underside. # the colonnade's underside.
("demo-spawn-view", unreal.Vector(-10722.5, -3500, 165), unreal.Vector(-10722.5, -1450, 700)), ("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)), # Street level, from the north sidewalk: the old camera sat at Y -400,
("demo-crossing", unreal.Vector(-23750, -1800, 480), unreal.Vector(-24291, -1400, 300)), # 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 two north-side background buildings (TCL, Tussaud's) seen from
# the south sidewalk, across Hollywood Blvd: their frontage and mass. # the south sidewalk, across Hollywood Blvd: their frontage and mass.
("demo-north-buildings", unreal.Vector(-22800, -1000, 400), unreal.Vector(-22800, 3000, 300)), ("demo-north-buildings", unreal.Vector(-22800, -1000, 400), unreal.Vector(-22800, 3000, 300)),