# Build /Game/Hollywood/DemoMap: the walkable demo level. # # /Engine/Binaries/Linux/UnrealEditor-Cmd -run=pythonscript \ # -script=/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}") log("staged: Demo_Sun (pitch -50, yaw 120, intensity 4, warm)") sky = spawn(unreal.SkyLight, unreal.Vector(0, 0, 2000), "Demo_SkyLight") if sky: comp = sky.get_component_by_class(unreal.SkyLightComponent) if comp: for prop, value in (("intensity", 2.6), ("real_time_capture", True)): try: comp.set_editor_property(prop, value) except Exception as exc: # noqa: BLE001 log(f"sky.{prop} not set: {exc!r}") log("staged: Demo_SkyLight (intensity 2.6, real-time capture)") spawn(unreal.SkyAtmosphere, unreal.Vector(0, 0, 0), "Demo_Sky") log("staged: Demo_Sky (sky atmosphere)") fog = spawn(unreal.ExponentialHeightFog, unreal.Vector(0, 0, 0), "Demo_Fog") if fog: comp = fog.get_component_by_class(unreal.ExponentialHeightFogComponent) if comp: for prop, value in ( ("fog_density", 0.008), ("fog_height_falloff", 0.15), ("start_distance", 3000.0), ): try: comp.set_editor_property(prop, value) except Exception as exc: # noqa: BLE001 log(f"fog.{prop} not set: {exc!r}") log("staged: Demo_Fog (height fog, density 0.008)") # Fixed exposure: without this the same road renders washed out or black # depending on how much sky the camera frames. Pinning min == max brightness # freezes the exposure at one value (the classic fixed-exposure trick); # AEM_MANUAL with a bias was tried first and blew the frame out. ppv = spawn(unreal.PostProcessVolume, unreal.Vector(0, 0, 0), "Demo_PostProcess") if ppv: ppv.set_editor_property("unbound", True) settings = ppv.get_editor_property("settings") for prop, value in ( ("override_auto_exposure_method", True), ("auto_exposure_method", unreal.AutoExposureMethod.AEM_HISTOGRAM), ("override_auto_exposure_min_brightness", True), ("auto_exposure_min_brightness", 1.0), ("override_auto_exposure_max_brightness", True), ("auto_exposure_max_brightness", 1.0), ("override_auto_exposure_bias", True), ("auto_exposure_bias", 0.0), ): try: settings.set_editor_property(prop, value) except Exception as exc: # noqa: BLE001 log(f"pp.{prop} not set: {exc!r}") ppv.set_editor_property("settings", settings) log("staged: Demo_PostProcess (fixed exposure, min=max brightness 1.0)") # --- hero facade on the measured building line ------------------------ hero = unreal.EditorAssetLibrary.load_asset("/Game/Hollywood/ElCapitan/SM_HeroFacade") 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.STATIC) 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.STATIC) mat = ensure_material(mat_name, rgb) if mat: actor.static_mesh_component.set_material(0, mat) imported += 1 log(f"staged {imported} site meshes 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")