# Builds /Game/Test/ReviewMap — the visual-review scene — and saves it as a # map asset. Run before the screenshot pass (Tools/render_screenshots.sh does # this automatically). # # Scene: metric floor, key light, fill sky, sky atmosphere, MPFB character, # the three 1 m scale cubes, and an AGigLineShotActor with the camera list. # # UnrealEditor-Cmd -run=pythonscript -script=/Tools/ue_build_review_map.py \ # -nullrhi -unattended -nopause -stdout # # Do NOT load_map here: after a map load the commandlet's spawn context dies # (every spawn_actor call returns None). Everything is spawned into the # default world and saved as a new map asset. import os import unreal RESULT = "/tmp/ue-review-map-result.txt" MAP_PATH = "/Game/Test/ReviewMap" lines = [] def log(msg): lines.append(str(msg)) def ensure_writable(path): # A packed .umap left read-only (git sets 644->444 on checkout, or the # editor re-flags it) makes save_map silently return False with no error. # chmod u+w it so the staged camera list and actors actually persist. p = os.path.join(unreal.Paths.project_dir(), path.replace("/Game/", "Content/") + ".umap") try: mode = os.stat(p).st_mode & 0o777 if mode & 0o200 == 0: os.chmod(p, 0o644) log(f"map writable: chmod u+w {p} (was {oct(mode)})") except FileNotFoundError: pass def spawn(cls, loc, label=None, scale=None, rot=None): a = unreal.EditorLevelLibrary.spawn_actor_from_class(cls, loc) if a is None: log(f"spawn {cls.__name__} returned None") return None if rot is not None: a.set_actor_rotation(rot, False) if scale is not None: a.set_actor_scale3d(scale) if label: a.set_actor_label(label) return a # --- scene ------------------------------------------------------------------- try: # Neutral gray floor material (the scale-test mesh ships red) floor_mat = None if not unreal.EditorAssetLibrary.does_asset_exist("/Game/Test/M_ReviewFloor"): floor_mat = unreal.AssetToolsHelpers.get_asset_tools().create_asset( "M_ReviewFloor", "/Game/Test", unreal.Material, unreal.MaterialFactoryNew()) n = unreal.MaterialEditingLibrary.create_material_expression( floor_mat, unreal.MaterialExpressionConstant3Vector) n.set_editor_property("constant", unreal.LinearColor(0.35, 0.35, 0.35, 1.0)) unreal.MaterialEditingLibrary.connect_material_property( n, "", unreal.MaterialProperty.MP_BASE_COLOR) unreal.MaterialEditingLibrary.recompile_material(floor_mat) unreal.EditorAssetLibrary.save_loaded_asset(floor_mat) else: floor_mat = unreal.EditorAssetLibrary.load_asset("/Game/Test/M_ReviewFloor") floor_mesh = unreal.EditorAssetLibrary.load_asset("/Game/Test/ScaleTest/ScaleTest_A") if floor_mesh and floor_mat: # 1 m reference cube scaled to 90 m x 90 m x 0.1 m, top face at Z 0. The # scale is applied on the actor, so Z is -5 cm (+/- 5 cm around centre). # Covers the review scene AND the hero blockout 100 m away, so facade shots # no longer end at the black horizon band (160 m square, top face at Z 0). floor = spawn(unreal.StaticMeshActor, unreal.Vector(-5000, 450, -5.0), "ReviewFloor", scale=(160.0, 160.0, 0.1)) if floor: floor.static_mesh_component.set_static_mesh(floor_mesh) floor.static_mesh_component.set_material(0, floor_mat) log("staged: floor (160 m square, gray, top face Z 0)") else: log("floor spawn failed") if spawn(unreal.DirectionalLight, unreal.Vector(0, 0, 900), "RenderKeyLight", rot=unreal.Rotator(pitch=-38.0, yaw=35.0, roll=0.0)): log("staged: DirectionalLight") if spawn(unreal.SkyLight, unreal.Vector(0, 0, 1200), "RenderFillSky"): log("staged: SkyLight") if spawn(unreal.SkyAtmosphere, unreal.Vector(0, 0, 0), "RenderSky"): log("staged: SkyAtmosphere") char_mesh = unreal.EditorAssetLibrary.load_asset("/Game/Test/Character/character") if char_mesh: ch = spawn(unreal.SkeletalMeshActor, unreal.Vector(0, 0, 0), "ReviewCharacter", rot=unreal.Rotator(pitch=0.0, yaw=180.0, roll=0.0)) if ch: comp = ch.skeletal_mesh_component comp.set_skinned_asset_and_update(char_mesh) # Root at the soles, not the world origin: the composes put feet on the # floor's top face (Z = 0). If the skeletal root is not at the soles the # character would float or sink, so report the bounds for the review. if hasattr(ch, "set_actor_location"): ch.set_actor_location(unreal.Vector(0, 0, 0), False, False) mn, mx = (None, None) try: _origin, _box = ch.get_actor_bounds(False) log(f"staged: MPFB character at origin, bounds z {_box.z:.1f} cm") except Exception as exc: # noqa: BLE001 log(f"character bounds unavailable: {exc!r}") log("staged: MPFB character at origin, facing -Y") else: log("MISSING: /Game/Test/Character/character") # Two cubes only, on purpose: one resting on the floor gives the ground-contact # and scale reference, one held at 2 m gives a clean cast shadow to compare # against. Their positions are chosen so both are in the overview frame and # neither overlaps the character. cube_specs = ( ("ScaleTest_A", -250.0, 600.0, 50.0, "on the ground: base at Z 0"), ("ScaleTest_B", 250.0, 600.0, 250.0, "floating at 2 m: casts a separation shadow"), ) for name, x, y, z, why in cube_specs: m = unreal.EditorAssetLibrary.load_asset(f"/Game/Test/ScaleTest/{name}") if m: c = spawn(unreal.StaticMeshActor, unreal.Vector(x, y, z), f"Cube_{name}") if c: c.static_mesh_component.set_static_mesh(m) log(f"staged: {name} at ({x}, {y}, {z}) - {why}") # Hero facade, if the import has run: two views a reviewer needs — straight on # from the north sidewalk, and the oblique street view from further west. # Hero placement in Unreal centimetres (see Geo/manifests/local-origin.md): # front plane on the building line, body behind it. hero_x = -12024.5 + 1302.0 hero_y = -1450.0 + 7.0 hero = unreal.EditorAssetLibrary.load_asset("/Game/Hollywood/ElCapitan/SM_HeroFacade") # The imported mesh spans Y -7..+1250 cm in local space (front plane at -7, the # body behind it) and is centred on X, so the actor sits at building line + 7 cm. if hero: actor = spawn(unreal.StaticMeshActor, unreal.Vector(hero_x, hero_y, 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 measured building line") else: log("MISSING: /Game/Hollywood/ElCapitan/SM_HeroFacade (run Tools/ue_hero_facade_import.py)") # --- player pawn --------------------------------------------------------- # The review rounds must show the pawn the game actually spawns, not only the # mannequin. Spawn the real character class so its capsule, camera height and # ground contact are what gets rendered. pawn = None char_class = unreal.load_class(None, "/Script/GigLine.GigLineCharacter") if char_class: pawn = spawn(char_class, unreal.Vector(0, 0, 90), "PlayerPawn") if pawn: log("staged: PlayerPawn (AGigLineCharacter) at the spawn point") else: log("MISSING: /Script/GigLine.GigLineCharacter") # --- NPC prefab ----------------------------------------------------------- # The reusable NPC person: a collision capsule + visible MPFB body, standing # feet on Z 0, placed clear of the pawn so the prefab reads on its own. npc = None npc_class = unreal.load_class(None, "/Script/GigLine.GigLineNPCPerson") if npc_class: npc = spawn(npc_class, unreal.Vector(-4500, 300, 0), "NPC_Person") if npc: log("staged: NPC_Person (AGigLineNPCPerson) at Z 0") else: log("MISSING: /Script/GigLine.GigLineNPCPerson") # --- camera list --------------------------------------------------------- # Every camera is a (name, location, target) triple; the look-at rotation is # computed from the pair so a camera cannot drift out of aim when one number # changes. Keep retired shots out of this list entirely. cams = [ # wide scene: character, both cubes, floor ("overview", unreal.Vector(700, -900, 620), unreal.Vector(0, 300, 80)), # the MPFB mannequin, mid shot ("character", unreal.Vector(200, -300, 160), unreal.Vector(0, 0, 85)), # both cubes from the south: contact shadow vs cast shadow ("scale-cubes", unreal.Vector(0, -900, 200), unreal.Vector(0, 600, 80)), # the pawn the game spawns, with the grounded cube beside it ("player-placement", unreal.Vector(320, -520, 140), unreal.Vector(0, 0, 80)), # the NPC prefab: capsule + visible body, feet on Z 0, at double scale # distance because it stands off to the west of the main staging ("npc-prefab", unreal.Vector(-4200, 600, 160), unreal.Vector(-4500, 300, 90)), # low, close on feet and cube base: ground contact must be obvious ("ground-contact", unreal.Vector(180, -260, 35), unreal.Vector(0, 40, 20)), # shadow comparison, low angle so both shadows read ("shadow-compare", unreal.Vector(-650, -700, 120), unreal.Vector(0, 600, 60)), # the three entry portals at street level # street level, aimed at the base of the centre portal: the portals sit at # Z 0..340 cm behind a 145 cm plinth, so the camera looks slightly down and # targets the portal sill rather than the centre of the elevation. # Standing 25 m out at eye height frames the whole elevation, which is what # the portal detail needs: the portals are small against a 14.5 m façade. # On the pavement in front of the flight: a pedestrian's own eye line, the # angle the photographs that established the portals were taken from. # eye height at 8 m: fills the frame with the three doorways, the flights and # the plinth, which is what "portal detail" means here. ("hero-portals", unreal.Vector(hero_x + 100, -2250, 170), unreal.Vector(hero_x + 100, hero_y, 230)), # straight on from the north sidewalk; the building body spans Y -1443..+250 ("hero-facade-front", unreal.Vector(hero_x + 200, -3200, 420), unreal.Vector(hero_x, hero_y + 400, 760)), # oblique from the north-west, reading the colonnade in depth ("hero-facade-oblique", unreal.Vector(hero_x - 2200, -3400, 620), unreal.Vector(hero_x + 300, hero_y, 700)), ] # --- demo map views: what the player actually sees ---------------------- demo = unreal.EditorAssetLibrary.load_asset("/Game/Hollywood/DemoMap") if demo and unreal.EditorAssetLibrary.does_asset_exist("/Game/Hollywood/DemoMap"): # The demo map has terrain, street, sidewalks and the facade; the review # floor does not. Load it, stage the shot cameras, save, then restore the # review map so the rest of the run is unaffected. unreal.EditorLevelLibrary.load_level("/Game/Hollywood/DemoMap") for actor in unreal.EditorLevelLibrary.get_all_level_actors(): if actor.get_actor_label() == "ShotSequencer": unreal.EditorLevelLibrary.destroy_actor(actor) # Unreal signs, measured from the imported site meshes: north sidewalk at # Y -1912..-1053, road -1382..+1047, south sidewalk +718..+1447, facade at # Y -1450. So "opposite the facade" is Y -1650 and the view faces -Y. demo_shots = [ # 14 m back so the 14.5 m elevation fits: at 2 m the camera sees only # the colonnade's underside. ("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)), ("demo-crossing", unreal.Vector(-23750, -1800, 480), unreal.Vector(-24291, -1400, 300)), # the two north-side background buildings (TCL, Tussaud's) seen from # the south sidewalk, across Hollywood Blvd: their frontage and mass. ("demo-north-buildings", unreal.Vector(-22800, -1000, 400), unreal.Vector(-22800, 3000, 300)), ] seq = spawn(unreal.GigLineShotActor, unreal.Vector(0, 0, 300), "ShotSequencer") if seq: seq.set_editor_property("shot_locations", [c[1] for c in demo_shots]) seq.set_editor_property("shot_rotations", [unreal.MathLibrary.find_look_at_rotation(c[1], c[2]) for c in demo_shots]) seq.set_editor_property("shot_names", [c[0] for c in demo_shots]) # quit after the demo shots so a dedicated DemoMap capture run # terminates cleanly (render_screenshots.sh runs both maps). seq.set_editor_property("quit_when_done", True) ensure_writable("/Game/Hollywood/DemoMap") saved = unreal.EditorLoadingAndSavingUtils.save_map( unreal.EditorLevelLibrary.get_editor_world(), "/Game/Hollywood/DemoMap") log(f"demo map staged: {len(demo_shots)} cameras, save {saved}") unreal.EditorLevelLibrary.load_level("/Game/Test/ReviewMap") for actor in unreal.EditorLevelLibrary.get_all_level_actors(): if actor.get_actor_label() == "ShotSequencer": unreal.EditorLevelLibrary.destroy_actor(actor) else: log("MISSING: /Game/Hollywood/DemoMap (run Tools/ue_build_demo_map.py)") shots = spawn(unreal.GigLineShotActor, unreal.Vector(0, 0, 300), "ShotSequencer") if shots: shots.set_editor_property("shot_locations", [c[1] for c in cams]) shots.set_editor_property( "shot_rotations", [unreal.MathLibrary.find_look_at_rotation(c[1], c[2]) for c in cams]) shots.set_editor_property("shot_names", [c[0] for c in cams]) shots.set_editor_property("quit_when_done", True) log(f"staged: ShotSequencer with {len(cams)} cameras: {', '.join(c[0] for c in cams)}") except Exception as exc: # noqa log(f"staging failed: {exc!r}") # --- save -------------------------------------------------------------------- try: ensure_writable(MAP_PATH) world = unreal.EditorLevelLibrary.get_editor_world() saved = unreal.EditorLoadingAndSavingUtils.save_map(world, MAP_PATH) log(f"save_map {MAP_PATH}: {saved}") except Exception as exc: # noqa log(f"save_map failed: {exc!r}") with open(RESULT, "w") as f: f.write("\n".join(lines) + "\n")