Files
gig-line/Tools/ue_build_demo_map.py
T
zwitschi 240bad9af1 lighting: dynamic-only lights so no lighting build is required
The editor warned 'LIGHTING NEEDS TO BE REBUILT' (Build Lighting Only /
BuildLighting) because the sun and sky were Stationary with no baked lightmap.
The demo is a fixed daytime scene lit entirely dynamically, so:

- Config/DefaultEngine.ini: r.AllowStaticLighting=False - nothing to build.
- DemoMap + ReviewMap builders: sun, sky light, height fog and the staged
  mesh components are set to ComponentMobility.MOVABLE.

Also clears the Android File Server SecurityToken in DefaultEngine.ini: the
plugin is disabled and excluded from shipping, and the value is a credential
that must not be committed.

All 14 screenshots re-rendered; no lighting warnings in the build or run logs.
2026-11-07 18:08:49 +01:00

265 lines
12 KiB
Python

# Build /Game/Hollywood/DemoMap: the walkable demo level.
#
# <engine>/Engine/Binaries/Linux/UnrealEditor-Cmd <uproject> -run=pythonscript \
# -script=<repo>/Tools/ue_build_demo_map.py -nullrhi -unattended -nopause -stdout
#
# Content this map expects (built by earlier steps):
# /Game/Hollywood/ElCapitan/SM_HeroFacade Tools/ue_hero_facade_import.py
# /Game/Hollywood/Site/... Tools/ue_import_site.py
#
# Map conventions, from Geo/manifests/local-origin.md:
# X east, Y south, Z up, centimetres, origin at Hollywood x Highland.
# The player starts on the north sidewalk opposite the hero frontage, looking
# south at it, which is the view the reference photographs were taken from.
#
# Every actor is (re)created by label, so rerunning the script rebuilds the map
# deterministically instead of stacking duplicates.
import os
import unreal
MAP = "/Game/Hollywood/DemoMap"
RESULT = "/tmp/gigline/demo-map.txt"
lines = []
# 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). The
# LevelEditorSubsystem actually switches.
LES = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
def switch_level(path):
return LES.load_level(path)
def log(msg):
lines.append(str(msg))
unreal.log(str(msg))
def spawn(cls, loc, label, rot=None):
actor = unreal.EditorLevelLibrary.spawn_actor_from_class(cls, loc)
if actor is None:
log(f"spawn {cls} failed")
return None
if rot is not None:
actor.set_actor_rotation(rot, False)
actor.set_actor_label(label)
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():
label = actor.get_actor_label()
if label.startswith(prefixes):
unreal.EditorLevelLibrary.destroy_actor(actor)
removed += 1
return removed
def build():
# --- level ------------------------------------------------------------
if unreal.EditorAssetLibrary.does_asset_exist(MAP):
if not switch_level(MAP):
log(f"could not load {MAP}")
return
else:
if not switch_level(MAP):
log(f"could not create {MAP}")
return
removed = clear(("Demo_", "PlayerStart", "Hero_Facade"))
log(f"cleared {removed} previous demo actors")
# --- 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}")
# 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")
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 = unreal.EditorAssetLibrary.load_asset("/Game/Hollywood/ElCapitan/SM_HeroFacade")
if hero:
actor = spawn(unreal.StaticMeshActor, unreal.Vector(-10722.5, -1443.0, 0.0), "Hero_Facade")
if actor:
actor.static_mesh_component.set_static_mesh(hero)
actor.static_mesh_component.set_mobility(unreal.ComponentMobility.MOVABLE)
log("staged: Hero_Facade at the building line")
else:
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, (mat_name, rgb) in SURFACE_MATERIALS.items():
mesh = unreal.EditorAssetLibrary.load_asset(f"/Game/Hollywood/Site/{name}")
if mesh is None:
continue
actor = spawn(unreal.StaticMeshActor, unreal.Vector(0, 0, 0), f"Demo_{name}")
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)
if mat:
actor.static_mesh_component.set_material(0, mat)
imported += 1
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
# preset flips north, so in Unreal the road spans Y -1382..+1047, the NORTH
# sidewalk is at Y -1912..-1053 and the SOUTH sidewalk at Y +718..+1447. The
# facade stands at Y -1450. Stand on the north sidewalk and look north (-Y).
spawn(unreal.PlayerStart, unreal.Vector(-10722.5, -3500.0, 120.0), "Demo_PlayerStart",
unreal.Rotator(pitch=0.0, yaw=0.0, roll=0.0))
log("staged: Demo_PlayerStart on the north sidewalk facing south")
# A lockable LFS .umap checks out read-only, which makes save_map silently
# return False — chmod u+w first or the staged actors never persist.
umap = os.path.join(unreal.Paths.project_dir(), MAP.replace("/Game/", "Content/") + ".umap")
if os.path.exists(umap) and not os.access(umap, os.W_OK):
os.chmod(umap, 0o644)
log(f"map writable: chmod u+w {umap}")
saved = unreal.EditorLoadingAndSavingUtils.save_map(
unreal.EditorLevelLibrary.get_editor_world(), MAP)
log(f"save_map {MAP}: {saved}")
try:
build()
except Exception as exc: # noqa: BLE001
import traceback
log(f"FAILED: {type(exc).__name__}: {exc}")
log(traceback.format_exc())
with open(RESULT, "w") as f:
f.write("\n".join(lines) + "\n")