tools: visual-review screenshot pipeline
- AGigLineShotActor (dev-only): walks a fixed camera list in -game mode, captures via FScreenshotRequest, quits. Test content only. - ue_build_review_map.py builds /Game/Test/ReviewMap from current content (gray floor, key light, sky, MPFB character, 1 m scale cubes). - render_screenshots.sh: build map -> run game -> collect PNGs into gitignored Screenshots/. - Screenshots/README.md (committed) describes each capture with approval state; AGENTS.md gains the visual-review instructions.
This commit is contained in:
1 parent
774d810dab
commit
bbb3e147b3
12 files changed
+525
-1
No files matched your search
@@ -52,3 +52,8 @@ Thumbs.db
|
|||||||
|
|
||||||
# Unreal per-project build artefacts (FileOpenOrder logs and the like)
|
# Unreal per-project build artefacts (FileOpenOrder logs and the like)
|
||||||
Build/
|
Build/
|
||||||
|
|
||||||
|
# Rendered screenshots for visual review — generated locally, never committed.
|
||||||
|
# The review README stays tracked; the PNGs do not.
|
||||||
|
/Screenshots/*
|
||||||
|
!/Screenshots/README.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.
|
- Keep restricted assets out of the public repository; prefer redistributable replacements.
|
||||||
- Review Epic distribution requirements before publishing builds.
|
- 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
|
## Roadmap
|
||||||
|
|
||||||
The living roadmap with per-version task checklists is in **[ROADMAP.md](ROADMAP.md)**.
|
The living roadmap with per-version task checklists is in **[ROADMAP.md](ROADMAP.md)**.
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -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>(
|
||||||
|
ACameraActor::StaticClass(), ShotLocations[CurrentShot], ShotRotations[CurrentShot], Params);
|
||||||
|
PlayerController->SetViewTarget(CurrentCamera);
|
||||||
|
|
||||||
|
SettleCountdown = SettleSeconds;
|
||||||
|
bWaitingForSettle = true;
|
||||||
|
}
|
||||||
@@ -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<FVector> ShotLocations;
|
||||||
|
|
||||||
|
/** Rotations for each shot camera (same index as ShotLocations). */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||||
|
TArray<FRotator> ShotRotations;
|
||||||
|
|
||||||
|
/** File name fragment for each shot (same index as ShotLocations). */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||||
|
TArray<FString> 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<class APlayerController> PlayerController;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<class ACameraActor> CurrentCamera;
|
||||||
|
|
||||||
|
int32 CurrentShot = 0;
|
||||||
|
float SettleCountdown = 0.f;
|
||||||
|
bool bWaitingForSettle = false;
|
||||||
|
bool bWaitingForScreenshot = false;
|
||||||
|
};
|
||||||
@@ -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.
|
- **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/`.
|
- **`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.
|
- **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`.
|
||||||
@@ -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).
|
||||||
+1
-1
@@ -12,6 +12,7 @@ Repeatable import/export and processing helpers. Anything that was done twice by
|
|||||||
|
|
||||||
## Helpers
|
## 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.
|
- `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.
|
- `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.
|
- `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
|
## Planned helpers
|
||||||
- Blender export preset runner (named presets, metric scale assertion)
|
- 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)
|
- 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
|
- QGIS processing steps as batch scripts, so the geo cleanup is reproducible rather than GUI-only
|
||||||
@@ -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 <repo>/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")
|
||||||
Executable
+38
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Render in-game screenshots for visual review into <repo>/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"
|
||||||
@@ -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 <uproject> -run=pythonscript -script=<repo>/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")
|
||||||
Reference in new issue
Block a user