The generated ashlar albedo never showed for two reasons that had nothing to do with the material graph,
which was verified correct the whole time (TextureSampleParameter2D -> Multiply -> Base Color, colour sampler,
texture set, 6x UV tiling):
1. **The map overrode every hero slot.** The generic per-slot material pass in ue_build_demo_map.py assigned
the facade KIT's flat stone to every actor whose slot names looked like stone, banner, glass - including
Hero_Facade. Whatever the import authored was replaced at map build time. The hero is now excluded and gets
its own materials.
2. **Mesh slot assignment does not persist.** Assigning to mesh.get_editor_property('static_materials') returns a
copy; after a reload the mesh still pointed at the original imported materials (measured twice). Component
level overrides do persist in the saved map, so apply_hero_materials() sets them in both builders.
The elevation now renders as coursed, grainy warm limestone with banners, window bays and recessed portals.
Rows 4-6 re-rendered and pending review; the comparison plate is regenerated. Still missing against the
reference: carved inscription, medallion relief, banner lettering, crimson banner tone. The two-model judge
returns no/no (0.90/0.95) - it wants the text and detail that are still absent, which is at least the right
complaint this time.
476 lines
26 KiB
Python
476 lines
26 KiB
Python
# Builds /Game/Test/ReviewMap — the visual-review scene — and saves it as a
|
|
# map asset. Run before the screenshot pass (Tools/review/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 <uproject> -run=pythonscript -script=<repo>/Tools/unreal/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 = []
|
|
|
|
|
|
# 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))
|
|
|
|
def apply_hero_materials(actor):
|
|
"""Point the hero's component slots at the authored *_Ref materials, if they exist.
|
|
|
|
The mesh's own slot list cannot be relied on: `get_editor_property("static_materials")` returns a copy, so
|
|
assigning to it does not persist across a reload. A component-level override does persist in the saved map,
|
|
and it is what the captures actually render.
|
|
"""
|
|
if actor is None or actor.get_actor_label() != "Hero_Facade":
|
|
return 0
|
|
comp = actor.get_component_by_class(unreal.StaticMeshComponent)
|
|
if not comp or not comp.static_mesh:
|
|
return 0
|
|
slots = comp.static_mesh.get_editor_property("static_materials")
|
|
applied = 0
|
|
for index, slot in enumerate(slots):
|
|
current = slot.get_editor_property("material_interface")
|
|
if not current:
|
|
continue
|
|
ref = unreal.EditorAssetLibrary.load_asset(
|
|
f"/Game/Hollywood/ElCapitan/{current.get_name()}_Ref")
|
|
if ref:
|
|
comp.set_material(index, ref)
|
|
applied += 1
|
|
if applied:
|
|
log(f"hero materials: {applied}/{len(slots)} slots use the authored *_Ref materials")
|
|
return applied
|
|
|
|
|
|
|
|
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 clear_level():
|
|
"""Destroy every actor before staging.
|
|
|
|
Without this the review map accumulated whatever previous runs and experiments left behind, and the
|
|
captures showed a scene nobody staged. The map is rebuilt from empty on every run.
|
|
"""
|
|
removed = 0
|
|
for actor in list(unreal.EditorLevelLibrary.get_all_level_actors()):
|
|
try:
|
|
unreal.EditorLevelLibrary.destroy_actor(actor)
|
|
removed += 1
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
log(f"cleared {removed} actors before staging")
|
|
return removed
|
|
|
|
|
|
def sit_on_floor(actor, floor_z=2.0):
|
|
"""Shift an actor so the mesh's lowest point lands on `floor_z`.
|
|
|
|
Staging heights were hand-typed and every one of them was a little wrong - the mannequin's root
|
|
is not at the soles, the scale cube's origin is not at its base - which is what the review keeps
|
|
catching. Measuring the bounds and correcting is the only version that stays right when an
|
|
asset changes. The default 2 cm keeps soles and tyres on TOP of the floor plane rather than
|
|
exactly level with it, where they read as sunk.
|
|
"""
|
|
try:
|
|
origin, box = actor.get_actor_bounds(False)
|
|
except Exception: # noqa: BLE001
|
|
return None
|
|
lowest = origin.z - box.z
|
|
loc = actor.get_actor_location()
|
|
actor.set_actor_location(unreal.Vector(loc.x, loc.y, loc.z + (floor_z - lowest)), False, False)
|
|
return floor_z - lowest
|
|
|
|
|
|
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:
|
|
# Start from an empty level: the review map used to keep whatever previous runs left behind, and
|
|
# captures showed a scene nobody staged.
|
|
clear_level()
|
|
|
|
# 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")
|
|
|
|
# Lights are Movable and the project disables static lighting
|
|
# (r.AllowStaticLighting=False), so no baked lighting build is needed and
|
|
# the editor stops asking to rebuild lighting.
|
|
sun = spawn(unreal.DirectionalLight, unreal.Vector(0, 0, 900), "RenderKeyLight",
|
|
rot=unreal.Rotator(pitch=-38.0, yaw=35.0, roll=0.0))
|
|
if sun:
|
|
comp = sun.get_component_by_class(unreal.DirectionalLightComponent)
|
|
if comp:
|
|
try:
|
|
comp.set_mobility(unreal.ComponentMobility.MOVABLE)
|
|
except Exception as exc: # noqa: BLE001
|
|
log(f"key light mobility not set: {exc!r}")
|
|
log("staged: DirectionalLight (movable)")
|
|
sky = spawn(unreal.SkyLight, unreal.Vector(0, 0, 1200), "RenderFillSky")
|
|
if sky:
|
|
comp = sky.get_component_by_class(unreal.SkyLightComponent)
|
|
if comp:
|
|
try:
|
|
comp.set_mobility(unreal.ComponentMobility.MOVABLE)
|
|
except Exception as exc: # noqa: BLE001
|
|
log(f"fill sky mobility not set: {exc!r}")
|
|
log("staged: SkyLight (movable)")
|
|
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.
|
|
# Actor bounds on a freshly assigned skeletal mesh read as zero, so the generic
|
|
# correction came out at +2 cm while the mannequin sat visibly low. 8 cm was then
|
|
# 2 cm too high on review, so it sits at 6.
|
|
shift = sit_on_floor(ch, floor_z=6.0)
|
|
if shift is not None:
|
|
log(f"staged: MPFB character sat on the floor (shift {shift:+.1f} cm)")
|
|
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)
|
|
if name == "ScaleTest_A":
|
|
# The resting cube is the ground-contact reference, so its base must be
|
|
# exactly on the floor rather than wherever its origin happens to be.
|
|
shift = sit_on_floor(c)
|
|
log(f"staged: {name} sat on the floor (shift {shift:+.1f} cm)")
|
|
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.
|
|
# Corrected 2026-11-07 with the block frontage: UE south is +Y (south
|
|
# sidewalk Y +718..+1447) and 6840 occupies the frontage gap X -14591..-12082.
|
|
hero_x = -13336.0
|
|
hero_y = 1307.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)
|
|
apply_hero_materials(actor)
|
|
log("staged: hero facade at the measured building line")
|
|
else:
|
|
log("MISSING: /Game/Hollywood/ElCapitan/SM_HeroFacade (run Tools/unreal/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")
|
|
|
|
# --- vehicle, from the latest Blender import ------------------------------
|
|
# Staged from the same assets the demo map uses, so the review always shows what
|
|
# Tools/blender/vehicle_from_template.py most recently produced rather than a copy that can
|
|
# drift. The panels are plain static meshes because the vehicle pawn keeps its
|
|
# physics only.
|
|
vehicle_origin = unreal.Vector(-1600.0, 600.0, 0.0)
|
|
vehicle_systems = ("SM_Vehicle_Body", "SM_Vehicle_Trim", "SM_Vehicle_Glass",
|
|
"SM_Vehicle_Lights", "SM_Vehicle_Wheels")
|
|
staged_vehicle = 0
|
|
for index, mesh_name in enumerate(vehicle_systems):
|
|
mesh = unreal.EditorAssetLibrary.load_asset(f"/Game/Hollywood/Vehicle/{mesh_name}")
|
|
if not mesh:
|
|
log(f"MISSING vehicle system: {mesh_name}")
|
|
continue
|
|
piece = spawn(unreal.StaticMeshActor, vehicle_origin, f"Review_Vehicle_{index:02d}")
|
|
if piece:
|
|
piece.static_mesh_component.set_static_mesh(mesh)
|
|
piece.static_mesh_component.set_mobility(unreal.ComponentMobility.STATIC)
|
|
if mesh_name == "SM_Vehicle_Wheels":
|
|
shift = sit_on_floor(piece)
|
|
log(f"staged: vehicle wheels sat on the floor (shift {shift:+.1f} cm)")
|
|
if mesh_name == "SM_Vehicle_Body":
|
|
paint = unreal.EditorAssetLibrary.load_asset(
|
|
"/Game/Hollywood/Vehicle/M_Vehicle_HatchPaint")
|
|
if paint:
|
|
piece.static_mesh_component.set_material(0, paint)
|
|
staged_vehicle += 1
|
|
log(f"staged: {staged_vehicle} vehicle systems at the staging area")
|
|
|
|
# --- 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 vehicle staging, three-quarter front, same framing idea as the demo shot
|
|
("vehicle", unreal.Vector(-900, -200, 260), unreal.Vector(-1600, 600, 90)),
|
|
# ground-level contact check: feet on the floor, shadow at the contact point
|
|
("character-feet", unreal.Vector(220, -420, 45), unreal.Vector(0, 0, 25)),
|
|
# 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
|
|
# Level horizon: camera height == target height, so the elevation's
|
|
# verticals stay parallel. The old shot looked up ~4 deg from 4.2 m at a
|
|
# 7.6 m target, which converged the columns — the one thing an
|
|
# architectural elevation must not do (archviz: "straight verticals").
|
|
("hero-facade-front", unreal.Vector(hero_x + 200, -3200, 760), unreal.Vector(hero_x, hero_y + 400, 760), 42.0),
|
|
# 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), 55.0),
|
|
]
|
|
# --- review shot cameras + save -------------------------------------------
|
|
# Order matters: the review scene is staged in the default (unsaved) world.
|
|
# Save it as ReviewMap BEFORE touching DemoMap — switching back to a map
|
|
# that only exists in memory fails silently, and the final save would then
|
|
# write the demo world over ReviewMap (this exact corruption happened once).
|
|
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])
|
|
# Optional 4th element: field of view in degrees. 0 keeps the default 90.
|
|
# Elevations shot from across the street use a long lens so the building
|
|
# fills the frame without pitching the camera (archviz: straight
|
|
# verticals, telephoto for elevations).
|
|
shots.set_editor_property(
|
|
"shot_field_of_view", [float(c[3]) if len(c) > 3 else 0.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)}")
|
|
|
|
ensure_writable(MAP_PATH)
|
|
saved = unreal.EditorLoadingAndSavingUtils.save_map(
|
|
unreal.EditorLevelLibrary.get_editor_world(), MAP_PATH)
|
|
log(f"save_map {MAP_PATH}: {saved}")
|
|
|
|
# --- 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. No switch back:
|
|
# the review map is already saved, and the run ends here.
|
|
switch_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.
|
|
# Level horizon for the same reason: a block elevation is read by its
|
|
# verticals, and a pitched camera makes them lean.
|
|
("demo-third-person-walk", unreal.Vector(-12940, -1280, 500), unreal.Vector(-13336, -1450, 220), 55.0),
|
|
# The sedan, staged on the carriageway at (-12950, -900). Three-quarter front
|
|
# view from the road so both the body and the wheels are visible: a side-on
|
|
# or rear view hides the wheel stances that the build is judged on.
|
|
("demo-vehicle", unreal.Vector(-12560, -2150, 420), unreal.Vector(-12950, -1450, 220), 45.0),
|
|
# Ground-level angles on the contact points. The wide shots cannot settle whether
|
|
# the feet and wheels touch the surface - that question needs the camera near the
|
|
# ground, looking along it, where a gap is a visible sliver of surface.
|
|
("demo-feet", unreal.Vector(-13140, -1280, 200), unreal.Vector(-13336, -1450, 170), 40.0),
|
|
("demo-wheel", unreal.Vector(-12380, -1050, 300), unreal.Vector(-12950, -1450, 175), 55.0),
|
|
("demo-spawn-view", unreal.Vector(-13336, -3100, 620), unreal.Vector(-13336, 1300, 620), 48.0),
|
|
# Street level, from the north sidewalk: the old camera sat at Y -400,
|
|
# inside the facade body (which spans Y -1457..-200), so it looked at
|
|
# the unlit back face. Y -2600 is on the sidewalk in front of the lit
|
|
# elevation.
|
|
("demo-street-level", unreal.Vector(-13420, -1650, 170), unreal.Vector(-13336, 1300, 170)),
|
|
# The designated mid-block crossing. Street arc length runs from the
|
|
# west end (s=0 at X -27966 cm), so s=130.1 m is UE X -14956, not the
|
|
# -24291 the old camera used (that is the s=36.6 crossing). Camera sits
|
|
# on the north sidewalk looking along the crosswalk itself, which is
|
|
# the pedestrian's view and shows the zebra bars as bars.
|
|
("demo-crossing", unreal.Vector(-15250, -2500, 400), unreal.Vector(-14956, 400, 150)),
|
|
# 0.1.6 retail frontage: 6800 Lee Drug (X -3340..-1417, face Y +1467)
|
|
# and 6806-6820 (X -6618..-3297, face Y +1252). Stand on the north
|
|
# sidewalk at 2 m, look south with a level horizon so the parapets and
|
|
# the cornice line read as horizontals. 68 deg covers both frontages at
|
|
# this distance; the ends crop rather than the elevation leaning.
|
|
("demo-retail-frontage", unreal.Vector(-4700, -2600, 210), unreal.Vector(-4700, 1400, 620), 66.0),
|
|
# the two north-side background buildings (TCL, Tussaud's) seen from
|
|
# the south across Hollywood Blvd. Probed world-space (2026-11-08):
|
|
# TCL bbox X -22889..-18442, Y -9431..-1802; Tussaud's X -27071..
|
|
# -22799, Y -7586..-1888. +Y = south, -Y = north (axis mapping). So
|
|
# stand SOUTH of the road (Y +1600) at the combined mass centre
|
|
# (X -22750) and look NORTH toward the mass. The old camera was at
|
|
# X -22800 Y +2600 aiming at its own X, giving an 85%-sky frame.
|
|
("demo-north-buildings", unreal.Vector(-22750, 1600, 500), unreal.Vector(-22750, -5200, 350)),
|
|
# 0.2.0 third-person body. Stands where the pawn's own boom camera sits —
|
|
# 3.4 m behind the spawn point, 1 m above it — and looks at the pawn, so
|
|
# the visible body and its idle clip are what the shot is for.
|
|
# Demo_PlayerStart is at (-13336, -1150, 600), yaw 90 (facing +Y).
|
|
("demo-third-person", unreal.Vector(-13336, -2050, 620), unreal.Vector(-13336, -1450, 250), 58.0),
|
|
# Front view of the same pawn: the third-person shot only ever shows its
|
|
# back, so a pose defect there is invisible. The pawn spawns facing +Y
|
|
# (yaw 90), so the camera stands on the road side and looks back north.
|
|
("demo-third-person-front", unreal.Vector(-13336, -820, 620), unreal.Vector(-13336, -1450, 240), 55.0),
|
|
|
|
]
|
|
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])
|
|
# No follow shots in the current list: the stride capture was retired
|
|
# because the walk clip does not visibly advance in game (see TODO.md).
|
|
# The follow flag stays wired for when it does.
|
|
seq.set_editor_property("shot_follow", [True] + [False] * (len(demo_shots) - 1))
|
|
seq.set_editor_property(
|
|
"shot_field_of_view",
|
|
[float(c[3]) if len(c) > 3 else 0.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}")
|
|
else:
|
|
log("MISSING: /Game/Hollywood/DemoMap (run Tools/unreal/ue_build_demo_map.py)")
|
|
except Exception as exc: # noqa
|
|
log(f"staging failed: {exc!r}")
|
|
|
|
with open(RESULT, "w") as f:
|
|
f.write("\n".join(lines) + "\n")
|