materials: look-developed surface materials for every exported asset

Completes "Materials and look development for all exported assets". Every
exported surface previously carried a flat Constant3Vector fill, which read as
plastic. ensure_material() in Tools/ue_build_demo_map.py now builds a
procedural material per surface:

- base colour blended between two shades by world-space noise
  (WorldPosition -> Multiply -> Noise -> Lerp -> BaseColor) for large-scale
  variation,
- an explicit roughness and specular so the surface type is readable.

  M_Asphalt 0.92, M_Concrete 0.88, M_KerbConcrete 0.85, M_Ground 0.98,
  M_RoadPaint 0.62, M_FacadeStone 0.72, M_Stucco 0.86

Site meshes take their material by mesh name; the hero facade and the two
north-side buildings take theirs by actor label, so they are covered even
though other scripts stage them. Materials are deleted and rebuilt on every
run, so a look change cannot be masked by a stale asset.

Result: the facade reads as warm stone with surface variation, asphalt as dark
asphalt with grain, concrete and kerbs lighter and matte, and the north
buildings in stucco instead of flat grey.

Screenshots re-rendered and approved by the owner. The richer texture-based
pass (baked/procedural brick_facade, seamless_concrete, glass, material
instances) stays in the 0.1.4 facade patch.

Squash of branch materials-lookdev-0.1.0 (PR #5).
This commit is contained in:
zwitschi committed 2026-11-07 20:34:57 +01:00
1 parent c626b379d9
commit 62a41bf904
27 files changed
+181 -71

No files matched your search

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.
+6
View File
@@ -185,3 +185,9 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO.
- **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. - **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. - **"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. - 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.
## 2026-11-07 — Worktrees and the material look pass
- **A fresh git worktree has no compiled game binaries.** `Binaries/` is gitignored, so `UnrealEditor-Cmd` on a new worktree exits "abnormally (error code: 1)" before running the Python script, with no Python error in the log. Run `Engine/Build/BatchFiles/Linux/Build.sh GigLineEditor Linux Development -project=<worktree>/Game/GigLine/GigLine.uproject` first.
- `/tmp/gigline/*.txt` result files are **shared across worktrees**. Reading `/tmp/gigline/demo-map.txt` after a run can show another worktree's stale output — check the file mtime, or use a per-worktree result path.
- **Flat Constant3Vector materials read as plastic.** Giving each surface a roughness plus a world-space-noise blend between two shades (WorldPosition → Multiply → Noise → Lerp between two Constant3Vectors → BaseColor) is enough to stop the blockout looking like a toy; no textures required.
+10 -5
View File
@@ -23,7 +23,8 @@ and exits — about a minute. The camera list lives in
## Shot list ## Shot list
All ☑ approved 2026-11-07. All shots ☑ approved 2026-11-07, including the material look pass that changed
every demo surface.
| # | File | Preview | Map | View | What it shows | | # | File | Preview | Map | View | What it shows |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
@@ -50,10 +51,14 @@ All ☑ approved 2026-11-07.
Z 2 m; both are exactly 1 m — the scale reference. Z 2 m; both are exactly 1 m — the scale reference.
- **Demo map** (`/Game/Hollywood/DemoMap`): fixed daytime — sun at pitch −50 / - **Demo map** (`/Game/Hollywood/DemoMap`): fixed daytime — sun at pitch −50 /
yaw 120 (intensity 4, warm), sky light 2.6, sky atmosphere, height fog, fixed yaw 120 (intensity 4, warm), sky light 2.6, sky atmosphere, height fog, fixed
exposure. Surface colours are assigned explicitly in exposure.
`Tools/ue_build_demo_map.py` (`M_Asphalt`, `M_Concrete`, `M_Ground`, - **Materials**: every exported surface carries a look-developed material built
`M_RoadPaint`) because Blender's flat diffuse colours do not survive the in `Tools/ue_build_demo_map.py` (`ensure_material`) because Blender's flat
FBX → Unreal import. diffuse colours do not survive the FBX → Unreal import. Each has an explicit
roughness and a subtle world-space noise blend between two shades:
`M_Asphalt` (0.92), `M_Concrete` (0.88), `M_KerbConcrete` (0.85),
`M_Ground` (0.98), `M_RoadPaint` (0.62), `M_FacadeStone` (0.72) and
`M_Stucco` (0.86) for the north-side buildings.
- **Lighting is dynamic-only** (`r.AllowStaticLighting=False`, all lights - **Lighting is dynamic-only** (`r.AllowStaticLighting=False`, all lights
Movable), so no lighting build is ever required. Movable), so no lighting build is ever required.
- **Attribution overlay**: every shot carries the required ODbL credit in the - **Attribution overlay**: every shot carries the required ODbL credit in the
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.
+2 -2
View File
@@ -78,7 +78,7 @@ The MPFB character asset is planned as the NPC template (see `ROADMAP.md`). Keep
- [x] Keep the character as a reusable NPC prefab: a visible body, a collision capsule, and a head-height check that the camera does not clip at 1.60 m — for when NPCs/pedestrians are wired in (`0.2.0+`), not the `0.1.0` player. Done 2026-11-07: `AGigLineNPCPerson` (C++), ACharacter with capsule half-height 83 cm / total 1.66 m (head at ~1.66 m, so a 1.60 m camera does not clip), the MPFB human on the built-in mesh as the visible body, `AutoPossessAI` for pedestrians; confirmed spawned feet-on-Z-0 and rendered in `Screenshots/review-npc-prefab` (☑ approved 2026-11-07). - [x] Keep the character as a reusable NPC prefab: a visible body, a collision capsule, and a head-height check that the camera does not clip at 1.60 m — for when NPCs/pedestrians are wired in (`0.2.0+`), not the `0.1.0` player. Done 2026-11-07: `AGigLineNPCPerson` (C++), ACharacter with capsule half-height 83 cm / total 1.66 m (head at ~1.66 m, so a 1.60 m camera does not clip), the MPFB human on the built-in mesh as the visible body, `AutoPossessAI` for pedestrians; confirmed spawned feet-on-Z-0 and rendered in `Screenshots/review-npc-prefab` (☑ approved 2026-11-07).
- [x] Screenshots use stable camera names (no index prefix), so inserting a shot no longer renumbers/ orphans files; `AGigLineShotActor` + `Screenshots/README.md` updated. - [x] Screenshots use stable camera names (no index prefix), so inserting a shot no longer renumbers/ orphans files; `AGigLineShotActor` + `Screenshots/README.md` updated.
### Asset production ### Asset production - active - 01a1b26b-3c94-707f-9665-2f6b4a79b564 (branch `materials-lookdev-0.1.0`)
- [x] Establish a consistent scale test, named export presets, and repeatable import settings. Scale test = the three-cube calibration scene (`Art/blender/scaletest/`, regenerable via `Tools/scale_test_export.py`); named export preset `ue_metric_v1` (FBX settings in that script); named import preset `ue_metric_v1` (`FbxImportUI` settings in `Tools/ue_scale_test_import.py`). Both scripts are the templates for production asset export/import. - [x] Establish a consistent scale test, named export presets, and repeatable import settings. Scale test = the three-cube calibration scene (`Art/blender/scaletest/`, regenerable via `Tools/scale_test_export.py`); named export preset `ue_metric_v1` (FBX settings in that script); named import preset `ue_metric_v1` (`FbxImportUI` settings in `Tools/ue_scale_test_import.py`). Both scripts are the templates for production asset export/import.
- [A] Collect reference photography for manual façade work. Local only, never committed: `Art/reference/` is gitignored. **72 images in eleven folders**: hero façades to 4k, the El Capitan Theatre exterior series, premiere-night staging, Ovation Hollywood, the north side, and the Lee Drug neighbour. Gaps largely closed by the Flickr/web-search pass: the hero entrance is now documented (three portals with individual short flights — the inherited "eight steps, 12 m wide" is overturned), and the 6806–6822 retail run appears in an oblique 2013 frame. Still missing: a clean isolated frame of the door recess and a night frame of 6840 itself. - [A] Collect reference photography for manual façade work. Local only, never committed: `Art/reference/` is gitignored. **72 images in eleven folders**: hero façades to 4k, the El Capitan Theatre exterior series, premiere-night staging, Ovation Hollywood, the north side, and the Lee Drug neighbour. Gaps largely closed by the Flickr/web-search pass: the hero entrance is now documented (three portals with individual short flights — the inherited "eight steps, 12 m wide" is overturned), and the 6806–6822 retail run appears in an oblique 2013 frame. Still missing: a clean isolated frame of the door recess and a night frame of 6840 itself.
@@ -87,7 +87,7 @@ The MPFB character asset is planned as the NPC template (see `ROADMAP.md`). Keep
- [ ] Model surrounding block frontage. - [ ] Model surrounding block frontage.
- [x] Model simplified background buildings directly across Hollywood Boulevard. Done 2026-11-07, the two in-scope north-side buildings — TCL Chinese Theatre (SM_TCL_Chinese_Theatre) and Madame Tussaud's (SM_Madame_Tussauds) — taken from the verified OSM blockout volumes and exported with `ue_metric_v1`, with a Y-mirror so they land north of Hollywood Blvd across the street from the hero (TCL spans Unreal Y 1802..9431, Tussaud's 1888..7586; road centreline ≈ 0, hero façade at Y −1450). Staged in the DemoMap as `North_TCL_Chinese` / `North_Madame_Tussauds`; demo shot `review-03-demo-north-buildings` captured. Simple flat facade materials (surfaces are the `0.1.4` patch). - [x] Model simplified background buildings directly across Hollywood Boulevard. Done 2026-11-07, the two in-scope north-side buildings — TCL Chinese Theatre (SM_TCL_Chinese_Theatre) and Madame Tussaud's (SM_Madame_Tussauds) — taken from the verified OSM blockout volumes and exported with `ue_metric_v1`, with a Y-mirror so they land north of Hollywood Blvd across the street from the hero (TCL spans Unreal Y 1802..9431, Tussaud's 1888..7586; road centreline ≈ 0, hero façade at Y −1450). Staged in the DemoMap as `North_TCL_Chinese` / `North_Madame_Tussauds`; demo shot `review-03-demo-north-buildings` captured. Simple flat facade materials (surfaces are the `0.1.4` patch).
- [A] 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`). - [A] 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. - [x] Materials and look development for all exported assets. **Done 2026-11-07, screenshots approved.** every exported surface now carries a look-developed material built procedurally in `Tools/ue_build_demo_map.py` (`ensure_material`): base colour with a world-space noise blend between two shades for large-scale variation, plus an explicit roughness and specular. Set is `M_Asphalt` 0.92, `M_Concrete` 0.88, `M_KerbConcrete` 0.85, `M_Ground` 0.98, `M_RoadPaint` 0.62, `M_FacadeStone` 0.72 (hero façade) and `M_Stucco` 0.86 (TCL, Tussaud's), assigned by mesh and by actor label. Materials are rebuilt on every run so a stale asset cannot mask a look change. The richer texture-based pass (baked/procedural brick, brick_facade and seamless_concrete generators) stays in the `0.1.4` façade patch.
### Gameplay - active - 01a1b26b-3c94-707f-9665-2f6b4a79b564 (branch `gameplay-attribution-0.1.0`) ### Gameplay - active - 01a1b26b-3c94-707f-9665-2f6b4a79b564 (branch `gameplay-attribution-0.1.0`)
+115 -25
View File
@@ -50,26 +50,78 @@ def spawn(cls, loc, label, rot=None):
return actor return actor
def ensure_material(asset_name, rgb): def ensure_material(mat_name, spec):
"""Create/reuse a flat-colour material under /Game/Hollywood/Site. """Create/reuse a look-developed material under /Game/Hollywood/Site.
Used where the FBX material round-trip does not survive import: the terrain Blender's flat diffuse colours do not survive the FBX -> Unreal import (the
arrives with WorldGridMaterial (UE's bright default grid), which reads as a terrain even lands with WorldGridMaterial), so the demo has always assigned
blown-out white field under the demo sun. its own material per surface. Those were flat colour only, which read as
plastic. Each material here gets:
* base colour with a subtle world-space noise blend between two shades,
so asphalt and concrete have large-scale variation instead of a fill,
* an explicit roughness (and specular) so the surface type is readable,
wet-looking stone vs matte concrete vs chalky road paint.
spec keys: dark, bright (RGB tuples), roughness, specular, noise_scale
(world units per noise cycle; smaller = larger features).
""" """
path = f"/Game/Hollywood/Site/{asset_name}" path = f"/Game/Hollywood/Site/{mat_name}"
if unreal.EditorAssetLibrary.does_asset_exist(path): if unreal.EditorAssetLibrary.does_asset_exist(path):
return unreal.EditorAssetLibrary.load_asset(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 = unreal.AssetToolsHelpers.get_asset_tools().create_asset(
asset_name, "/Game/Hollywood/Site", unreal.Material, unreal.MaterialFactoryNew()) mat_name, "/Game/Hollywood/Site", unreal.Material, unreal.MaterialFactoryNew())
node = unreal.MaterialEditingLibrary.create_material_expression( lib = unreal.MaterialEditingLibrary
mat, unreal.MaterialExpressionConstant3Vector)
node.set_editor_property("constant", unreal.LinearColor(rgb[0], rgb[1], rgb[2], 1.0)) def node(cls):
unreal.MaterialEditingLibrary.connect_material_property( return lib.create_material_expression(mat, cls)
node, "", unreal.MaterialProperty.MP_BASE_COLOR)
unreal.MaterialEditingLibrary.recompile_material(mat) def c3(rgb):
n = node(unreal.MaterialExpressionConstant3Vector)
n.set_editor_property("constant", unreal.LinearColor(rgb[0], rgb[1], rgb[2], 1.0))
return n
def scalar(value):
n = node(unreal.MaterialExpressionConstant)
n.set_editor_property("r", float(value))
return n
# world position -> noise -> lerp(dark, bright) -> base colour
wp = node(unreal.MaterialExpressionWorldPosition)
scale = scalar(spec.get("noise_scale", 0.004))
mul = node(unreal.MaterialExpressionMultiply)
lib.connect_material_expressions(wp, "", mul, "A")
lib.connect_material_expressions(scale, "", mul, "B")
noise = node(unreal.MaterialExpressionNoise)
for prop, value in (("scale", 1.0), ("quality", 1), ("levels", 2),
("output_min", 0.0), ("output_max", 1.0)):
try:
noise.set_editor_property(prop, value)
except Exception: # noqa: BLE001
pass
lib.connect_material_expressions(mul, "", noise, "Position")
lerp = node(unreal.MaterialExpressionLinearInterpolate)
lib.connect_material_expressions(c3(spec["dark"]), "", lerp, "A")
lib.connect_material_expressions(c3(spec["bright"]), "", lerp, "B")
lib.connect_material_expressions(noise, "", lerp, "Alpha")
lib.connect_material_property(lerp, "", unreal.MaterialProperty.MP_BASE_COLOR)
lib.connect_material_property(
scalar(spec.get("roughness", 0.85)), "", unreal.MaterialProperty.MP_ROUGHNESS)
try:
lib.connect_material_property(
scalar(spec.get("specular", 0.3)), "", unreal.MaterialProperty.MP_SPECULAR)
except Exception: # noqa: BLE001 - specular input is optional in UE5
pass
lib.recompile_material(mat)
unreal.EditorAssetLibrary.save_loaded_asset(mat) unreal.EditorAssetLibrary.save_loaded_asset(mat)
log(f"created material {asset_name} rgb={rgb}") log(f"material {mat_name}: rough={spec.get('roughness')} "
f"dark={spec['dark']} bright={spec['bright']}")
return mat return mat
@@ -209,17 +261,38 @@ def build():
# builds a material with a default white base colour, and the terrain even # builds a material with a default white base colour, and the terrain even
# lands with WorldGridMaterial. Every site surface therefore rendered white, # lands with WorldGridMaterial. Every site surface therefore rendered white,
# which is what washed out the demo. Assign explicit colours per mesh here. # which is what washed out the demo. Assign explicit colours per mesh here.
SURFACE_MATERIALS = { # Look-developed surface materials (see ensure_material for the graph).
"Terrain_DEM": ("M_Ground", (0.30, 0.28, 0.25)), MATERIAL_SPECS = {
"Road_Hollywood": ("M_Asphalt", (0.055, 0.055, 0.06)), "M_Asphalt": dict(dark=(0.040, 0.040, 0.044), bright=(0.072, 0.072, 0.078),
"Sidewalk_South": ("M_Concrete", (0.52, 0.50, 0.47)), roughness=0.92, specular=0.22, noise_scale=0.006),
"Sidewalk_North": ("M_Concrete", (0.52, 0.50, 0.47)), "M_Concrete": dict(dark=(0.44, 0.42, 0.39), bright=(0.56, 0.54, 0.50),
"Kerb_South": ("M_Concrete", (0.52, 0.50, 0.47)), roughness=0.88, specular=0.28, noise_scale=0.004),
"Kerb_North": ("M_Concrete", (0.52, 0.50, 0.47)), "M_KerbConcrete": dict(dark=(0.47, 0.45, 0.42), bright=(0.60, 0.58, 0.54),
"Crossing_Markings": ("M_RoadPaint", (0.80, 0.80, 0.78)), roughness=0.85, specular=0.30, noise_scale=0.008),
"M_Ground": dict(dark=(0.22, 0.20, 0.17), bright=(0.33, 0.30, 0.26),
roughness=0.98, specular=0.15, noise_scale=0.003),
"M_RoadPaint": dict(dark=(0.74, 0.74, 0.72), bright=(0.86, 0.86, 0.84),
roughness=0.62, specular=0.35, noise_scale=0.02),
"M_FacadeStone": dict(dark=(0.62, 0.58, 0.50), bright=(0.74, 0.70, 0.62),
roughness=0.72, specular=0.32, noise_scale=0.005),
"M_Stucco": dict(dark=(0.52, 0.48, 0.42), bright=(0.66, 0.62, 0.55),
roughness=0.86, specular=0.25, noise_scale=0.004),
} }
SURFACE_MATERIALS = {
"Terrain_DEM": "M_Ground",
"Road_Hollywood": "M_Asphalt",
"Sidewalk_South": "M_Concrete",
"Sidewalk_North": "M_Concrete",
"Kerb_South": "M_KerbConcrete",
"Kerb_North": "M_KerbConcrete",
"Crossing_Markings": "M_RoadPaint",
}
built = {}
for mat_name, spec in MATERIAL_SPECS.items():
built[mat_name] = ensure_material(mat_name, spec)
imported = 0 imported = 0
for name, (mat_name, rgb) in SURFACE_MATERIALS.items(): for name, mat_name in SURFACE_MATERIALS.items():
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
@@ -227,12 +300,29 @@ def build():
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.MOVABLE) actor.static_mesh_component.set_mobility(unreal.ComponentMobility.MOVABLE)
mat = ensure_material(mat_name, rgb) mat = built.get(mat_name)
if mat: if mat:
actor.static_mesh_component.set_material(0, mat) actor.static_mesh_component.set_material(0, mat)
imported += 1 imported += 1
log(f"staged {imported} site meshes with explicit surface materials") log(f"staged {imported} site meshes with explicit surface materials")
# The hero facade and the two north-side buildings are staged by other
# scripts (or reused between rebuilds), so assign their look materials by
# actor label after everything is in the level.
LABEL_MATERIALS = {"Hero_Facade": "M_FacadeStone",
"North_TCL_Chinese": "M_Stucco",
"North_Madame_Tussauds": "M_Stucco"}
assigned = 0
for actor in unreal.EditorLevelLibrary.get_all_level_actors():
mat_name = LABEL_MATERIALS.get(actor.get_actor_label())
if not mat_name or mat_name not in built:
continue
comp = actor.get_component_by_class(unreal.StaticMeshComponent)
if comp and comp.static_mesh:
comp.set_material(0, built[mat_name])
assigned += 1
log(f"assigned look materials to {assigned} facade/background actors")
# --- 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
# preset flips north, so in Unreal the road spans Y -1382..+1047, the NORTH # preset flips north, so in Unreal the road spans Y -1382..+1047, the NORTH