2 Commits
Author SHA1 Message Date
zwitschi 45a206e45f materials: look-developed surface materials for every exported asset
Replaces the flat Constant3Vector fills with a procedural material set built in
ensure_material(): base colour blended between two shades by world-space noise
for large-scale variation, plus explicit roughness and specular so each surface
type reads.

- 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.

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

Docs: Screenshots/README documents the material set; TODO marks the item done
and leaves the richer texture-based pass to the 0.1.4 facade patch.
2026-11-07 19:53:39 +01:00
zwitschi 75694d2577 todo: claim materials and look development on branch materials-lookdev-0.1.0 2026-11-07 19:39:24 +01:00
26 changed files with 173 additions and 70 deletions

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.
+8 -4
View File
@@ -50,10 +50,14 @@ All ☑ approved 2026-11-07.
Z 2 m; both are exactly 1 m — the scale reference.
- **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
exposure. 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.
exposure.
- **Materials**: every exported surface carries a look-developed material built
in `Tools/ue_build_demo_map.py` (`ensure_material`) because Blender's flat
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
Movable), so no lighting build is ever required.
- **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] 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.
- [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.
- [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`).
- [ ] Materials and look development for all exported assets.
- [x] Materials and look development for all exported assets. Done 2026-11-07: 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`)
+115 -25
View File
@@ -50,26 +50,78 @@ 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.
def ensure_material(mat_name, spec):
"""Create/reuse a look-developed 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.
Blender's flat diffuse colours do not survive the FBX -> Unreal import (the
terrain even lands with WorldGridMaterial), so the demo has always assigned
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):
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(
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)
mat_name, "/Game/Hollywood/Site", unreal.Material, unreal.MaterialFactoryNew())
lib = unreal.MaterialEditingLibrary
def node(cls):
return lib.create_material_expression(mat, cls)
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)
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
@@ -209,17 +261,38 @@ def build():
# 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)),
# Look-developed surface materials (see ensure_material for the graph).
MATERIAL_SPECS = {
"M_Asphalt": dict(dark=(0.040, 0.040, 0.044), bright=(0.072, 0.072, 0.078),
roughness=0.92, specular=0.22, noise_scale=0.006),
"M_Concrete": dict(dark=(0.44, 0.42, 0.39), bright=(0.56, 0.54, 0.50),
roughness=0.88, specular=0.28, noise_scale=0.004),
"M_KerbConcrete": dict(dark=(0.47, 0.45, 0.42), bright=(0.60, 0.58, 0.54),
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
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}")
if mesh is None:
continue
@@ -227,12 +300,29 @@ def build():
if actor:
actor.static_mesh_component.set_static_mesh(mesh)
actor.static_mesh_component.set_mobility(unreal.ComponentMobility.MOVABLE)
mat = ensure_material(mat_name, rgb)
mat = built.get(mat_name)
if mat:
actor.static_mesh_component.set_material(0, mat)
imported += 1
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 -------------
# 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