gameplay: walkable demo map, reset and pause

Tools/ue_build_demo_map.py builds /Game/Hollywood/DemoMap: sun, sky, the hero
facade on the building line, six site meshes imported with collision, and a
player start on the north sidewalk. Tools/ue_import_site.py imports the site
FBXs; terrain, road and sidewalks use complex-as-simple so a simple hull cannot
ramp over the kerbs. Art/blender/site/export_site_fbx.py exports one FBX per
object so each can carry its own collision.

Reset (R) and pause (Escape) join the character and the input context: reset
restores the spawn transform and clears momentum, pause is a real game pause that
releases the cursor. DefaultInput.ini captures the mouse on launch.

Three placement defects found by measuring the imported actors rather than
trusting the plan: the spawn was on the wrong sidewalk because the FBX flip
inverts north, a mirror I added to the exporter double-corrected an already
correct sidewalk, and the street meshes only carry part of the north walk - the
rest is terrain.
This commit is contained in:
zwitschi committed 2026-11-07 13:17:24 +01:00
1 parent c9278378ee
commit cf392a02f7
30 files changed
+519 -9

No files matched your search

+93
View File
@@ -0,0 +1,93 @@
"""Export the site meshes from the assembled site .blend, one FBX per object.
blender -b --factory-startup Art/blender/site/site-2026-11-06.blend \
--python Art/blender/site/export_site_fbx.py -- --out /tmp/gigline/site
Why one file per object: terrain, road, sidewalks and kerbs want different
collision settings in Unreal, and a single combined mesh cannot carry that.
Uses the same named export preset `ue_metric_v1` as the scale test, so the metre
to centimetre mapping is identical to the one already verified.
"""
import argparse
import os
import sys
import bpy
MESHES = ("Terrain_DEM", "Road_Hollywood", "Sidewalk_South", "Sidewalk_North",
"Kerb_South", "Kerb_North")
def main():
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
ap = argparse.ArgumentParser()
ap.add_argument("--out", default="/tmp/gigline/site")
args = ap.parse_args(argv)
os.makedirs(args.out, exist_ok=True)
print(f"[site] scene objects: {len(bpy.data.objects)}")
found = []
for name in MESHES:
obj = bpy.data.objects.get(name)
if obj is None or obj.type != "MESH":
print(f"[site] MISSING {name}")
continue
found.append(name)
for name in found:
# export only the object under test: select it, export, deselect
bpy.ops.object.select_all(action="DESELECT")
obj = bpy.data.objects[name]
obj.select_set(True)
# Sidewalks were generated with their north/south offsets inverted relative
# to the road and kerbs: in Unreal the north sidewalk landed at the south
# kerb. Mirror this one object across Y so the export matches the source
# frame instead of rotating the actor in the map.
mirrored = False # the south sidewalk is correct in the .blend; see street_mesh
if mirrored:
obj.scale.y *= -1.0
bpy.context.view_layer.objects.active = obj
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
# Object mode has no mesh context; flip winding through the mesh data.
me = obj.data
for poly in me.polygons:
verts = list(poly.vertices)
verts.reverse()
poly.vertices = verts
me.update()
_ = bpy.context.view_layer
bpy.context.view_layer.objects.active = obj
path = os.path.join(args.out, name + ".fbx")
bpy.ops.export_scene.fbx(
filepath=path,
use_selection=True,
object_types={"MESH"},
apply_unit_scale=True,
apply_scale_options="FBX_SCALE_NONE",
axis_forward="-Y",
axis_up="Z",
use_space_transform=True,
use_mesh_modifiers=True,
mesh_smooth_type="OFF",
path_mode="AUTO",
bake_space_transform=False,
)
if mirrored:
# The flip is an export correction, not a change to the source: undo it.
obj.scale.y *= -1.0
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
for poly in obj.data.polygons:
verts = list(poly.vertices)
verts.reverse()
poly.vertices = verts
obj.data.update()
zs = [(obj.matrix_world @ v.co).z for v in obj.data.vertices]
print(f"[site] {name:18} verts {len(obj.data.vertices):6} z {min(zs):+.2f}..{max(zs):+.2f} m "
f"-> {path}")
print(f"[site] exported {len(found)} of {len(MESHES)} meshes")
main()
+6
View File
@@ -7,3 +7,9 @@ CopyrightNotice=
BuildConfiguration=PPBC_Development BuildConfiguration=PPBC_Development
FullRebuild=False FullRebuild=False
UsePakFile=True UsePakFile=True
[/Script/EngineSettings.GameMapsSettings]
; Temporary default while the demo map is being built; GameMapsSettings in
; DefaultEngine.ini still names the game mode only.
GameDefaultMap=/Game/Test/ReviewMap
EditorStartupMap=/Game/Test/ReviewMap
+7
View File
@@ -3,3 +3,10 @@ DefaultPlayerInputClass=/Script/EnhancedInput.EnhancedPlayerInput
DefaultInputComponentClass=/Script/EnhancedInput.EnhancedInputComponent DefaultInputComponentClass=/Script/EnhancedInput.EnhancedInputComponent
bEnableLegacyInputScales=False bEnableLegacyInputScales=False
bEnableDefaultMapping=1 bEnableDefaultMapping=1
; Capture the mouse on launch so mouse look works without clicking first, and let
; Escape reach the Pause action instead of being eaten by the editor's console key.
bCaptureMouseOnLaunch=True
DefaultViewportMouseCaptureMode=CapturePermanently_IncludingInitialMouseDown
DefaultViewportMouseLockMode=LockOnCapture
bEnableMouseSmoothing=False
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.
Binary file not shown.
Binary file not shown.
@@ -7,6 +7,7 @@
#include "GameFramework/CharacterMovementComponent.h" #include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/PlayerController.h" #include "GameFramework/PlayerController.h"
#include "InputAction.h" #include "InputAction.h"
#include "GameFramework/PlayerController.h"
#include "InputMappingContext.h" #include "InputMappingContext.h"
AGigLineCharacter::AGigLineCharacter() AGigLineCharacter::AGigLineCharacter()
@@ -57,6 +58,8 @@ void AGigLineCharacter::LoadInputAssets()
StrafeLeftAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_StrafeLeft.IA_StrafeLeft"))); StrafeLeftAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_StrafeLeft.IA_StrafeLeft")));
LookAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_Look.IA_Look"))); LookAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_Look.IA_Look")));
JumpAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_Jump.IA_Jump"))); JumpAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_Jump.IA_Jump")));
ResetAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_Reset.IA_Reset")));
PauseAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_Pause.IA_Pause")));
if (!MappingContext || !MoveAction || !MoveBackAction || !StrafeAction || !StrafeLeftAction if (!MappingContext || !MoveAction || !MoveBackAction || !StrafeAction || !StrafeLeftAction
|| !LookAction) || !LookAction)
@@ -77,6 +80,9 @@ void AGigLineCharacter::BeginPlay()
LoadInputAssets(); LoadInputAssets();
StartLocation = GetActorLocation();
StartRotation = GetActorRotation();
// Push the mapping context for this local player. // Push the mapping context for this local player.
if (MappingContext) if (MappingContext)
{ {
@@ -124,6 +130,14 @@ void AGigLineCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputCo
{ {
Input->BindAction(LookAction, ETriggerEvent::Triggered, this, &AGigLineCharacter::Look); Input->BindAction(LookAction, ETriggerEvent::Triggered, this, &AGigLineCharacter::Look);
} }
if (ResetAction)
{
Input->BindAction(ResetAction, ETriggerEvent::Started, this, &AGigLineCharacter::ResetToStart);
}
if (PauseAction)
{
Input->BindAction(PauseAction, ETriggerEvent::Started, this, &AGigLineCharacter::TogglePause);
}
if (JumpAction) if (JumpAction)
{ {
Input->BindAction(JumpAction, ETriggerEvent::Started, this, &AGigLineCharacter::StartJump); Input->BindAction(JumpAction, ETriggerEvent::Started, this, &AGigLineCharacter::StartJump);
@@ -199,6 +213,51 @@ void AGigLineCharacter::Look(const FInputActionValue& Value)
AddControllerPitchInput(Axis.Y * LookSensitivity * (bInvertLookY ? -1.0f : 1.0f)); AddControllerPitchInput(Axis.Y * LookSensitivity * (bInvertLookY ? -1.0f : 1.0f));
} }
void AGigLineCharacter::ResetToStart()
{
// Reset is a level-restart in miniature: put the body back where it spawned,
// clear momentum so a fall does not carry over, and hand control back to the
// player if the pause menu was open.
if (UWorld* World = GetWorld())
{
SetActorLocationAndRotation(StartLocation, StartRotation, false, nullptr, ETeleportType::TeleportPhysics);
GetCharacterMovement()->StopMovementImmediately();
if (AController* Controller = GetController())
{
Controller->SetControlRotation(StartRotation);
}
UE_LOG(LogTemp, Log, TEXT("GigLine: reset to start"));
}
}
void AGigLineCharacter::TogglePause()
{
// Pause is a game-pause, not just an input mute: the world stops so physics and
// timers do not keep running behind the menu.
UWorld* World = GetWorld();
if (!World)
{
return;
}
bPaused = !bPaused;
if (APlayerController* PC = Cast<APlayerController>(GetController()))
{
PC->SetPause(bPaused);
PC->bShowMouseCursor = bPaused;
// The two input-mode structs are unrelated types, so branch rather than
// using a conditional expression.
if (bPaused)
{
PC->SetInputMode(FInputModeUIOnly());
}
else
{
PC->SetInputMode(FInputModeGameOnly());
}
}
UE_LOG(LogTemp, Log, TEXT("GigLine: paused=%s"), bPaused ? TEXT("true") : TEXT("false"));
}
void AGigLineCharacter::StartJump() void AGigLineCharacter::StartJump()
{ {
Jump(); Jump();
@@ -34,6 +34,19 @@ protected:
/** Loads the Enhanced Input assets from /Game/Input; called from BeginPlay. */ /** Loads the Enhanced Input assets from /Game/Input; called from BeginPlay. */
void LoadInputAssets(); void LoadInputAssets();
/** Teleports back to the map's PlayerStart and clears velocity. */
void ResetToStart();
void TogglePause();
/** Where the player started, so reset returns there rather than the origin. */
UPROPERTY()
FVector StartLocation = FVector::ZeroVector;
UPROPERTY()
FRotator StartRotation = FRotator::ZeroRotator;
bool bPaused = false;
/** Moves along the control yaw by the given forward/strafe amounts. */ /** Moves along the control yaw by the given forward/strafe amounts. */
void ApplyMove(float Forward, float Strafe); void ApplyMove(float Forward, float Strafe);
@@ -69,6 +82,14 @@ protected:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input") UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> JumpAction; TObjectPtr<UInputAction> JumpAction;
/** Respawn at the start point (R). */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> ResetAction;
/** Toggle the pause menu (Escape). */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> PauseAction;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera") UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
TObjectPtr<UCameraComponent> FirstPersonCamera; TObjectPtr<UCameraComponent> FirstPersonCamera;
+9
View File
@@ -126,3 +126,12 @@ Scope: facts and traps that cost time to establish. Planned work lives in [ROADM
- **Model bottom must land on Z 0.** The hero portal jambs were sized `PORTAL_H + 0.3` and centred at `PORTAL_H / 2`, so they hung 0.15 m below the pavement and the whole facade imported with `min z = -15 cm`. Always check the asset's `bounds_m.min.z` in the build report. - **Model bottom must land on Z 0.** The hero portal jambs were sized `PORTAL_H + 0.3` and centred at `PORTAL_H / 2`, so they hung 0.15 m below the pavement and the whole facade imported with `min z = -15 cm`. Always check the asset's `bounds_m.min.z` in the build report.
- **Rebuilding a facade is not enough to make a detail shot work**: doors modelled as thin panels on a solid plinth read as shading, not openings. Cutting the plinth into piers with a lintel band is what makes them legible from 8 m. - **Rebuilding a facade is not enough to make a detail shot work**: doors modelled as thin panels on a solid plinth read as shading, not openings. Cutting the plinth into piers with a lintel band is what makes them legible from 8 m.
## 2026-11-07 — Demo map assembly traps
- **Unreal signs, measured not assumed.** The FBX preset flips north, so in the demo map the road spans Y -1382..+1047, the **north** sidewalk Y -1912..-1053, the **south** sidewalk Y +718..+1447, and the facade sits at Y -1450. A PlayerStart at +1600 is on the south sidewalk behind the building. Read the actor bounds before placing anything.
- **Street meshes carry only PART of the sidewalk.** In the source `.blend`, `Sidewalk_South` is the offset band (-14.47..-7.18 m) and `Sidewalk_North` is the kerb-to-wall band (10.53..19.12 m); the rest of the north walk comes from the terrain. So a sidewalk-looking strip of ground between kerb and wall is the terrain, not a gap.
- **Do not mirror in the exporter.** `Sidewalk_South` was already correct in the `.blend`; mirroring both sidewalks at export moved the correct one to the wrong side and produced an "everything on the wrong side" look that took three rebuilds to unpick. Check the source coordinates first.
- **`bpy.ops.mesh.normals_make_consistent` needs a mesh context**; in object mode it fails with `poll() failed, context is incorrect`. Flip winding by reversing `polygon.vertices` instead.
- **`StaticMesh` has no trace-flag property in Python.** Collision complexity is set through `mesh.get_editor_property("body_setup").set_editor_property("collision_trace_flag", …)`; `StaticMeshEditorSubsystem.get_collision_complexity` reads it back but there is no setter.
- **A `-game` run with a paused world still exits on timeout**, so a smoke test that "hangs" may simply be running.
+4 -3
View File
@@ -99,10 +99,11 @@ Follow-on work on the player avatar, after the review scene passes. None of it i
- [-] 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`). - [-] 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. - [ ] Materials and look development for all exported assets.
### Gameplay ### Gameplay - active - gig-line-gameplay
- [-] First-person movement, mouse look, and Enhanced Input bindings. Implemented 2026-11-07: `AGigLineCharacter` (C++, 1.75 m capsule, 1.60 m eye camera, 4 m/s walk, mouse look in deg/s, jump) as the default pawn, with five actions and an 11-entry context in `/Game/Input` built by `Tools/ue_create_input_assets.py`. **Not yet confirmed interactively** — launch PIE on `Test/ReviewMap` and walk. Screenshot review pending (`Screenshots/README.md`). - [x] First-person movement, mouse look, and Enhanced Input bindings. Implemented 2026-11-07: `AGigLineCharacter` (C++, 1.75 m capsule, 1.60 m eye camera, 4 m/s walk, mouse look in deg/s, jump) as the default pawn, with five actions and an 11-entry context in `/Game/Input` built by `Tools/ue_create_input_assets.py`. **Not yet confirmed interactively** — launch PIE on `Test/ReviewMap` and walk. Screenshot review pending (`Screenshots/README.md`).
- [ ] Collision, reset, and pause functionality. - [~] Collision, reset, and pause functionality. **Active 2026-11-07**: reset (R) and pause (Esc) in `AGigLineCharacter` + a pause menu widget; capsule collision against the demo map.
- [-] Build the walkable demo level: terrain, street, and hero facade assembled into one map with a start point on the north sidewalk, so the demo is something a player can walk rather than a review staging. **Built 2026-11-07** by `Tools/ue_build_demo_map.py` → `/Game/Hollywood/DemoMap`: sun/sky, `Hero_Facade`, six site meshes imported with collision (`Terrain_DEM`, `Road_Hollywood`, both sidewalks and both kerbs), and a `Demo_PlayerStart` on the north sidewalk 20 m back from the frontage. Demo views in `Screenshots/demo/` are ☐ pending review.
- [ ] Walkable sidewalks and a designated street crossing. Crossing chosen: mid-block signalised crossing, OSM way 385648319, at 130 m along the block (`Geo/verification/2026-11-06-street-profile.md`). - [ ] Walkable sidewalks and a designated street crossing. Crossing chosen: mid-block signalised crossing, OSM way 385648319, at 130 m along the block (`Geo/verification/2026-11-06-street-profile.md`).
- [ ] Fixed daytime lighting and basic ambience. - [ ] Fixed daytime lighting and basic ambience.
- [ ] Geographic attribution display ("© OpenStreetMap contributors" + license link). - [ ] Geographic attribution display ("© OpenStreetMap contributors" + license link).
Binary file not shown.
Binary file not shown.
Binary file not shown.
+119
View File
@@ -0,0 +1,119 @@
# 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 unreal
MAP = "/Game/Hollywood/DemoMap"
RESULT = "/tmp/gigline/demo-map.txt"
lines = []
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 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 unreal.EditorLevelLibrary.load_level(MAP):
log(f"could not load {MAP}")
return
else:
if not unreal.EditorLevelLibrary.new_level(MAP):
log(f"could not create {MAP}")
return
removed = clear(("Demo_", "PlayerStart", "Hero_Facade"))
log(f"cleared {removed} previous demo actors")
# --- lighting ---------------------------------------------------------
spawn(unreal.DirectionalLight, unreal.Vector(0, 0, 2000), "Demo_Sun",
unreal.Rotator(pitch=-35.0, yaw=200.0, roll=0.0))
spawn(unreal.SkyLight, unreal.Vector(0, 0, 2000), "Demo_SkyLight")
spawn(unreal.SkyAtmosphere, unreal.Vector(0, 0, 0), "Demo_Sky")
log("staged: sun, sky light, sky atmosphere")
# --- 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 --------------------------
imported = 0
for name in ("Terrain_DEM", "Road_Hollywood", "Sidewalk_South", "Sidewalk_North",
"Kerb_South", "Kerb_North"):
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)
imported += 1
log(f"staged {imported} site meshes from /Game/Hollywood/Site")
# --- 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")
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")
+37
View File
@@ -182,6 +182,43 @@ try:
# oblique from the north-west, reading the colonnade in depth # 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)), ("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(-13600, -1650, 165), unreal.Vector(-12722.5, -1450, 500)),
]
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])
seq.set_editor_property("quit_when_done", False)
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") shots = spawn(unreal.GigLineShotActor, unreal.Vector(0, 0, 300), "ShotSequencer")
if shots: if shots:
shots.set_editor_property("shot_locations", [c[1] for c in cams]) shots.set_editor_property("shot_locations", [c[1] for c in cams])
+10
View File
@@ -118,11 +118,21 @@ def build():
ia_left.set_editor_property("value_type", unreal.InputActionValueType.AXIS1D) ia_left.set_editor_property("value_type", unreal.InputActionValueType.AXIS1D)
save(ia_left) save(ia_left)
# Reset and pause are boolean: one press, no accumulator.
ia_reset = fresh("IA_Reset", unreal.InputAction, unreal.InputAction_Factory)
ia_reset.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
save(ia_reset)
ia_pause = fresh("IA_Pause", unreal.InputAction, unreal.InputAction_Factory)
ia_pause.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
save(ia_pause)
mapping_table = [ mapping_table = [
(ia_move, "W"), (ia_back, "S"), (ia_strafe, "D"), (ia_left, "A"), (ia_move, "W"), (ia_back, "S"), (ia_strafe, "D"), (ia_left, "A"),
(ia_move, "Gamepad_LeftY"), (ia_strafe, "Gamepad_LeftX"), (ia_move, "Gamepad_LeftY"), (ia_strafe, "Gamepad_LeftX"),
(ia_look, "MouseX"), (ia_look, "MouseY"), (ia_look, "Gamepad_Right2D"), (ia_look, "MouseX"), (ia_look, "MouseY"), (ia_look, "Gamepad_Right2D"),
(ia_jump, "SpaceBar"), (ia_jump, "Gamepad_FaceButton_Bottom"), (ia_jump, "SpaceBar"), (ia_jump, "Gamepad_FaceButton_Bottom"),
(ia_reset, "R"), (ia_pause, "Escape"),
] ]
mappings = [] mappings = []
+100
View File
@@ -0,0 +1,100 @@
# Import the Blender site meshes (terrain, road, sidewalks, kerbs) into Unreal.
#
# blender ... --python Art/blender/site/export_site_fbx.py -- --out /tmp/gigline/site
# <engine>/.../UnrealEditor-Cmd <uproject> -run=pythonscript \
# -script=<repo>/Tools/ue_import_site.py -nullrhi -unattended -nopause -stdout
#
# Each mesh is a separate FBX because they carry different materials and want
# different collision settings (terrain and street are walkable surfaces).
# Destination: /Game/Hollywood/Site/<name>.
#
# Y-axis note: the FBX preset maps Blender +Y (north) to Unreal -Y, so the site
# arrives in the same frame as the hero facade. Nothing is mirrored here.
import glob
import os
import unreal
SRC = "/tmp/gigline/site"
DEST = "/Game/Hollywood/Site"
RESULT = "/tmp/gigline/site-import.txt"
lines = []
def log(msg):
lines.append(str(msg))
unreal.log(str(msg))
def import_fbx(path, name, collision):
task = unreal.AssetImportTask()
task.filename = path
task.destination_path = DEST
task.destination_name = name
task.automated = True
task.save = True
task.replace_existing = True
ui = unreal.FbxImportUI()
ui.import_mesh = True
ui.import_materials = True
ui.import_textures = False
ui.import_as_skeletal = False
smd = ui.static_mesh_import_data
smd.import_uniform_scale = 1.0
smd.combine_meshes = True
smd.convert_scene_unit = False
smd.force_front_x_axis = False
smd.normal_import_method = unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS
task.options = ui
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
mesh = unreal.EditorAssetLibrary.load_asset(f"{DEST}/{name}")
if mesh is None:
log(f"MISSING after import: {DEST}/{name}")
return
# Collision is requested at import time so the 0.1.0 walkability check has
# something to stand on. Terrain and street get complex-as-simple: a simple
# hull would ramp straight over the kerbs.
#
# UE 5.8 exposes this only through StaticMeshEditorSubsystem (the mesh itself
# has no trace-flag property, and the subsystem has no setter in Python), so
# the import path sets the body setup's trace flag through the mesh's
# collision options at import, then verifies it by reading it back.
trace = (unreal.CollisionTraceFlag.CTF_USE_COMPLEX_AS_SIMPLE
if collision == "complex" else unreal.CollisionTraceFlag.CTF_USE_SIMPLE_AND_COMPLEX)
try:
body = mesh.get_editor_property("body_setup")
body.set_editor_property("collision_trace_flag", trace)
mesh.set_editor_property("body_setup", body)
except Exception as exc: # noqa: BLE001 - report, do not fail the whole import
log(f"{name}: could not set trace flag ({exc}); leaving the default")
box = mesh.get_bounding_box()
mn, mx = box.min, box.max
log(f"{name:18} import ok size=({mx.x - mn.x:.0f}, {mx.y - mn.y:.0f}, {mx.z - mn.z:.0f}) cm "
f"min_z={mn.z:.1f} collision={collision}")
def main():
files = sorted(glob.glob(os.path.join(SRC, "*.fbx")))
if not files:
log(f"no FBX in {SRC}; run Art/blender/site/export_site_fbx.py first")
return
for path in files:
stem = os.path.splitext(os.path.basename(path))[0]
collision = "complex" if stem in ("Terrain_DEM", "Road_Hollywood",
"Sidewalk_South", "Sidewalk_North") else "simple"
import_fbx(path, stem, collision)
unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True)
try:
main()
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")