diff --git a/.gitignore b/.gitignore index dac8763..1a84a57 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,8 @@ Thumbs.db # Unreal per-project build artefacts (FileOpenOrder logs and the like) Build/ + +# Rendered screenshots for visual review — generated locally, never committed. +# The review README stays tracked; the PNGs do not. +/Screenshots/* +!/Screenshots/README.md diff --git a/AGENTS.md b/AGENTS.md index fdb7c51..c2ac8aa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -77,6 +77,17 @@ Always use a consistent scale test, named export presets, and repeatable import - Keep restricted assets out of the public repository; prefer redistributable replacements. - Review Epic distribution requirements before publishing builds. +## Visual review (screenshots) + +Any change that affects the game's appearance — meshes, materials, lighting, levels, character, camera, post-processing — must be rendered for human review: + +1. Run `Tools/render_screenshots.sh` (rebuilds `Game/GigLine/Content/Test/ReviewMap.umap` from current content, runs the game on it, captures the fixed camera list, copies the PNGs into `Screenshots/`). +2. Update `Screenshots/README.md`: one row per screenshot with what it shows and an approval state; describe any new shots or scene changes in the same change. +3. The PNGs are **never committed** — `.gitignore` keeps only `Screenshots/README.md`. Review happens on disk. +4. Wait for the user's approval before treating a visual change as done; record approval by ticking the table in `Screenshots/README.md`. + +New content that needs a different camera angle gets a new entry in the shot list in `Tools/ue_build_review_map.py`. + ## Roadmap The living roadmap with per-version task checklists is in **[ROADMAP.md](ROADMAP.md)**. diff --git a/Game/GigLine/Content/Test/M_ReviewFloor.uasset b/Game/GigLine/Content/Test/M_ReviewFloor.uasset new file mode 100644 index 0000000..fae2e7c --- /dev/null +++ b/Game/GigLine/Content/Test/M_ReviewFloor.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2bf64131055c5dda897d2b12a5d76cb7839374a6ea1d7705e4f35c614b826fd0 +size 4605 diff --git a/Game/GigLine/Content/Test/ReviewMap.umap b/Game/GigLine/Content/Test/ReviewMap.umap new file mode 100644 index 0000000..c58db03 --- /dev/null +++ b/Game/GigLine/Content/Test/ReviewMap.umap @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6dd7e36945ccaa392f9b87db4acca5fb97525c75be92c8e071f6d5e712abd7c +size 22501 diff --git a/Game/GigLine/Source/GigLine/GigLineShotActor.cpp b/Game/GigLine/Source/GigLine/GigLineShotActor.cpp new file mode 100644 index 0000000..ae492cc --- /dev/null +++ b/Game/GigLine/Source/GigLine/GigLineShotActor.cpp @@ -0,0 +1,78 @@ +#include "GigLineShotActor.h" + +#include "Camera/CameraActor.h" +#include "Engine/World.h" +#include "GameFramework/PlayerController.h" +#include "UnrealClient.h" + +AGigLineShotActor::AGigLineShotActor() +{ + PrimaryActorTick.bCanEverTick = true; + PrimaryActorTick.bStartWithTickEnabled = true; +} + +void AGigLineShotActor::BeginPlay() +{ + Super::BeginPlay(); + NextShot(); +} + +void AGigLineShotActor::Tick(float DeltaSeconds) +{ + Super::Tick(DeltaSeconds); + + if (bWaitingForSettle) + { + SettleCountdown -= DeltaSeconds; + if (SettleCountdown <= 0.f) + { + bWaitingForSettle = false; + const FString FileName = FString::Printf(TEXT("review-%02d-%s"), CurrentShot, + CurrentShot < ShotNames.Num() ? *ShotNames[CurrentShot] : *FString::FromInt(CurrentShot)); + // Resolution is the game window's; the runner passes -ResX/-ResY. + FScreenshotRequest::RequestScreenshot(FileName, false, false); + bWaitingForScreenshot = true; + } + return; + } + + if (bWaitingForScreenshot && !FScreenshotRequest::IsScreenshotRequested()) + { + bWaitingForScreenshot = false; + UE_LOG(LogTemp, Log, TEXT("GigLineShotActor: captured %s"), *FScreenshotRequest::GetFilename()); + ++CurrentShot; + NextShot(); + } +} + +void AGigLineShotActor::NextShot() +{ + if (!PlayerController) + { + PlayerController = GetWorld()->GetFirstPlayerController(); + } + + if (CurrentShot >= ShotLocations.Num() || !PlayerController) + { + UE_LOG(LogTemp, Log, TEXT("GigLineShotActor: done (%d shots)"), CurrentShot); + if (bQuitWhenDone) + { + PlayerController->ConsoleCommand(TEXT("quit")); + } + return; + } + + if (CurrentCamera) + { + CurrentCamera->Destroy(); + } + + FActorSpawnParameters Params; + Params.ObjectFlags |= RF_Transient; + CurrentCamera = GetWorld()->SpawnActor( + ACameraActor::StaticClass(), ShotLocations[CurrentShot], ShotRotations[CurrentShot], Params); + PlayerController->SetViewTarget(CurrentCamera); + + SettleCountdown = SettleSeconds; + bWaitingForSettle = true; +} diff --git a/Game/GigLine/Source/GigLine/GigLineShotActor.h b/Game/GigLine/Source/GigLine/GigLineShotActor.h new file mode 100644 index 0000000..9aa0777 --- /dev/null +++ b/Game/GigLine/Source/GigLine/GigLineShotActor.h @@ -0,0 +1,60 @@ +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "GigLineShotActor.generated.h" + +/** + * Dev-only helper for the visual-review screenshot pipeline. + * + * Placed in the review map by Tools/ue_build_review_map.py. In -game mode it + * walks a fixed list of camera setups, sets the view target to a spawned + * CameraActor for each, requests a screenshot via FScreenshotRequest, and + * quits the game when all shots are written. Lives in test content only; + * the shipped game never references this map. + */ +UCLASS() +class GIGLINE_API AGigLineShotActor : public AActor +{ + GENERATED_BODY() + +public: + AGigLineShotActor(); + + virtual void BeginPlay() override; + virtual void Tick(float DeltaSeconds) override; + + /** World-space locations for each shot camera. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite) + TArray ShotLocations; + + /** Rotations for each shot camera (same index as ShotLocations). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite) + TArray ShotRotations; + + /** File name fragment for each shot (same index as ShotLocations). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite) + TArray ShotNames; + + /** Seconds to let the view target settle before the capture. Resolution comes from the runner's -ResX/-ResY. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite) + float SettleSeconds = 0.35f; + + /** Quit the game after the last shot (disable to keep the game open). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite) + bool bQuitWhenDone = true; + +private: + void NextShot(); + + UPROPERTY() + TObjectPtr PlayerController; + + UPROPERTY() + TObjectPtr CurrentCamera; + + int32 CurrentShot = 0; + float SettleCountdown = 0.f; + bool bWaitingForSettle = false; + bool bWaitingForScreenshot = false; +}; diff --git a/MEMORY.md b/MEMORY.md index d26af9d..efab7f1 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -71,3 +71,9 @@ Scope: facts and traps that cost time to establish. Planned work lives in [ROADM - **Two sessions edit this repo at once.** The other session created the Unreal project and the blank package while this one was documenting. Check `git log` and `git status` before committing, and never `git add -A` blindly: it once swept generated `Game/GigLine/Build/**` logs into a commit. Stages the source files deliberately. - **`Build/` under a project is generated**, not source — `FileOpenOrder/*.log` and packaging output. `.gitignore` now covers it alongside `Binaries/`, `Intermediate/`, `Saved/`, `DerivedDataCache/`. - **Scope boundary for `0.1.0`:** do not tag `v0.1.0` without an explicit go-ahead from the owner. It is a minor release and carries the walking/driving demo promise; the board's release checks decide when it is ready, not the pace of work. + +## 2026-11-07 — Screenshot pipeline + +- **`-executePythonScript` + editor is unreliable for captures**: the editor quits right after a *successful* deferred script (async screenshot requests never render), and after `load_map` in a commandlet every `spawn_actor` call returns None — build scenes in the default world instead. +- **Robust in-game capture recipe**: commandlet builds a map (`ue_build_review_map.py`), then the game runs in `-game` mode and a tiny C++ actor (`AGigLineShotActor`) walks camera setups via `SetViewTarget` + `FScreenshotRequest::RequestScreenshot`. In 5.8 `FScreenshotRequest` has no resolution argument (window size via `-ResX/-ResY`) and no `IsFinished()` — poll `!IsScreenshotRequested()`. Lives in `UnrealClient.h`, not `Misc/ScreenshotManager.h`. +- **Commandlet-only quirks**: bulk render-target readback (`read_render_target_raw`, `export_render_target`) silently returns empty in commandlets; single-pixel `read_render_target_uv` works. Game targets must use `BuildSettingsVersion.V7`. diff --git a/Screenshots/README.md b/Screenshots/README.md new file mode 100644 index 0000000..2547db9 --- /dev/null +++ b/Screenshots/README.md @@ -0,0 +1,56 @@ +# Screenshots — visual review + +Rendered in-game captures of the current project content, generated by +[`Tools/render_screenshots.sh`](../Tools/render_screenshots.sh). **The PNGs in +this folder are never committed** (`.gitignore` excludes everything here +except this README) — they exist on disk so a human can review the game's +appearance after every change that could affect it. + +## How to regenerate + +```bash +Tools/render_screenshots.sh +``` + +This rebuilds `Game/GigLine/Content/Test/ReviewMap.umap` from the current +content (floor, key light, sky, MPFB character, the three 1 m scale cubes), +runs the game on it in `-game` mode, lets `AGigLineShotActor` walk its fixed +camera list and capture, and copies the PNGs here. Takes about a minute. + +Each screenshot is described below. After reviewing, mark the approval state +in the table so agents know whether the look is accepted or needs work. + +## Current screenshots (generated 2026-11-07) + +| File | View | Shows | Approval | +| --- | --- | --- | --- | +| `review-00-overview.png` | Wide shot from the south-west, elevated | The review scene: character at origin, three 1 m calibration cubes in a row at 4 m north, gray floor, key light + sky | ☐ pending | +| `review-01-character.png` | Medium shot, eye height, from the south | The MPFB-generated character with its baked skin material and A-pose bind | ☐ pending | +| `review-02-scale-cubes.png` | Close three-quarter view of the cube row | The 1 m cubes for scale judgement against the character | ☐ pending | + +All captures are 2560×1440 PNG (the game window's size), unedited. + +## What to look for + +- **Character**: proportions, bind pose, skin material readability. +- **Scale cubes**: each is exactly 1 m — use them to sanity-check anything + whose size looks off. +- **Lighting**: one warm key light and fill sky; this is the temporary review + rig, not the `0.1.0` daytime look. + +## Known cosmetic issues in the review scene + +- The gray floor material renders with a speckled pattern instead of flat + gray — suspect the `Constant3Vector` wiring in `Tools/ue_build_review_map.py`. + Cosmetic only; the floor exists so shadows and scale read correctly. +- The horizon shows a black band below the sky atmosphere (no distant ground + plane). Expected in the empty review scene. + +## Conventions + +- `review-NN-name.png` names are produced by `AGigLineShotActor` + (`Game/GigLine/Source/GigLine/GigLineShotActor.h`); the camera list lives in + `Tools/ue_build_review_map.py`. +- Add or adjust shots there when new content needs review, then update this + README's table in the same change. +- Agents: see the *Visual review* section in [`AGENTS.md`](../AGENTS.md). diff --git a/Tools/README.md b/Tools/README.md index 64500a1..65e28c5 100644 --- a/Tools/README.md +++ b/Tools/README.md @@ -12,6 +12,7 @@ Repeatable import/export and processing helpers. Anything that was done twice by ## Helpers +- `render_screenshots.sh` — visual-review pipeline: rebuilds `/Game/Test/ReviewMap` from current content (`ue_build_review_map.py`), runs the game on it in `-game` mode, lets `AGigLineShotActor` (`Game/GigLine/Source/GigLine/`) capture the fixed camera list, and copies the PNGs into the gitignored `Screenshots/`. See `Screenshots/README.md`. - `check-lfs.sh` — repository guard: fails if `git-lfs` or the `lfs` filter is missing, or if an LFS-tracked file was committed as a plain blob. - `site_assembly.py` — assembles terrain + street + blockout into one file and raycasts the walking routes for holes and steps. - `street_mesh.py` — road, kerbs, and sidewalks on the DEM, in the project frame; kerbs flush at the three crossings. @@ -22,6 +23,5 @@ Repeatable import/export and processing helpers. Anything that was done twice by ## Planned helpers - Blender export preset runner (named presets, metric scale assertion) -- Blender→Unreal scale test (export a known-size cube, verify 100× conversion) - OSM extract fetch and manifest writer (records date, bounds, checksum) - QGIS processing steps as batch scripts, so the geo cleanup is reproducible rather than GUI-only diff --git a/Tools/render_screenshots.py b/Tools/render_screenshots.py new file mode 100644 index 0000000..24f1cd5 --- /dev/null +++ b/Tools/render_screenshots.py @@ -0,0 +1,136 @@ +# UE-side screenshot renderer for visual review (commandlet, real RHI). +# +# Builds a small review scene in the default world (floor, sky, key light, +# MPFB character, the three scale-test cubes) and renders three fixed camera +# angles synchronously via SceneCapture2D render targets, writing PNGs +# directly into /Screenshots/ (gitignored). +# +# Invoked by Tools/render_screenshots.sh (UnrealEditor-Cmd -run=pythonscript). +# NOTE: no -nullrhi — rendering needs the real Vulkan RHI. In 5.8 the +# commandlet swallows print(); results go to /tmp/ue-screenshots-result.txt. +# Do NOT load_map here: after a map load the commandlet's spawn context dies +# and every spawn_actor call returns None. + +import os + +import unreal + +RESULT = "/tmp/ue-screenshots-result.txt" +lines = [] + + +def log(msg): + lines.append(str(msg)) + + +REPO = os.path.abspath(os.path.join(unreal.Paths.project_dir(), "..", "..")) +OUT_DIR = os.path.join(REPO, "Screenshots") +WORLD = unreal.EditorLevelLibrary.get_editor_world() + + +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 + + +def spawn_mesh(mesh, loc, label=None, scale=None): + a = unreal.EditorLevelLibrary.spawn_actor_from_object(mesh, loc) + if a is None: + log(f"spawn mesh {label} returned None") + return None + if scale is not None: + a.set_actor_scale3d(scale) + if label: + a.set_actor_label(label) + return a + + +# --- stage content ----------------------------------------------------------- + +try: + floor_mesh = unreal.EditorAssetLibrary.load_asset("/Game/Test/ScaleTest/ScaleTest_A") + if floor_mesh: + spawn_mesh(floor_mesh, unreal.Vector(450, 450, -50.0), "ReviewFloor", scale=(45.0, 45.0, 0.1)) + log("staged: floor (45 m plane from ScaleTest_A)") + + dl = spawn(unreal.DirectionalLight, unreal.Vector(0, 0, 900), "RenderKeyLight", + rot=unreal.Rotator(pitch=-38.0, yaw=35.0, roll=0.0)) + if dl: + 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: + if spawn_mesh(char_mesh, unreal.Vector(0, 0, 0), "ReviewCharacter"): + log("staged: MPFB character at origin") + else: + log("MISSING: /Game/Test/Character/character") + + for name, x, y in (("ScaleTest_A", 0.0, 0.0), ("ScaleTest_B", 400.0, 0.0), ("ScaleTest_C", 0.0, 400.0)): + m = unreal.EditorAssetLibrary.load_asset(f"/Game/Test/ScaleTest/{name}") + if m: + spawn_mesh(m, unreal.Vector(x, y, 50.0), f"Cube_{name}") + log(f"staged: {name} at ({x}, {y}, 50)") +except Exception as exc: # noqa + log(f"staging failed: {exc!r}") + +# --- render ------------------------------------------------------------------ + + +def render_shot(name, loc, target, fov=70.0): + rt = unreal.RenderingLibrary.create_render_target2d( + WORLD, 1920, 1080, unreal.TextureRenderTargetFormat.RTF_RGBA8_SRGB, + unreal.LinearColor(0.0, 0.0, 0.0, 1.0)) + cap = spawn(unreal.SceneCapture2D, loc) + if cap is None: + log(f"{name}: no capture actor") + return + cap.set_actor_rotation(unreal.MathLibrary.find_look_at_rotation(loc, target), False) + comp = cap.get_component_by_class(unreal.SceneCaptureComponent2D) + comp.set_editor_property("texture_target", rt) + comp.set_editor_property("capture_source", unreal.SceneCaptureSource.SCS_FINAL_COLOR_LDR) + comp.set_editor_property("fov_angle", fov) + comp.set_editor_property("capture_every_frame", False) + comp.set_editor_property("capture_on_movement", False) + try: + pps = comp.get_editor_property("post_process_settings") + pps.set_editor_property("auto_exposure_method", unreal.AutomaticExposureMethod.AEM_MANUAL) + pps.set_editor_property("auto_exposure_bias", 1.0) + comp.set_editor_property("post_process_settings", pps) + except Exception as exc: # noqa + log(f"{name}: exposure control skipped ({exc!r})") + comp.capture_scene() + os.makedirs(OUT_DIR, exist_ok=True) + ok = unreal.RenderingLibrary.export_render_target(WORLD, rt, OUT_DIR, name) + out = os.path.join(OUT_DIR, f"{name}.png") + size = os.path.getsize(out) if os.path.exists(out) else 0 + log(f"rendered {name}: export ok={ok}, {size} bytes") + unreal.EditorLevelLibrary.destroy_actor(cap) + + +SHOTS = [ + ("01-overview", unreal.Vector(900, -1100, 500), unreal.Vector(200, 100, 80), 70.0), + ("02-character", unreal.Vector(180, -260, 160), unreal.Vector(0, 0, 85), 55.0), + ("03-scale-cubes", unreal.Vector(550, -700, 150), unreal.Vector(200, 200, 50), 60.0), +] + +for shot_name, loc, target, fov in SHOTS: + try: + render_shot(shot_name, loc, target, fov) + except Exception as exc: # noqa + log(f"shot {shot_name} failed: {exc!r}") + +with open(RESULT, "w") as f: + f.write("\n".join(lines) + "\n") diff --git a/Tools/render_screenshots.sh b/Tools/render_screenshots.sh new file mode 100755 index 0000000..e8424f9 --- /dev/null +++ b/Tools/render_screenshots.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Render in-game screenshots for visual review into /Screenshots/ (gitignored). +# +# Usage: Tools/render_screenshots.sh +# Rebuilds /Game/Test/ReviewMap from current content, runs the game on it +# (the GigLineShotActor walks three cameras, captures, quits), then copies +# the PNGs into Screenshots/. +set -euo pipefail + +REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ENGINE="${UE_ENGINE_DIR:-/run/media/system/DATA/Engines/UE_5.8.4}" +PROJ="$REPO/Game/GigLine/GigLine.uproject" + +echo "== 1/3 build review map ==" +cd "$ENGINE" +./Engine/Binaries/Linux/UnrealEditor-Cmd "$PROJ" \ + -run=pythonscript -script="$REPO/Tools/ue_build_review_map.py" \ + -nullrhi -unattended -nopause -stdout +cat /tmp/ue-review-map-result.txt + +echo "== 2/3 run game on review map ==" +./Engine/Binaries/Linux/UnrealEditor-Cmd "$PROJ" /Game/Test/ReviewMap \ + -game -ResX=1920 -ResY=1080 -nopause -nosplash -stdout + +echo "== 3/3 collect screenshots ==" +mkdir -p "$REPO/Screenshots" +rm -f "$REPO"/Screenshots/review-*.png +found=0 +for src in \ + "$REPO/Game/GigLine/Saved/Screenshots"/*/review-*.png \ + "$REPO/Game/GigLine/Saved/Screenshots"/*/*/review-*.png \ + "$HOME/.config/Epic/GigLine/Saved/Screenshots"/*/review-*.png; do + [ -e "$src" ] || continue + cp -f "$src" "$REPO/Screenshots/" + found=1 +done +[ "$found" = "1" ] || { echo "ERROR: no review-*.png found"; exit 1; } +ls -la "$REPO/Screenshots" diff --git a/Tools/ue_build_review_map.py b/Tools/ue_build_review_map.py new file mode 100644 index 0000000..e4aa1d5 --- /dev/null +++ b/Tools/ue_build_review_map.py @@ -0,0 +1,128 @@ +# 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 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: + floor = spawn(unreal.StaticMeshActor, unreal.Vector(450, 450, -50.0), "ReviewFloor", + scale=(45.0, 45.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 (45 m plane, gray)") + 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: + ch.skeletal_mesh_component.set_skinned_asset_and_update(char_mesh) + log("staged: MPFB character at origin, facing -Y") + else: + log("MISSING: /Game/Test/Character/character") + + for name, x in (("ScaleTest_A", -400.0), ("ScaleTest_B", 0.0), ("ScaleTest_C", 400.0)): + m = unreal.EditorAssetLibrary.load_asset(f"/Game/Test/ScaleTest/{name}") + if m: + c = spawn(unreal.StaticMeshActor, unreal.Vector(x, 600.0, 50.0), f"Cube_{name}") + if c: + c.static_mesh_component.set_static_mesh(m) + log(f"staged: {name} at ({x}, 600, 50)") + + shots = spawn(unreal.GigLineShotActor, unreal.Vector(0, 0, 300), "ShotSequencer") + if shots: + shots.set_editor_property("shot_locations", [ + unreal.Vector(900, -1100, 500), + unreal.Vector(200, -300, 160), + unreal.Vector(200, 100, 200), + ]) + shots.set_editor_property("shot_rotations", [ + unreal.MathLibrary.find_look_at_rotation( + unreal.Vector(900, -1100, 500), unreal.Vector(100, 200, 80)), + unreal.MathLibrary.find_look_at_rotation( + unreal.Vector(200, -300, 160), unreal.Vector(0, 0, 85)), + unreal.MathLibrary.find_look_at_rotation( + unreal.Vector(200, 100, 200), unreal.Vector(0, 600, 50)), + ]) + shots.set_editor_property("shot_names", ["overview", "character", "scale-cubes"]) + shots.set_editor_property("quit_when_done", True) + log("staged: ShotSequencer with 3 cameras") +except Exception as exc: # noqa + log(f"staging failed: {exc!r}") + +# --- save -------------------------------------------------------------------- + +try: + 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")