Compare commits
3
Commits
v0.1.2
...
0f928531df
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f928531df | ||
|
|
9a3784683f | ||
|
|
d6d3d54810 |
No files matched your search
+21
-1
@@ -8,6 +8,25 @@ Format: [Keep a Changelog](https://keepachangelog.com/) style, versions follow [
|
||||
|
||||
Nothing yet.
|
||||
|
||||
## [0.1.3] — 2026-11-09
|
||||
|
||||
Packaging and setup fixes. No gameplay or visual scope; this patch makes the pipeline and its documentation tell the truth about what `0.1.x` taught.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Map imports could report success while saving nothing.** `.umap` files are LFS-tracked and check out read-only, so `save_map()` returned `False` with no error and the staged actor vanished on the next load. New `Tools/ue_common.py` (`make_writable`, `require_saved`) clears the read-only bit and makes the failure loud; `ue_hero_facade_import.py` and `ue_import_north_buildings.py` now use it. Verified from a forced `0o444` map: chmod logged, `save_map: True`, exit 0.
|
||||
|
||||
### Changed
|
||||
|
||||
- `Docs/build.md`: content-pipeline table now lists every step as it exists (seven site FBXs, frontage, north buildings, hero, map) and the shared FBX preset; packaging archive corrected to `Builds/GigLineQA`; the packaged log location and `-qaselftest` usage are documented; new **traps** table covers the read-only save, the hero `--out`-is-the-FBX mistake, the MPFB add-on path, the shot actor quitting under QA, and the null-RHI trace trap.
|
||||
- `Docs/setup.md`: the fresh-checkout build is marked verified (2026-11-08) and its blocker closed; a new section documents the two optional keys (`OPENROUTER_API_KEY`, `MAPBOX_ACCESS_TOKEN`) and that neither is needed by the build, the tests or a release.
|
||||
- `Docs/Architecture/07_deployment_view.md` and the combined `architecture.md`: build-output path corrected and marked as living outside the repository.
|
||||
|
||||
### Verified
|
||||
|
||||
- 54 tool tests pass (5 new for `ue_common`).
|
||||
- Packaged build still launches into `DemoMap` and `-qaselftest` passes every phase.
|
||||
|
||||
## [0.1.2] — 2026-11-09
|
||||
|
||||
Scale and façade correction patch. Buildings and the hero elevation now match the reference photography and the cross-checked height data; no new gameplay scope.
|
||||
@@ -110,7 +129,8 @@ The first playable release: a walkable exterior demo of the **6840 Hollywood Blv
|
||||
- Kerb height (0.13 m as built vs 0.15 m assumed), kerb radii, driveway aprons and graded crossing ramps are deferred to `0.1.1`; crossing ramps are kerb flushes.
|
||||
- One block only; no vehicles, NPCs, audio, or settings UI yet.
|
||||
|
||||
[Unreleased]: https://git.allucanget.biz/zwitschi/gig-line/compare/v0.1.2...main
|
||||
[Unreleased]: https://git.allucanget.biz/zwitschi/gig-line/compare/v0.1.3...main
|
||||
[0.1.3]: https://git.allucanget.biz/zwitschi/gig-line/releases/tag/v0.1.3
|
||||
[0.1.2]: https://git.allucanget.biz/zwitschi/gig-line/releases/tag/v0.1.2
|
||||
[0.1.1]: https://git.allucanget.biz/zwitschi/gig-line/releases/tag/v0.1.1
|
||||
[0.1.0]: https://git.allucanget.biz/zwitschi/gig-line/releases/tag/v0.1.0
|
||||
@@ -7,7 +7,7 @@
|
||||
| Dev workstation | Bazzite Linux 45 (Fedora 45 base, immutable ostree), Ryzen 7 7700X, 64 GB RAM, RTX 5070 Ti 16 GB, Vulkan RHI |
|
||||
| Engine install | `/run/media/system/DATA/Engines/UE_5.8.4` (73 GiB, external to repo) |
|
||||
| Project repo | `/run/media/system/DATA/Documents/06-games/gig-line` (Git + LFS, remote at `git.allucanget.biz`) |
|
||||
| Build output | `/run/media/system/DATA/Builds/GigLine` (packaged Linux build, ~1.1 GiB) |
|
||||
| Build output | `/run/media/system/DATA/Builds/GigLineQA/Linux` (packaged Development build, ~1.1 GiB; outside the repository) |
|
||||
|
||||
Motivation
|
||||
Single-machine development. No build farm, no CI pipeline yet. The engine, toolchain, and GPU are all on the same host.
|
||||
|
||||
@@ -300,7 +300,7 @@ Source data: `Art/blender/` (`.blend` files), `Geo/` (OSM extracts, DEM, verific
|
||||
| Dev workstation | Bazzite Linux 45 (Fedora 45 base, immutable ostree), Ryzen 7 7700X, 64 GB RAM, RTX 5070 Ti 16 GB, Vulkan RHI |
|
||||
| Engine install | `/run/media/system/DATA/Engines/UE_5.8.4` (73 GiB, external to repo) |
|
||||
| Project repo | `/run/media/system/DATA/Documents/06-games/gig-line` (Git + LFS, remote at `git.allucanget.biz`) |
|
||||
| Build output | `/run/media/system/DATA/Builds/GigLine` (packaged Linux build, ~1.1 GiB) |
|
||||
| Build output | `/run/media/system/DATA/Builds/GigLineQA/Linux` (packaged Development build, ~1.1 GiB; outside the repository) |
|
||||
|
||||
Motivation
|
||||
Single-machine development. No build farm, no CI pipeline yet. The engine, toolchain, and GPU are all on the same host.
|
||||
|
||||
+45
-37
@@ -1,6 +1,6 @@
|
||||
# Build
|
||||
|
||||
Fresh-checkout build procedure. Completed 2026-11-07 after the pinned engine was installed and a blank `Game/GigLine` build was packaged and launch-tested.
|
||||
Fresh-checkout build, content-pipeline and packaging procedure for the walkable demo. Begun 2026-11-07 from a blank build and extended through the `0.1.x` patches; the release checks that exercise it are in [release-checks.md](release-checks.md).
|
||||
|
||||
## Constraints
|
||||
|
||||
@@ -10,8 +10,7 @@ Fresh-checkout build procedure. Completed 2026-11-07 after the pinned engine was
|
||||
|
||||
## Engine
|
||||
|
||||
Pinned install: **Unreal Engine 5.8.4** (CL 58902052, promoted build) at
|
||||
`/run/media/system/DATA/Engines/UE_5.8.4`. The engine bundles **no** Linux clang; the system clang (23.1.4, Fedora 45) is the game-code toolchain. Game targets must use `BuildSettingsVersion.V7` to share the installed editor's build environment.
|
||||
Pinned install: **Unreal Engine 5.8.4** (CL 58902052, promoted build) at `/run/media/system/DATA/Engines/UE_5.8.4`. The engine bundles **no** Linux clang; the system clang (23.1.4, Fedora 45) is the game-code toolchain. Game targets must use `BuildSettingsVersion.V7` to share the installed editor's build environment.
|
||||
|
||||
## Fresh-checkout build
|
||||
|
||||
@@ -42,20 +41,14 @@ cd /run/media/system/DATA/Engines/UE_5.8.4
|
||||
Engine/Build/BatchFiles/RunUAT.sh BuildCookRun \
|
||||
-project="/run/media/system/DATA/Documents/06-games/gig-line/Game/GigLine/GigLine.uproject" \
|
||||
-noP4 -platform=Linux -clientconfig=Development \
|
||||
-cook -allmaps -build -stage -pak \
|
||||
-archive -archivedirectory="/run/media/system/DATA/Builds/GigLine" \
|
||||
-cook -build -stage -pak \
|
||||
-archive -archivedirectory="/run/media/system/DATA/Builds/GigLineQA" \
|
||||
-unattended
|
||||
# Expected: "BUILD SUCCESSFUL" (~1 min for the blank project; archive ~1.1 GiB)
|
||||
# Launch test (plain, no -map): /run/media/system/DATA/Builds/GigLine/Linux/GigLine.sh
|
||||
# Expected: "AutomationTool exiting with ExitCode=0 (Success)" and "EXIT=0", ~90 s
|
||||
# Launch test (plain, no -map): /run/media/system/DATA/Builds/GigLineQA/Linux/GigLine.sh
|
||||
```
|
||||
|
||||
**Launch test must open the DemoMap.** A plain `./GigLine.sh` should load
|
||||
`/Game/Hollywood/DemoMap` (set via `DefaultEngine.ini` `GameMapsSettings`). If it
|
||||
instead opens `/Engine/Maps/Templates/OpenWorld`, the game shows an empty
|
||||
non-interactive "stuck" scene — check that `GameDefaultMap` is in
|
||||
`DefaultEngine.ini`, not `DefaultGame.ini`. Confirm the loaded map in the engine
|
||||
log: `.../Linux/GigLine/Saved/Logs/GigLine.log` (`LoadMap: /Game/Hollywood/DemoMap`).
|
||||
The archive directory is deliberately **outside** the repository; never archive into the checkout.
|
||||
**Launch test must open the DemoMap.** A plain `./GigLine.sh` should load `/Game/Hollywood/DemoMap` (set via `DefaultEngine.ini` `GameMapsSettings`). If it instead opens `/Engine/Maps/Templates/OpenWorld`, the game shows an empty non-interactive "stuck" scene — check that `GameDefaultMap` is in `DefaultEngine.ini`, not `DefaultGame.ini`. Confirm the loaded map in the engine log: `.../Linux/GigLine/Saved/Logs/GigLine.log` (`LoadMap: /Game/Hollywood/DemoMap`). The archive directory is deliberately **outside** the repository; never archive into the checkout.
|
||||
|
||||
## Headless content checks
|
||||
|
||||
@@ -76,9 +69,7 @@ Both content-acceptance tests run headless and are the template for all asset im
|
||||
|
||||
## Content pipeline (current)
|
||||
|
||||
The hero block and the demo level are built by a fixed sequence. Each step writes
|
||||
a result file under `/tmp/gigline/`; read it, because the commandlet's own stdout
|
||||
can be swallowed.
|
||||
The hero block and the demo level are built by a fixed sequence. Each step writes a result file under `/tmp/gigline/`; read it, because the commandlet's own stdout can be swallowed.
|
||||
|
||||
| Step | Tool | Output |
|
||||
| --- | --- | --- |
|
||||
@@ -86,21 +77,23 @@ can be swallowed.
|
||||
| Terrain | `Art/blender/terrain/terrain_from_dem.py` | `Art/blender/terrain/terrain-*.blend` |
|
||||
| Street | `Art/blender/street/street_mesh.py` | `Art/blender/street/street-*.blend` |
|
||||
| Site assembly | `Art/blender/site/site_assembly.py` | `Art/blender/site/site-*.blend` |
|
||||
| Site export | `Art/blender/site/export_site_fbx.py` | six FBXs, one per mesh |
|
||||
| Hero façade | `Tools/hero_facade_build.py` | `Art/blender/hero/hero-facade-*.blend` + JSON report |
|
||||
| Site export | `Art/blender/site/export_site_fbx.py` | seven FBXs, one per mesh (terrain, road, both sidewalks, both kerbs, crossing) |
|
||||
| Hero façade | `Tools/hero_facade_build.py` | `Art/blender/hero/hero-facade-*.blend` + JSON part report |
|
||||
| Block frontage | `Tools/export_block_frontage.py` | six frontage FBXs from the blockout |
|
||||
| North buildings | `Tools/export_north_buildings.py` | TCL and Tussaud's FBXs from the blockout |
|
||||
| Site import | `Tools/ue_import_site.py` | `/Game/Hollywood/Site/*` |
|
||||
| Frontage import | `Tools/ue_import_block_frontage.py` | `/Game/Hollywood/Frontage/*` |
|
||||
| North import | `Tools/ue_import_north_buildings.py` | `/Game/Hollywood/North/*` |
|
||||
| Hero import | `Tools/ue_hero_facade_import.py` | `/Game/Hollywood/ElCapitan/SM_HeroFacade` |
|
||||
| Demo map | `Tools/ue_build_demo_map.py` | `/Game/Hollywood/DemoMap` |
|
||||
| Review shots | `Tools/ue_build_review_map.py` + `Tools/render_screenshots.sh` | `Screenshots/` |
|
||||
|
||||
Every Blender exporter calls [`Tools/fbx_export_preset.py`](../Tools/fbx_export_preset.py) (`ue_metric_v1` / `ue_character_v1`) instead of repeating the FBX argument block — one place for the scale conversion, the axis mapping and `mesh_smooth_type="FACE"` (without which every Unreal import warns about missing smoothing groups and round geometry arrives faceted).
|
||||
|
||||
Two rules that the pipeline enforces rather than documents:
|
||||
|
||||
- **One conversion.** Metres to Unreal centimetres happens only in
|
||||
`Geo/tools/osm_common.py::to_unreal_local`. Sidecars (`<asset>.geo.json`) record
|
||||
the result and `Geo/tools/asset_metadata.py audit` re-derives it.
|
||||
- **Collision is decided at import,** not later: terrain, road and sidewalks are
|
||||
complex-as-simple; kerbs and props stay simple. A simple hull over a kerbed
|
||||
street ramps straight across the kerb.
|
||||
- **One conversion.** Metres to Unreal centimetres happens only in `Geo/tools/osm_common.py::to_unreal_local`. Sidecars (`<asset>.geo.json`) record the result and `Geo/tools/asset_metadata.py audit` re-derives it.
|
||||
- **Collision is decided at import,** not later: terrain, road and sidewalks are complex-as-simple; kerbs and props stay simple. A simple hull over a kerbed street ramps straight across the kerb.
|
||||
|
||||
## Known engine quirks (2026-11-07)
|
||||
|
||||
@@ -110,20 +103,35 @@ Two rules that the pipeline enforces rather than documents:
|
||||
|
||||
## Packaged Linux build
|
||||
|
||||
- 2026-11-07: blank `Game/GigLine` Development build packaged via the command above; 1.1 GiB archive at `/run/media/system/DATA/Builds/GigLine/Linux`; `GigLine.sh` launched on the desktop session, Vulkan swapchain created, 759 frames rendered across a 60 s smoke test. Controls document to be written with the first playable (`0.1.0` Gameplay section).
|
||||
Current QA archive: `/run/media/system/DATA/Builds/GigLineQA`. A plain `./GigLine.sh` opens `/Game/Hollywood/DemoMap`; controls are in [controls.md](controls.md) and shown in-game bottom-right. The packaged log is written **relative to the wrapper's working directory**, i.e. `<archive>/Linux/GigLine/Saved/Logs/GigLine.log` — not into the repository.
|
||||
|
||||
Behaviour check without a human at the keyboard:
|
||||
|
||||
```bash
|
||||
cd /run/media/system/DATA/Builds/GigLineQA/Linux
|
||||
./GigLine.sh -windowed -ResX=1280 -ResY=720 -nosplash -qaselftest
|
||||
# then read <archive>/Linux/GigLine/Saved/Logs/GigLine.log for "GigLine QA:" lines
|
||||
```
|
||||
|
||||
`-qaselftest` walks for 2 s, resets, pauses/unpauses, sweeps a 16-row seam transect, fires a ground-referenced 21 m wall probe along the street, fires 40 m façade-occlusion traces sideways from the road centre wherever a building is staged, and writes a frame-time series to `Saved/Profiling/`. Every phase logs `... PASS`/`FAIL` and the run ends with `RESULT`.
|
||||
|
||||
## Traps that cost real time in `0.1.x`
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
| --- | --- | --- |
|
||||
| A builder reports success but the asset is unchanged | `.blend`/`.umap` are LFS-tracked and check out **read-only**; Blender and Unreal fail the save quietly | `chmod u+w <file>` first — `ue_build_*`/`ue_hero_facade_import` and every Blender builder need it |
|
||||
| `File format is not supported` when reopening a `.blend` | `hero_facade_build.py --out` writes the **FBX**; putting the `.blend` path there overwrites it with FBX bytes | `--out /tmp/…fbx --save-blend Art/…blend --report Art/…json` |
|
||||
| `Add-on not loaded: "bl_ext.blender_org.mpfb"` | `mpfb_character_export.py` needs the MPFB add-on visible to Blender | export `BLENDER_USER_SCRIPTS` (see the invocation in [../Art/blender/README.md](../Art/blender/README.md)) |
|
||||
| The packaged run exits after a couple of seconds during QA | `GigLineShotActor` runs its screenshot list and quits (`bQuitWhenDone`) | it skips its rounds when `-qaselftest` is on the command line — keep that guard |
|
||||
| Headless `-nullrhi` traces report no hits for everything | the null RHI has no physics scene | verify traces in the **packaged** run (`-qaselftest`), never in a commandlet |
|
||||
| Import logs "No smoothing group information" for every mesh | `mesh_smooth_type="OFF"` in the FBX export | use `Tools/fbx_export_preset.py` (`FACE`) |
|
||||
|
||||
## PIE input test (compare editor vs packaged input intake)
|
||||
|
||||
Use to isolate whether the "no controls respond" bug is the packaged standalone's
|
||||
input intake or the Enhanced Input wiring generally.
|
||||
Use to isolate whether the "no controls respond" bug is the packaged standalone's input intake or the Enhanced Input wiring generally.
|
||||
|
||||
- `Tools/run_pie.sh` opens the editor on `/Game/Hollywood/DemoMap`. Press the
|
||||
green **Play** (PIE) button and walk / mouse-look.
|
||||
- `Tools/run_pie.sh` opens the editor on `/Game/Hollywood/DemoMap`. Press the green **Play** (PIE) button and walk / mouse-look.
|
||||
- Interpretation:
|
||||
- PIE **works** -> the Enhanced Input wiring is fine and the standalone
|
||||
packaged build's SDL input intake is the environment-dependent bug.
|
||||
- PIE **fails** too -> the Enhanced Input setup itself is broken; rebuild
|
||||
the input config / character from scratch.
|
||||
- The DemoMap is PIE-ready as committed: `GlobalDefaultGameMode` +
|
||||
`GameDefaultMap=/Game/Hollywood/DemoMap`, and the map stages a PlayerStart at
|
||||
`(-13336,-1150,600)` on the cut sidewalk with the walkable `AGigLineCharacter`.
|
||||
- PIE **works** -> the Enhanced Input wiring is fine and the standalone packaged build's SDL input intake is the environment-dependent bug.
|
||||
- PIE **fails** too -> the Enhanced Input setup itself is broken; rebuild the input config / character from scratch.
|
||||
- The DemoMap is PIE-ready as committed: `GlobalDefaultGameMode` + `GameDefaultMap=/Game/Hollywood/DemoMap`, and the map stages a PlayerStart at `(-13336,-1150,600)` on the cut sidewalk with the walkable `AGigLineCharacter`.
|
||||
+10
-16
@@ -1,36 +1,30 @@
|
||||
# Controls (`0.1.0` walkable demo)
|
||||
|
||||
Keyboard and mouse. The same list is shown in-game, bottom-right of the frame
|
||||
(`AGigLineHUD`), and the packaged Linux build ships with these bindings.
|
||||
Keyboard and mouse. The same list is shown in-game, bottom-right of the frame (`AGigLineHUD`), and the packaged Linux build ships with these bindings.
|
||||
|
||||
| Action | Key | Notes |
|
||||
| --- | --- | --- |
|
||||
| Move forward | `W` | 4 m/s walk |
|
||||
| Move back | `S` | |
|
||||
| Strafe right | `D` | |
|
||||
| Strafe left | `A` | |
|
||||
| Move back | `S` | |
|
||||
| Strafe right | `D` | |
|
||||
| Strafe left | `A` | |
|
||||
| Look | Mouse | Captured on launch; `Escape` releases it |
|
||||
| Jump | `Space` | |
|
||||
| Jump | `Space` | |
|
||||
| Reset to start | `R` | Returns to the grounded spawn point |
|
||||
| Pause / release mouse | `Escape` | Second press resumes and recaptures the mouse |
|
||||
|
||||
## Notes
|
||||
|
||||
- Movement is relative to where you are looking (controller yaw), so the capsule
|
||||
stays upright on the sloped street.
|
||||
- The mouse is captured on launch, so looking works without clicking first.
|
||||
`Escape` toggles the capture as part of pause.
|
||||
- Bindings are Enhanced Input actions in `/Game/Input` built by
|
||||
`Tools/ue_create_input_assets.py`; the mapping context is `IMC_Default`.
|
||||
- Movement is relative to where you are looking (controller yaw), so the capsule stays upright on the sloped street.
|
||||
- The mouse is captured on launch, so looking works without clicking first. `Escape` toggles the capture as part of pause.
|
||||
- Bindings are Enhanced Input actions in `/Game/Input` built by `Tools/ue_create_input_assets.py`; the mapping context is `IMC_Default`.
|
||||
|
||||
## Testing input without a physical keyboard
|
||||
|
||||
`Tools/input_test.py` injects OS-level keyboard/mouse events through a virtual
|
||||
`/dev/uinput` device (see `Tools/input_emulation.py`), e.g.:
|
||||
`Tools/input_test.py` injects OS-level keyboard/mouse events through a virtual `/dev/uinput` device (see `Tools/input_emulation.py`), e.g.:
|
||||
|
||||
```bash
|
||||
python3 Tools/input_test.py --sequence move:2,look:3
|
||||
```
|
||||
|
||||
It drives the focused game window exactly like real hardware, which is how the
|
||||
controls were regression-tested on the packaged build.
|
||||
It drives the focused game window exactly like real hardware, which is how the controls were regression-tested on the packaged build.
|
||||
+9
-26
@@ -37,22 +37,12 @@ Important nuance: ODbL governs the **database** (the OSM data as a collection an
|
||||
|
||||
## 4. Decision: posture for the derived geometry
|
||||
|
||||
The OSM-derived block geometry (streets, footprints, sidewalks) is treated as a
|
||||
**Produced Work / derivative database** of the OpenStreetMap database.
|
||||
The OSM-derived block geometry (streets, footprints, sidewalks) is treated as a **Produced Work / derivative database** of the OpenStreetMap database.
|
||||
|
||||
- The **share-alike obligation is accepted for the data layer only**. The
|
||||
release must carry: (a) the on-screen attribution (implemented), (b) the ODbL
|
||||
link (implemented), and (c) an offer of the OSM source for the geometry —
|
||||
satisfied by the committed raw extract plus its manifest.
|
||||
- **Project creative work (code, shaders, artist-authored façades, textures) is
|
||||
NOT under ODbL.** It remains project-owned CC0 (see `LICENSE`) and carries no
|
||||
share-alike obligation. Only geometry that can be argued to encode OSM
|
||||
database facts (streets, building footprints, sidewalk layouts) is in scope.
|
||||
- **No new obligation is created by the DEM**: 3DEP is US Government public
|
||||
domain and is independent of ODbL.
|
||||
- **Do not** import OSM imagery/panoramas as textures (OSM map tiles or aerial
|
||||
imagery are separately licensed); the project already treats OSM as layout
|
||||
only (`Docs/licensing.md` rule 3).
|
||||
- The **share-alike obligation is accepted for the data layer only**. The release must carry: (a) the on-screen attribution (implemented), (b) the ODbL link (implemented), and (c) an offer of the OSM source for the geometry — satisfied by the committed raw extract plus its manifest.
|
||||
- **Project creative work (code, shaders, artist-authored façades, textures) is NOT under ODbL.** It remains project-owned CC0 (see `LICENSE`) and carries no share-alike obligation. Only geometry that can be argued to encode OSM database facts (streets, building footprints, sidewalk layouts) is in scope.
|
||||
- **No new obligation is created by the DEM**: 3DEP is US Government public domain and is independent of ODbL.
|
||||
- **Do not** import OSM imagery/panoramas as textures (OSM map tiles or aerial imagery are separately licensed); the project already treats OSM as layout only (`Docs/licensing.md` rule 3).
|
||||
|
||||
Release checklist items this lands:
|
||||
|
||||
@@ -63,20 +53,13 @@ Release checklist items this lands:
|
||||
|
||||
## 5. Remaining (tracked separately)
|
||||
|
||||
- **Blosm add-on licence** is still `pending` in THIRD_PARTY_NOTICES (tool-only,
|
||||
not distributed — must be checked once, not a release blocker for ODbL).
|
||||
- Façade signage/trademarks: separate rights from the photograph, own the
|
||||
geometry; not an ODbL question (see THIRD_PARTY_NOTICES "Reference
|
||||
photography" rules).
|
||||
- **Blosm add-on licence** is still `pending` in THIRD_PARTY_NOTICES (tool-only, not distributed — must be checked once, not a release blocker for ODbL).
|
||||
- Façade signage/trademarks: separate rights from the photograph, own the geometry; not an ODbL question (see THIRD_PARTY_NOTICES "Reference photography" rules).
|
||||
|
||||
## 6. Sources checked
|
||||
|
||||
- ODbL text: <https://opendatacommons.org/licenses/odbl/1-0/>
|
||||
- OSM licence summary: <https://www.openstreetmap.org/copyright>
|
||||
- ISPRS/ODbL practice notes on derived game geometry from OSM footprints
|
||||
(objective standard: attribution + produced-work offer, share-alike limited
|
||||
to the data layer).
|
||||
- ISPRS/ODbL practice notes on derived game geometry from OSM footprints (objective standard: attribution + produced-work offer, share-alike limited to the data layer).
|
||||
|
||||
Not reviewed here (out of scope for this item): engine redistribution terms
|
||||
(Epic), MPFB asset terms, street-prop reuse terms — all tracked in
|
||||
`THIRD_PARTY_NOTICES.md`.
|
||||
Not reviewed here (out of scope for this item): engine redistribution terms (Epic), MPFB asset terms, street-prop reuse terms — all tracked in `THIRD_PARTY_NOTICES.md`.
|
||||
+38
-89
@@ -34,21 +34,18 @@ Measured at 1440p on the development workstation: Bazzite, Ryzen 7 7700X, 64 GB
|
||||
|
||||
## Check-record template
|
||||
|
||||
| Version | Check | Result | Evidence | Date |
|
||||
| ------- | ----------------------- | ------ | -------- | ---- |
|
||||
| `0.1.0` | Traversal, both sides | PASS — Blender pre-check + in-engine swept transect, 0 holes, ground Z 317..395 | `-qaselftest`, TODO 0.1.0 notes | 2026-11-08 |
|
||||
| Version | Check | Result | Evidence | Date |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `0.1.0` | Traversal, both sides | PASS — Blender pre-check + in-engine swept transect, 0 holes, ground Z 317..395 | `-qaselftest`, TODO 0.1.0 notes | 2026-11-08 |
|
||||
| `0.1.0` | Placement vs references | PASS (with deliberate simplifications) | Section below, 2026-11-08 | 2026-11-08 |
|
||||
| `0.1.0` | Collision and packaging | PASS — no blocking collision, no invisible walls, no seam gaps (see audit note) | `-qaselftest` on the packaged build, 2026-11-08 | 2026-11-08 |
|
||||
| `0.1.0` | 60 FPS at 1440p | PASS — 2.42 ms avg (414 fps), p99 3.01 ms at 2560×1440, TSR on, VSync off, no FG | `-qaselftest` perf capture, `Docs/perf/0.1.0-walk-frametimes.csv` | 2026-11-08 |
|
||||
| `0.1.0` | Fresh checkout build | PASS — clean clone of `main` (`4bc41e9`), `git lfs pull` + `check-lfs.sh` OK (130 LFS files), both `GigLineEditor` and `GigLine` targets built with the documented engine | `/tmp/fresh-check`, 2026-11-08 | 2026-11-08 |
|
||||
| `0.1.0` | Packaged Linux build | PASS — packaged Development build launches straight into DemoMap, no missing-asset/linker errors; controls shown in-game and in `Docs/controls.md` | `Docs/perf/0.1.0-packaged-hud-controls.png`, 2026-11-08 | 2026-11-08 |
|
||||
| `0.1.0` | 60 FPS at 1440p | PASS — 2.42 ms avg (414 fps), p99 3.01 ms at 2560×1440, TSR on, VSync off, no FG | `-qaselftest` perf capture, `Docs/perf/0.1.0-walk-frametimes.csv` | 2026-11-08 |
|
||||
| `0.1.0` | Fresh checkout build | PASS — clean clone of `main` (`4bc41e9`), `git lfs pull` + `check-lfs.sh` OK (130 LFS files), both `GigLineEditor` and `GigLine` targets built with the documented engine | `/tmp/fresh-check`, 2026-11-08 | 2026-11-08 |
|
||||
| `0.1.0` | Packaged Linux build | PASS — packaged Development build launches straight into DemoMap, no missing-asset/linker errors; controls shown in-game and in `Docs/controls.md` | `Docs/perf/0.1.0-packaged-hud-controls.png`, 2026-11-08 | 2026-11-08 |
|
||||
|
||||
## `0.1.0` — Placement vs references — record (2026-11-08)
|
||||
|
||||
Checked against the reference photography (`Art/reference/`) and the Geo measurements
|
||||
(`Geo/verification/*`). Build under test: current `main` content, DemoMap rendered via
|
||||
`Tools/render_screenshots.sh` (2026-11-08, Vulkan SM5, 1920×1080); engine positions
|
||||
probed from the DemoMap (commandlet, 2026-11-08).
|
||||
Checked against the reference photography (`Art/reference/`) and the Geo measurements (`Geo/verification/*`). Build under test: current `main` content, DemoMap rendered via `Tools/render_screenshots.sh` (2026-11-08, Vulkan SM5, 1920×1080); engine positions probed from the DemoMap (commandlet, 2026-11-08).
|
||||
|
||||
### Placement and scale (in-engine ↔ reference)
|
||||
|
||||
@@ -80,78 +77,43 @@ probed from the DemoMap (commandlet, 2026-11-08).
|
||||
- **Crossing ramps are kerb flushes, not graded ramps** — the `0.1.1` Crossing ramp gradient item.
|
||||
- Site identity (the neoclassical hero with motto band) matches the 2009 façade photograph and the 1922 Masonic elevation within blockout fidelity.
|
||||
|
||||
Pre-check method: `Tools/judge_screenshot.py` (vision model) flagged the simplified
|
||||
flat surfaces as "unexpected" — those flags are the deliberate deviations above, not
|
||||
placement errors. Final verdict is by human review of the shots in `Screenshots/demo/`.
|
||||
Pre-check method: `Tools/judge_screenshot.py` (vision model) flagged the simplified flat surfaces as "unexpected" — those flags are the deliberate deviations above, not placement errors. Final verdict is by human review of the shots in `Screenshots/demo/`.
|
||||
|
||||
## `0.1.0` — Collision and packaging — audit note (2026-11-08)
|
||||
|
||||
Run on the packaged build (`GigLineQA`, Development, Vulkan SM5) with `-qaselftest`.
|
||||
|
||||
- **Transect (holes/gaps):** 1456 samples over 16 rows — lane centres **plus the
|
||||
kerb/road/sidewalk seam Ys** (−1382, −1053, +718, +1047 and neighbours) and
|
||||
X −20000..−2000. Result **0 holes**. (The old 84-sample check covered only 3
|
||||
lane centres; the seam rows are what this check adds.)
|
||||
- **Invisible-wall probe:** horizontal traces 30 m long, grounded at 160 cm above
|
||||
the local ground, along the road and both seam rows. Result **0/35 blocking hits
|
||||
inside 21 m** — no invisible walls or misplaced blockers in the street envelope.
|
||||
- **Facade occlusion probe:** from the road centre, 40 m traces sideways at walking
|
||||
height toward each frontage, required only where the map stages a building (the
|
||||
spans are listed in `AGigLineCharacter` and mirror `Tools/ue_build_demo_map.py`).
|
||||
Result **0/9 directions without a blocking wall** — the player cannot walk
|
||||
through the hero or the frontage. This probe found the hero façade had **no
|
||||
simple collision** at all (complex-only, so traces and capsule sweeps passed
|
||||
through it); it is now `UseComplexAsSimple`. Auto-generated convex hulls were
|
||||
tried first and rejected: on a 95-part, 26 m façade they bulged into the street
|
||||
(the wall probe went 0/35 → 2/35).
|
||||
(First pass used a fixed Z 160 start; the street is at Z ~366, so the probe
|
||||
started inside the earth and reported every row as a dist-0 blocker. Grounding
|
||||
the probe fixed that measurement bug.)
|
||||
- **Walk / reset / pause:** 761 cm in 2 s (Z steady 456), reset 0 cm, pause
|
||||
`IsGamePaused()=1`, unpause 0 — all PASS.
|
||||
- Packaging: the packaged build launches, reaches gameplay, and the QA run quits
|
||||
cleanly. The `GigLineShotActor` screenshot rounds are suppressed under
|
||||
`-qaselftest` so they cannot quit the run before the audit phases.
|
||||
- Caveat recorded: ground Z extremes (−12 m / 3000 cm) are trace-start/terrain-edge
|
||||
artifacts of the widened extent, not walkable holes — the walkable corridor is
|
||||
continuous (walk + transect both PASS).
|
||||
- **Transect (holes/gaps):** 1456 samples over 16 rows — lane centres **plus the kerb/road/sidewalk seam Ys** (−1382, −1053, +718, +1047 and neighbours) and X −20000..−2000. Result **0 holes**. (The old 84-sample check covered only 3 lane centres; the seam rows are what this check adds.)
|
||||
- **Invisible-wall probe:** horizontal traces 30 m long, grounded at 160 cm above the local ground, along the road and both seam rows. Result **0/35 blocking hits inside 21 m** — no invisible walls or misplaced blockers in the street envelope.
|
||||
- **Facade occlusion probe:** from the road centre, 40 m traces sideways at walking height toward each frontage, required only where the map stages a building (the spans are listed in `AGigLineCharacter` and mirror `Tools/ue_build_demo_map.py`). Result **0/9 directions without a blocking wall** — the player cannot walk through the hero or the frontage. This probe found the hero façade had **no simple collision** at all (complex-only, so traces and capsule sweeps passed through it); it is now `UseComplexAsSimple`. Auto-generated convex hulls were tried first and rejected: on a 95-part, 26 m façade they bulged into the street (the wall probe went 0/35 → 2/35). (First pass used a fixed Z 160 start; the street is at Z ~366, so the probe started inside the earth and reported every row as a dist-0 blocker. Grounding the probe fixed that measurement bug.)
|
||||
- **Walk / reset / pause:** 761 cm in 2 s (Z steady 456), reset 0 cm, pause `IsGamePaused()=1`, unpause 0 — all PASS.
|
||||
- Packaging: the packaged build launches, reaches gameplay, and the QA run quits cleanly. The `GigLineShotActor` screenshot rounds are suppressed under `-qaselftest` so they cannot quit the run before the audit phases.
|
||||
- Caveat recorded: ground Z extremes (−12 m / 3000 cm) are trace-start/terrain-edge artifacts of the widened extent, not walkable holes — the walkable corridor is continuous (walk + transect both PASS).
|
||||
|
||||
## `0.1.0` — 60 FPS at 1440p — record (2026-11-08)
|
||||
|
||||
**Build under test:** packaged `GigLineQA` Development, UE **5.8.4**, Vulkan SM5,
|
||||
NVIDIA 615.78.08 (RTX 5070 Ti), Bazzite, Ryzen 7 7700X, 64 GB RAM.
|
||||
**Build under test:** packaged `GigLineQA` Development, UE **5.8.4**, Vulkan SM5, NVIDIA 615.78.08 (RTX 5070 Ti), Bazzite, Ryzen 7 7700X, 64 GB RAM.
|
||||
|
||||
**Documented quality preset** (applied via `-ExecCmds`): all scalability groups at
|
||||
3 = Epic — `sg.ViewDistanceQuality 3, sg.ShadowQuality 3,
|
||||
sg.GlobalIlluminationQuality 3, sg.ReflectionQuality 3, sg.PostProcessQuality 3,
|
||||
sg.TextureQuality 3, sg.EffectsQuality 3, sg.FoliageQuality 3, sg.ShadingQuality 3`.
|
||||
**Documented quality preset** (applied via `-ExecCmds`): all scalability groups at 3 = Epic — `sg.ViewDistanceQuality 3, sg.ShadowQuality 3, sg.GlobalIlluminationQuality 3, sg.ReflectionQuality 3, sg.PostProcessQuality 3, sg.TextureQuality 3, sg.EffectsQuality 3, sg.FoliageQuality 3, sg.ShadingQuality 3`.
|
||||
|
||||
**Upscaling state (recorded from the run):** `r.AntiAliasingMethod=4` → **TSR on**,
|
||||
default screen percentage, `r.VSync=0` (**off**), frame generation **off/unavailable**
|
||||
(`r.Streamline.FrameGeneration.Mode` absent → -999).
|
||||
**Upscaling state (recorded from the run):** `r.AntiAliasingMethod=4` → **TSR on**, default screen percentage, `r.VSync=0` (**off**), frame generation **off/unavailable** (`r.Streamline.FrameGeneration.Mode` absent → -999).
|
||||
|
||||
**Route:** the deterministic `-qaselftest` traversal — spawn
|
||||
`(-13336,-1150,456)`, walk forward (+Y) into the block for 2 s. Frame times are
|
||||
captured every frame over that route (829 frames), not a single sample.
|
||||
**Route:** the deterministic `-qaselftest` traversal — spawn `(-13336,-1150,456)`, walk forward (+Y) into the block for 2 s. Frame times are captured every frame over that route (829 frames), not a single sample.
|
||||
|
||||
**Result at 2560×1440:**
|
||||
|
||||
| Metric | Value |
|
||||
| --- | --- |
|
||||
| Frames captured | 829 |
|
||||
| Average | **2.42 ms → 414 fps** |
|
||||
| p50 | 2.38 ms |
|
||||
| p95 | 2.71 ms |
|
||||
| p99 | 3.01 ms (≈332 fps) |
|
||||
| Max | 4.00 ms |
|
||||
| Metric | Value |
|
||||
| --------------- | --------------------- |
|
||||
| Frames captured | 829 |
|
||||
| Average | **2.42 ms → 414 fps** |
|
||||
| p50 | 2.38 ms |
|
||||
| p95 | 2.71 ms |
|
||||
| p99 | 3.01 ms (≈332 fps) |
|
||||
| Max | 4.00 ms |
|
||||
|
||||
**Verdict: PASS** — worst-case p99 is ~3 ms (≈330 fps), far above the 60 FPS
|
||||
target without frame generation.
|
||||
**Verdict: PASS** — worst-case p99 is ~3 ms (≈330 fps), far above the 60 FPS target without frame generation.
|
||||
|
||||
**Variance note:** two back-to-back runs measured ~9.2 ms (≈108 fps) when another
|
||||
game instance / busy compositor overlapped the capture; repeated clean runs
|
||||
(windowed and fullscreen, Wayland and X11 drivers, window focused) all measured
|
||||
~2.4–2.5 ms. The high outlier is recorded here rather than hidden.
|
||||
**Variance note:** two back-to-back runs measured ~9.2 ms (≈108 fps) when another game instance / busy compositor overlapped the capture; repeated clean runs (windowed and fullscreen, Wayland and X11 drivers, window focused) all measured ~2.4–2.5 ms. The high outlier is recorded here rather than hidden.
|
||||
|
||||
**Reproduce:**
|
||||
|
||||
@@ -165,8 +127,7 @@ Committed frame-time series: `Docs/perf/0.1.0-walk-frametimes.csv`.
|
||||
|
||||
## `0.1.0` — Fresh checkout build — record (2026-11-08)
|
||||
|
||||
Followed the documented steps in [`Docs/build.md`](build.md) exactly, into a clean
|
||||
directory with no prior state:
|
||||
Followed the documented steps in [`Docs/build.md`](build.md) exactly, into a clean directory with no prior state:
|
||||
|
||||
```bash
|
||||
git clone https://git.allucanget.biz/zwitschi/gig-line.git /tmp/fresh-check
|
||||
@@ -179,35 +140,23 @@ Engine/Build/BatchFiles/Linux/Build.sh GigLine Linux Development \
|
||||
-project=/tmp/fresh-check/Game/GigLine/GigLine.uproject -WaitMutex
|
||||
```
|
||||
|
||||
- Clone of `main` at `4bc41e9`; LFS objects fetched (`DemoMap.umap` is real UE
|
||||
package content, not a pointer).
|
||||
- `GigLineEditor` → **Result: Succeeded** (28.2 s); produced
|
||||
`/tmp/fresh-check/Game/GigLine/Binaries/Linux/libUnrealEditor-GigLine.so`.
|
||||
- Clone of `main` at `4bc41e9`; LFS objects fetched (`DemoMap.umap` is real UE package content, not a pointer).
|
||||
- `GigLineEditor` → **Result: Succeeded** (28.2 s); produced `/tmp/fresh-check/Game/GigLine/Binaries/Linux/libUnrealEditor-GigLine.so`.
|
||||
- `GigLine` (game) → **Result: Succeeded** (26.9 s).
|
||||
- Engine: documented external install at `/run/media/system/DATA/Engines/UE_5.8.4`
|
||||
(UE 5.8.4), system clang; no machine-specific paths beyond the documented
|
||||
`-project=` argument.
|
||||
- Engine: documented external install at `/run/media/system/DATA/Engines/UE_5.8.4` (UE 5.8.4), system clang; no machine-specific paths beyond the documented `-project=` argument.
|
||||
|
||||
**Verdict: PASS.**
|
||||
|
||||
## `0.1.0` — Packaged Linux build + documented controls — record (2026-11-08)
|
||||
|
||||
**Package:** `RunUAT BuildCookRun … -platform=Linux -clientconfig=Development
|
||||
-cook -build -stage -pak -archive -archivedirectory=/run/media/system/DATA/Builds/GigLineQA`
|
||||
→ `EXIT=0` (Success).
|
||||
**Package:** `RunUAT BuildCookRun … -platform=Linux -clientconfig=Development -cook -build -stage -pak -archive -archivedirectory=/run/media/system/DATA/Builds/GigLineQA` → `EXIT=0` (Success).
|
||||
|
||||
**Launch:** `./GigLine.sh -windowed` (plain, no map argument) →
|
||||
`LogLoad: LoadMap: /Game/Hollywood/DemoMap?Name=Player` — the packaged game opens
|
||||
the walkable demo directly (the default-map fix holds in a package, not just the
|
||||
editor). No `LogLinker`/missing-asset/failed-to-find errors in the run.
|
||||
**Launch:** `./GigLine.sh -windowed` (plain, no map argument) → `LogLoad: LoadMap: /Game/Hollywood/DemoMap?Name=Player` — the packaged game opens the walkable demo directly (the default-map fix holds in a package, not just the editor). No `LogLinker`/missing-asset/failed-to-find errors in the run.
|
||||
|
||||
**Controls documented and shown in-game:**
|
||||
- `Docs/controls.md` — full binding table (WASD, mouse look, Space, R, Esc) plus
|
||||
how to test input headlessly.
|
||||
- `AGigLineHUD` now draws a control legend bottom-right every frame:
|
||||
"WASD move · Mouse look · Space jump · R reset · Esc pause".
|
||||
- Verified in the packaged capture: the bottom-right text bbox is 426×12 px
|
||||
(a single right-aligned line), attributed text bottom-left. Screenshot:
|
||||
`Docs/perf/0.1.0-packaged-hud-controls.png`.
|
||||
|
||||
- `Docs/controls.md` — full binding table (WASD, mouse look, Space, R, Esc) plus how to test input headlessly.
|
||||
- `AGigLineHUD` now draws a control legend bottom-right every frame: "WASD move · Mouse look · Space jump · R reset · Esc pause".
|
||||
- Verified in the packaged capture: the bottom-right text bbox is 426×12 px (a single right-aligned line), attributed text bottom-left. Screenshot: `Docs/perf/0.1.0-packaged-hud-controls.png`.
|
||||
|
||||
**Verdict: PASS.**
|
||||
+13
-2
@@ -83,13 +83,24 @@ Without `git lfs install`, adding an LFS-matched file does not error — it comm
|
||||
|
||||
Sizes: the engine lives outside the repository (pinned install path recorded in `toolchain.md`). The repository holds the Unreal project, Blender sources, geo sources, and tooling.
|
||||
|
||||
**Fresh checkout not yet tested end to end.** The build procedure in [build.md](build.md) was produced from a working tree, not from a clean clone. Exercise it before `v0.1.0` is tagged — it is a release check on the board.
|
||||
**Fresh checkout verified 2026-11-08.** A clean clone of `main` with `git lfs pull` built both `GigLineEditor` and `GigLine` against the documented external engine (recorded in [release-checks.md](release-checks.md)). Re-run it before a release tag if the engine pin or the module's dependencies change.
|
||||
|
||||
### Optional keys (local only)
|
||||
|
||||
Two tools talk to paid services and read their keys from the gitignored `.env`:
|
||||
|
||||
| Variable | Used by | Needed for |
|
||||
| --- | --- | --- |
|
||||
| `OPENROUTER_API_KEY` | `Tools/judge_screenshot.py`, `Tools/facade_detail_concepts.py` | vision pre-check and AI concept plates |
|
||||
| `MAPBOX_ACCESS_TOKEN` | `Geo/tools/mapbox_buildings.py` | the second building-height source |
|
||||
|
||||
Nothing in the build, the tests or the release path needs either key. Never commit `.env`; `git check-ignore .env` should succeed.
|
||||
|
||||
## Known blockers
|
||||
|
||||
| Blocker | Owner | Needed for |
|
||||
| --- | --- | --- |
|
||||
| Delete the fallback 5.2.0 engine archive (`/run/media/system/DATA/Downloads/Linux_Unreal_Engine_5.2.0.zip`) once the 5.8.4 pin is trusted through asset production | agent | Disk hygiene (19.6 GiB), pin safety |
|
||||
| Fresh-checkout build exercised against the documented procedure | agent | `v0.1.0` release check |
|
||||
| ~~Fresh-checkout build exercised against the documented procedure~~ | — | Done 2026-11-08 |
|
||||
|
||||
Closed 2026-11-07: Epic account and engine download, QGIS `lts` removal, Blosm and MPFB on Blender 5.2 (headless invocation fix in `Art/blender/README.md`), engine validation on Bazzite, full project bootstrap. Earlier closed: Bazzite update and reboot, driver and Vulkan verification, `git-lfs`/`clang`/`cmake`/`ninja` layering.
|
||||
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.
@@ -1,6 +1,7 @@
|
||||
#include "GigLineShotActor.h"
|
||||
|
||||
#include "Camera/CameraActor.h"
|
||||
#include "Camera/CameraComponent.h"
|
||||
#include "Engine/World.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "UnrealClient.h"
|
||||
@@ -83,6 +84,15 @@ void AGigLineShotActor::NextShot()
|
||||
Params.ObjectFlags |= RF_Transient;
|
||||
CurrentCamera = GetWorld()->SpawnActor<ACameraActor>(
|
||||
ACameraActor::StaticClass(), ShotLocations[CurrentShot], ShotRotations[CurrentShot], Params);
|
||||
if (ShotFieldOfView.IsValidIndex(CurrentShot) && ShotFieldOfView[CurrentShot] > 0.0f)
|
||||
{
|
||||
if (UCameraComponent* Cam = CurrentCamera->GetCameraComponent())
|
||||
{
|
||||
// Long lens for elevations: fills the frame from across the street
|
||||
// without pitching the camera, so verticals stay parallel.
|
||||
Cam->SetFieldOfView(ShotFieldOfView[CurrentShot]);
|
||||
}
|
||||
}
|
||||
PlayerController->SetViewTarget(CurrentCamera);
|
||||
|
||||
SettleCountdown = SettleSeconds;
|
||||
|
||||
@@ -36,6 +36,16 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
TArray<FString> ShotNames;
|
||||
|
||||
/**
|
||||
* Optional per-shot field of view (degrees), same index as ShotLocations.
|
||||
* Zero or missing entries keep the engine default (90 deg). Elevation shots
|
||||
* taken from across the street need a long lens: a level camera 45 m out with
|
||||
* the default FOV leaves the building small in frame, and tilting it up to fill
|
||||
* the frame is what breaks the verticals.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
TArray<float> ShotFieldOfView;
|
||||
|
||||
/** 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;
|
||||
|
||||
@@ -507,3 +507,46 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO.
|
||||
scan: banners, windows and the parapet produce different edge sets at different
|
||||
heights and the pairwise match is meaningless (it reported a 418 px "drift").
|
||||
Verify it geometrically in the camera list instead.
|
||||
|
||||
## 2026-11-09 — 0.1.3 packaging/setup fixes
|
||||
|
||||
- **`Tools/ue_common.py`**: `make_writable()` clears the read-only bit an LFS
|
||||
`.umap` checks out with (otherwise `save_map()` returns False with no error and
|
||||
the staged actors vanish), and `require_saved()` makes that failure visible.
|
||||
`ue_hero_facade_import.py` and `ue_import_north_buildings.py` never did this —
|
||||
which is why importing the hero appeared to succeed while saving nothing.
|
||||
Verified by forcing `chmod u-w` on DemoMap first: the run logged
|
||||
`map writable: chmod u+w … (was 0o444)`, then `save_map: True`, exit 0.
|
||||
- Gotcha while adding it: `ue_hero_facade_import.py` had **no** `import os`/`sys`
|
||||
(only `unreal`), so the sys.path line raised `NameError: name 'sys' is not
|
||||
defined` inside the commandlet — an exit code of 255 with no obvious cause.
|
||||
- Docs: `build.md` gained the current pipeline table (7 site FBXs, frontage, north,
|
||||
hero, map), the `Builds/GigLineQA` archive, the packaged-log path, `-qaselftest`
|
||||
usage and a **traps** table; `setup.md` marks the fresh-checkout build verified and
|
||||
documents the two optional API keys; architecture 07 + combined doc corrected the
|
||||
build-output path.
|
||||
|
||||
## 2026-11-09 — 0.1.4 materials + architecture-camera pass
|
||||
|
||||
- **Material graphs**: `ensure_material()` now supports masonry coursing
|
||||
(`courses_m`) and roughness variation. **UE's `WorldPosition` is in
|
||||
CENTIMETRES**, so a course height in metres must be ×100 — the first attempt
|
||||
divided by 0.55 and produced coursing 100× too fine to see.
|
||||
- **`connect_material_expressions` returns False instead of raising**, so an
|
||||
unknown pin name leaves an input unconnected and the material silently compiles
|
||||
with a default (a mask of 0 is invisible). All links now go through a `link()`
|
||||
helper that logs failures. There is also **no `MaterialExpressionLess`**; use
|
||||
`Frac → Divide → OneMinus → Clamp` for a soft threshold band, or
|
||||
`MaterialExpressionStep`/`SmoothStep` if a hard one is wanted.
|
||||
- **Material assignment was slot-0-only**, which on the hero is the *banner* — the
|
||||
stone, shadowed stone and glazing kept their flat Blender materials. Assignment
|
||||
is now per slot, keyed on the imported slot material's name
|
||||
(`glass`/`banner`/`stoneshadow`/`stone`/`roof`/`wall`). Hero: 4/4 slots.
|
||||
- New materials: `M_FacadeStoneShadow` (same coursing, a stop darker),
|
||||
`M_FacadeGlass` (roughness 0.14, specular 0.62 ≈ IOR 1.5 expressed without a
|
||||
refraction pass), `M_FacadeBanner` (the one saturated surface).
|
||||
- **Camera FOV**: levelling the elevation cameras left the building small in frame
|
||||
(39 % sky). Added optional per-shot `ShotFieldOfView` to `AGigLineShotActor` and
|
||||
a 4th element on the shot tuples; a long lens (42–55°) fills the frame from
|
||||
across the street while the camera stays level, so verticals stay parallel.
|
||||
`review-hero-facade-front` sky 39 → 20 %, `demo-spawn-view` 23 → 16 %.
|
||||
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.
@@ -9,25 +9,15 @@ Actionable task list for this project. This file is the single, central place fo
|
||||
- When all steps in a version section are done: write a `CHANGELOG.md` entry, then remove the version section from this file.
|
||||
- Release gate: a version is tagged only on the owner's explicit go-ahead once its release checks pass, not automatically when the checkboxes close.
|
||||
|
||||
## 0.1.3 — Packaging and setup fixes (patch)
|
||||
|
||||
Suggested blender skills: **export-pipeline** and **unreal-export** (scale/pivot/normals/materials/collision verification) if any packaging defect traces back to the Blender export presets.
|
||||
|
||||
### Patch tasks
|
||||
|
||||
- [ ] Fix any packaging or build issues encountered in `0.1.x`.
|
||||
- [ ] Update `Docs/` setup instructions for anything that changed.
|
||||
- [ ] Tag `v0.1.3` and write `CHANGELOG.md` entry; then remove this section.
|
||||
|
||||
## 0.1.4 — Proper building façades (patch)
|
||||
|
||||
Suggested blender skills: **procedural-modeling** + **geometry-nodes** (the façade module kit — bay, pilaster, cornice, parapet, window, shopfront, door — as reusable node groups), **archviz** (real-world scale and façade composition), **materials** (procedural brick, concrete, stone, render, glass — the ported generators), **texture-workflow** (AO/curvature/normal bakes and decals for the motto band and medallions), **uv-workflow** (texel density and modular UV layouts for the kit), **scene-assembly** (assembling kit pieces onto the fifteen block buildings), **lookdev** (screenshot-comparison loop until shading reads correctly), **historical-worlds** or **realistic-style** (1920s Hollywood Blvd neoclassical direction), **asset-optimization** + **unreal-export** (pre-export validation, `ue_metric_v1` preset), **qa-review** (final gate before the `Tools/render_screenshots.sh` review).
|
||||
|
||||
### Patch tasks
|
||||
### Patch tasks - active - 01a1b26b-3c94-707f-9665-2f6b4a79b564 (branch `facades-0.1.4`)
|
||||
|
||||
- [ ] **Finish the Blosm blockout cleaning/detailing (moved from the deferred `0.1.0` item).** Import state: `Art/blender/blockout/blockout-2026-11-06.blend` (86 meshes, EPSG:26911, 0.0 cm affine residual) with cleaning tooling in `Geo/tools/` and `Art/blender/blockout/blosm_blockout.py`. To do here: (a) untextured roofs/facades → **Replace placeholder materials** below; (b) Blosm `setupScript` façade styles → **Give OSM buildings a façade style** below; (c) the 3 of 15 buildings lacking height data are the `0.1.2` placeholder-heights item.
|
||||
- [ ] **Define the façade module.** One set of reusable pieces (bay, pilaster, cornice, parapet, window, shopfront, door) at a fixed module, exported with the `ue_metric_v1` preset so every building assembles from the same kit.
|
||||
- [ ] **Replace placeholder materials.** Every blockout building currently carries Blender flat colours. Port the procedural generators from `Tools/sibling_asset_audit.py`'s sibling project (`brick_facade`, `seamless_concrete`, `asphalt`) and add stone, render, and glass, then wire them through Unreal material instances.
|
||||
- [A] **Replace placeholder materials.** **In progress 2026-11-09.** `ensure_material()` now builds masonry coursing (world-space course height + joint band + tint, corrected for UE's centimetre WorldPosition) and roughness variation; new `M_FacadeStoneShadow`, `M_FacadeGlass` (specular 0.62 ≈ IOR 1.5 without a refraction pass) and `M_FacadeBanner`; assignment is per **slot** keyed on the imported slot name (it was slot-0 only, which on the hero is the banner, so stone/glass/shadow were still flat). Hero: 4/4 slots, frontage and north buildings 2/2. Remaining: bake real texture maps (brick/stone/asphalt generators) rather than graph-only detail, and give the frontage shopfront line its own material.
|
||||
- [ ] **Give OSM buildings a façade style.** Blosm exposes `scene.blosm.setupScript` (`setup/neoclassical.py` is the example) so a building can carry a real elevation instead of a plain extrusion. Build one style script for the south frontage and one for the north.
|
||||
- [ ] **Hero façade detail pass.** Ionic capitals and fluting, the parapet motto band and medallions as geometry or decal, window rhythm behind the colonnade, and the three portal flights at their photographed proportions.
|
||||
- [ ] **El Capitan Theatre (6838) and the retail neighbours.** Model from the 18-frame 5472×3648 reference set; the theatre shares the measured 4.96 m bay rhythm with the temple.
|
||||
|
||||
@@ -16,6 +16,7 @@ Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side b
|
||||
| Tool | Purpose |
|
||||
| --- | --- |
|
||||
| `check-lfs.sh` | Fails if the `lfs` filter is missing or an LFS-tracked file was committed as a plain blob |
|
||||
| `ue_common.py` | Shared helpers for the Unreal commandlet scripts: `make_writable()` clears the read-only bit an LFS `.umap` checks out with (otherwise `save_map()` returns False silently), and `require_saved()` makes that failure explicit. |
|
||||
| `fbx_export_preset.py` | The **single** FBX export preset (`ue_metric_v1` / `ue_character_v1`): metres→centimetres via `FBX_SCALE_NONE`, axis mapping east +X / north −Y / up +Z, `mesh_smooth_type="FACE"` (smoothing groups survive the import). Every Blender exporter imports it instead of copying the argument block; `preset_kwargs()` is bpy-free and unit-tested. |
|
||||
| `sibling_asset_audit.py` | Reads the sibling project's prop library from GLB headers into `Art/props/reuse-manifest.json`; copies nothing |
|
||||
| `scale_test_export.py` / `ue_scale_test_import.py` | The 1 m = 100 uu scale test; the named presets `ue_metric_v1` live in these two files |
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Tests for Tools/ue_common.py — the shared Unreal commandlet helpers.
|
||||
|
||||
Pure stdlib: these helpers exist to turn two silent failures into loud ones, so
|
||||
they are tested without starting Unreal.
|
||||
"""
|
||||
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
sys.path.insert(0, os.path.join(ROOT, "Tools"))
|
||||
|
||||
import ue_common # noqa: E402
|
||||
|
||||
|
||||
class MakeWritableTest(unittest.TestCase):
|
||||
def test_read_only_file_is_made_writable(self):
|
||||
# Exactly how an LFS-tracked .umap checks out: 0o444.
|
||||
with tempfile.NamedTemporaryFile(delete=False) as handle:
|
||||
path = handle.name
|
||||
try:
|
||||
os.chmod(path, 0o444)
|
||||
self.assertFalse(os.access(path, os.W_OK))
|
||||
note = ue_common.make_writable(path)
|
||||
self.assertIsNotNone(note)
|
||||
self.assertIn("chmod u+w", note)
|
||||
self.assertTrue(os.access(path, os.W_OK))
|
||||
finally:
|
||||
os.chmod(path, 0o644)
|
||||
os.unlink(path)
|
||||
|
||||
def test_writable_file_reports_nothing(self):
|
||||
with tempfile.NamedTemporaryFile(delete=False) as handle:
|
||||
path = handle.name
|
||||
try:
|
||||
os.chmod(path, 0o644)
|
||||
self.assertIsNone(ue_common.make_writable(path))
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
def test_missing_file_raises(self):
|
||||
# A typo must not look like a permissions problem.
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
ue_common.make_writable(os.path.join(tmp, "absent.umap"))
|
||||
|
||||
def test_read_bits_are_preserved(self):
|
||||
with tempfile.NamedTemporaryFile(delete=False) as handle:
|
||||
path = handle.name
|
||||
try:
|
||||
os.chmod(path, 0o400)
|
||||
ue_common.make_writable(path)
|
||||
mode = os.stat(path).st_mode
|
||||
self.assertTrue(mode & stat.S_IRUSR)
|
||||
finally:
|
||||
os.chmod(path, 0o644)
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
class RequireSavedTest(unittest.TestCase):
|
||||
def test_passes_the_save_result_through(self):
|
||||
self.assertTrue(ue_common.require_saved(True, "/Game/X"))
|
||||
self.assertFalse(ue_common.require_saved(False, "/Game/X"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+135
-12
@@ -65,6 +65,13 @@ def ensure_material(mat_name, spec):
|
||||
|
||||
spec keys: dark, bright (RGB tuples), roughness, specular, noise_scale
|
||||
(world units per noise cycle; smaller = larger features).
|
||||
|
||||
Optional `courses_m` adds **masonry coursing**: horizontal joint lines at a
|
||||
given course height in world metres, plus a per-course horizontal offset so
|
||||
the vertical joints stagger like real ashlar. Optional `joint_tint` is the
|
||||
colour the joints darken toward, and `roughness_variation` lerps roughness
|
||||
between `roughness` and a rougher value using the same noise, so the surface
|
||||
does not read as one flat specular value.
|
||||
"""
|
||||
path = f"/Game/Hollywood/Site/{mat_name}"
|
||||
if unreal.EditorAssetLibrary.does_asset_exist(path):
|
||||
@@ -88,6 +95,20 @@ def ensure_material(mat_name, spec):
|
||||
n.set_editor_property("r", float(value))
|
||||
return n
|
||||
|
||||
def link(src, dst, pin=""):
|
||||
"""Connect, and say so when it fails.
|
||||
|
||||
`connect_material_expressions` returns False rather than raising, so an
|
||||
unknown pin name silently leaves an input unconnected and the material
|
||||
compiles with a default value — a coursing mask of 0 is invisible, which
|
||||
is exactly how the first attempt failed.
|
||||
"""
|
||||
ok = lib.connect_material_expressions(src, "", dst, pin)
|
||||
if not ok:
|
||||
log(f"material {mat_name}: FAILED to connect {src.__class__.__name__} "
|
||||
f"-> {dst.__class__.__name__}.{pin or '(default)'}")
|
||||
return ok
|
||||
|
||||
# world position -> noise -> lerp(dark, bright) -> base colour
|
||||
wp = node(unreal.MaterialExpressionWorldPosition)
|
||||
scale = scalar(spec.get("noise_scale", 0.004))
|
||||
@@ -108,10 +129,57 @@ def ensure_material(mat_name, spec):
|
||||
lib.connect_material_expressions(c3(spec["dark"]), "", lerp, "A")
|
||||
lib.connect_material_expressions(c3(spec["bright"]), "", lerp, "B")
|
||||
lib.connect_material_expressions(noise, "", lerp, "Alpha")
|
||||
lib.connect_material_property(lerp, "", unreal.MaterialProperty.MP_BASE_COLOR)
|
||||
|
||||
lib.connect_material_property(
|
||||
scalar(spec.get("roughness", 0.85)), "", unreal.MaterialProperty.MP_ROUGHNESS)
|
||||
# --- optional masonry coursing -----------------------------------------
|
||||
# A world-space course mask: the fractional part of Z / course height is a
|
||||
# sawtooth per course; zeroing it gives a joint line every course. The row
|
||||
# index shifts each course sideways by half a brick, so vertical joints
|
||||
# stagger. All of it is world-space, so every building shares the coursing
|
||||
# without needing its own UVs.
|
||||
base_color_expr = lerp
|
||||
if spec.get("courses_m"):
|
||||
# WorldPosition is in CENTIMETRES, so a course height given in metres must
|
||||
# be scaled by 100 here; getting this wrong makes the coursing 100x too
|
||||
# fine to see at all (the first attempt did exactly that).
|
||||
course_cm = float(spec["courses_m"]) * 100.0
|
||||
row = node(unreal.MaterialExpressionDivide)
|
||||
link(wp, row, "A")
|
||||
link(c3((course_cm, course_cm, course_cm)), row, "B")
|
||||
saw = node(unreal.MaterialExpressionFrac)
|
||||
link(row, saw)
|
||||
# Joint mask without a comparison node (UE has no Less): 1 - saw/joint is
|
||||
# 1 at the course boundary and 0 once past the joint width; clamped to 0..1
|
||||
# so it is a linear band rather than a hard step.
|
||||
div = node(unreal.MaterialExpressionDivide)
|
||||
link(saw, div, "A")
|
||||
# joint_width is a fraction of one course (0.11 ~= a 6 cm joint on a
|
||||
# 55 cm course), which is the space `saw` lives in after the divide.
|
||||
link(scalar(spec.get("joint_width", 0.11)), div, "B")
|
||||
inv = node(unreal.MaterialExpressionOneMinus)
|
||||
link(div, inv)
|
||||
joint = node(unreal.MaterialExpressionClamp)
|
||||
link(inv, joint)
|
||||
tinted = node(unreal.MaterialExpressionLinearInterpolate)
|
||||
link(lerp, tinted, "A")
|
||||
link(c3(spec.get("joint_tint", (0.34, 0.31, 0.27))), tinted, "B")
|
||||
link(joint, tinted, "Alpha")
|
||||
base_color_expr = tinted
|
||||
|
||||
lib.connect_material_property(base_color_expr, "", unreal.MaterialProperty.MP_BASE_COLOR)
|
||||
|
||||
# Roughness: lerp(base, base + variation, noise) so the highlight breaks up.
|
||||
variation = float(spec.get("roughness_variation", 0.0))
|
||||
if variation > 0.0:
|
||||
rough_lerp = node(unreal.MaterialExpressionLinearInterpolate)
|
||||
lib.connect_material_expressions(
|
||||
scalar(spec.get("roughness", 0.85)), "", rough_lerp, "A")
|
||||
lib.connect_material_expressions(
|
||||
scalar(min(1.0, spec.get("roughness", 0.85) + variation)), "", rough_lerp, "B")
|
||||
lib.connect_material_expressions(noise, "", rough_lerp, "Alpha")
|
||||
lib.connect_material_property(rough_lerp, "", unreal.MaterialProperty.MP_ROUGHNESS)
|
||||
else:
|
||||
lib.connect_material_property(
|
||||
scalar(spec.get("roughness", 0.85)), "", unreal.MaterialProperty.MP_ROUGHNESS)
|
||||
try:
|
||||
lib.connect_material_property(
|
||||
scalar(spec.get("specular", 0.3)), "", unreal.MaterialProperty.MP_SPECULAR)
|
||||
@@ -278,10 +346,35 @@ def build():
|
||||
roughness=0.98, specular=0.15, noise_scale=0.003),
|
||||
"M_RoadPaint": dict(dark=(0.74, 0.74, 0.72), bright=(0.86, 0.86, 0.84),
|
||||
roughness=0.62, specular=0.35, noise_scale=0.02),
|
||||
"M_FacadeStone": dict(dark=(0.62, 0.58, 0.50), bright=(0.74, 0.70, 0.62),
|
||||
roughness=0.72, specular=0.32, noise_scale=0.005),
|
||||
"M_Stucco": dict(dark=(0.52, 0.48, 0.42), bright=(0.66, 0.62, 0.55),
|
||||
roughness=0.86, specular=0.25, noise_scale=0.004),
|
||||
# Ashlar coursing at 0.55 m (a 0.10 m band joint), the reading the 2009
|
||||
# facade photograph gives for the hero's stone; roughness breaks up so the
|
||||
# wall is not one specular plane.
|
||||
"M_FacadeStone": dict(dark=(0.62, 0.58, 0.50), bright=(0.76, 0.72, 0.64),
|
||||
roughness=0.70, specular=0.32, noise_scale=0.005,
|
||||
courses_m=0.55, joint_width=0.11,
|
||||
joint_tint=(0.40, 0.37, 0.32), roughness_variation=0.12),
|
||||
# Rendered/stucco frontage: no coursing, but a fine tooth and a wider
|
||||
# roughness range than before, because flat stucco is what read as plastic.
|
||||
"M_Stucco": dict(dark=(0.50, 0.46, 0.40), bright=(0.68, 0.64, 0.57),
|
||||
roughness=0.76, specular=0.25, noise_scale=0.0025,
|
||||
roughness_variation=0.20),
|
||||
# Stone in shadow: the hero's recessed wall behind the colonnade. Same
|
||||
# coursing as the face, a stop darker, so depth reads without a light.
|
||||
"M_FacadeStoneShadow": dict(dark=(0.40, 0.37, 0.32), bright=(0.52, 0.49, 0.43),
|
||||
roughness=0.78, specular=0.26, noise_scale=0.005,
|
||||
courses_m=0.55, joint_width=0.11,
|
||||
joint_tint=(0.28, 0.26, 0.23),
|
||||
roughness_variation=0.10),
|
||||
# Glazing: low roughness, strong specular. A plausible IOR for glass sits
|
||||
# near 1.5, which UE expresses through specular ~0.5-0.6 on an opaque
|
||||
# surface (there is no refraction pass at blockout fidelity).
|
||||
"M_FacadeGlass": dict(dark=(0.030, 0.045, 0.055), bright=(0.075, 0.095, 0.110),
|
||||
roughness=0.14, specular=0.62, noise_scale=0.01,
|
||||
roughness_variation=0.06),
|
||||
# The JIMMY KIMMEL banners: the one saturated surface on the elevation.
|
||||
"M_FacadeBanner": dict(dark=(0.32, 0.05, 0.07), bright=(0.55, 0.10, 0.12),
|
||||
roughness=0.72, specular=0.22, noise_scale=0.02,
|
||||
roughness_variation=0.12),
|
||||
}
|
||||
SURFACE_MATERIALS = {
|
||||
"Terrain_DEM": "M_Ground",
|
||||
@@ -337,15 +430,45 @@ def build():
|
||||
LABEL_MATERIALS = {"Hero_Facade": "M_FacadeStone",
|
||||
"North_TCL_Chinese": "M_Stucco",
|
||||
"North_Madame_Tussauds": "M_Stucco"}
|
||||
|
||||
# Assign per SLOT, by what the slot's imported material is called, not just
|
||||
# slot 0: the hero mesh's slot 0 is the banner, so the stone, the shadowed
|
||||
# stone and the glazing were all still wearing their flat Blender materials.
|
||||
SLOT_RULES = (
|
||||
("glass", "M_FacadeGlass"),
|
||||
("banner", "M_FacadeBanner"),
|
||||
("stoneshadow", "M_FacadeStoneShadow"),
|
||||
("stone", "M_FacadeStone"),
|
||||
("roof", "M_Concrete"),
|
||||
("wall", "M_Stucco"),
|
||||
)
|
||||
|
||||
def slot_material_name(slot_mesh_material):
|
||||
if not slot_mesh_material:
|
||||
return None
|
||||
key = slot_mesh_material.get_name().lower()
|
||||
for needle, target in SLOT_RULES:
|
||||
if needle in key:
|
||||
return target
|
||||
return None
|
||||
|
||||
assigned = 0
|
||||
for actor in unreal.EditorLevelLibrary.get_all_level_actors():
|
||||
mat_name = LABEL_MATERIALS.get(actor.get_actor_label())
|
||||
if not mat_name or mat_name not in built:
|
||||
continue
|
||||
fallback = LABEL_MATERIALS.get(actor.get_actor_label())
|
||||
comp = actor.get_component_by_class(unreal.StaticMeshComponent)
|
||||
if comp and comp.static_mesh:
|
||||
comp.set_material(0, built[mat_name])
|
||||
if not comp or not comp.static_mesh:
|
||||
continue
|
||||
slots = comp.static_mesh.get_editor_property("static_materials")
|
||||
touched = 0
|
||||
for index, slot in enumerate(slots):
|
||||
mesh_mat = slot.get_editor_property("material_interface")
|
||||
target = slot_material_name(mesh_mat) or fallback
|
||||
if target and target in built:
|
||||
comp.set_material(index, built[target])
|
||||
touched += 1
|
||||
if touched:
|
||||
assigned += 1
|
||||
log(f"materials {actor.get_actor_label()}: {touched}/{len(slots)} slots")
|
||||
log(f"assigned look materials to {assigned} facade/background actors")
|
||||
|
||||
# --- surrounding block frontage ---------------------------------------
|
||||
|
||||
@@ -240,9 +240,9 @@ try:
|
||||
# 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)),
|
||||
("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)),
|
||||
("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.
|
||||
@@ -256,6 +256,12 @@ try:
|
||||
"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)}")
|
||||
|
||||
@@ -282,7 +288,7 @@ try:
|
||||
# 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-spawn-view", unreal.Vector(-13336, -3100, 620), unreal.Vector(-13336, 1300, 620)),
|
||||
("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
|
||||
@@ -309,6 +315,9 @@ try:
|
||||
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(
|
||||
"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)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Small shared helpers for the Unreal commandlet scripts.
|
||||
|
||||
These run inside `UnrealEditor-Cmd -run=pythonscript`, where a Python `print()`
|
||||
is swallowed and a failed save reports `False` rather than raising. Both are traps
|
||||
that cost time in `0.1.x`, so they live here once instead of in each script.
|
||||
|
||||
import os, sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import ue_common
|
||||
|
||||
ue_common.make_map_writable("/run/media/.../Content/Hollywood/DemoMap.umap")
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import stat
|
||||
|
||||
|
||||
def is_writable(path: str) -> bool:
|
||||
"""True when the current user may write `path` (existing file)."""
|
||||
return os.access(path, os.W_OK)
|
||||
|
||||
|
||||
def make_writable(path: str) -> str | None:
|
||||
"""Clear read-only bits on `path` so a save cannot fail silently.
|
||||
|
||||
Content is LFS-tracked and `.umap`/`.blend` check out read-only, which makes
|
||||
`EditorLoadingAndSavingUtils.save_map()` return False with no error — the
|
||||
staged actors then vanish on the next load. Returns a note when the mode was
|
||||
changed, `None` when it was already writable, and raises when the file is
|
||||
missing so a typo cannot masquerade as a permissions problem.
|
||||
"""
|
||||
if not os.path.exists(path):
|
||||
raise FileNotFoundError(path)
|
||||
mode = os.stat(path).st_mode
|
||||
if mode & stat.S_IWUSR:
|
||||
return None
|
||||
os.chmod(path, mode | stat.S_IWUSR | stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH |
|
||||
stat.S_IWGRP)
|
||||
return f"chmod u+w {path} (was {oct(mode & 0o777)})"
|
||||
|
||||
|
||||
def require_saved(saved: bool, what: str) -> bool:
|
||||
"""Turn a silent `save_map(False)` into a loud failure.
|
||||
|
||||
Use right after a save call: `ue_common.require_saved(saved, LEVEL)`. A
|
||||
commandlet cannot be relied on to exit non-zero, so the caller should log this
|
||||
and make the failure obvious in the result file.
|
||||
"""
|
||||
return bool(saved)
|
||||
@@ -12,8 +12,14 @@
|
||||
# centreline, from the street profile), and the hero's east edge at
|
||||
# X = -12024.5 cm (projected control point in local-origin.md).
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import unreal
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import ue_common # noqa: E402
|
||||
|
||||
FBX = "/tmp/gigline/hero-facade.fbx"
|
||||
DEST = "/Game/Hollywood/ElCapitan"
|
||||
ACTOR_LABEL = "Hero_Facade"
|
||||
@@ -56,6 +62,19 @@ def import_fbx():
|
||||
unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True)
|
||||
|
||||
|
||||
def _make_map_writable(level: str) -> None:
|
||||
"""Clear the read-only bit an LFS .umap checks out with.
|
||||
|
||||
Without this `save_map()` returns False and the staged actor is lost on the
|
||||
next load — no exception, no non-zero exit (see MEMORY: 0.1.x trap).
|
||||
"""
|
||||
content = unreal.Paths.project_content_dir()
|
||||
rel = level[len("/Game/"):] if level.startswith("/Game/") else level
|
||||
umap = os.path.join(content, rel + ".umap")
|
||||
note = ue_common.make_writable(umap)
|
||||
if note:
|
||||
log(f"map writable: {note}")
|
||||
|
||||
def measure_and_place():
|
||||
asset = unreal.EditorAssetLibrary.load_asset(f"{DEST}/SM_HeroFacade")
|
||||
if asset is None:
|
||||
@@ -99,9 +118,12 @@ def measure_and_place():
|
||||
comp.set_static_mesh(asset)
|
||||
comp.set_mobility(unreal.ComponentMobility.STATIC)
|
||||
log(f"placed {ACTOR_LABEL} at {PLACEMENT_CM} (cm) on {LEVEL}")
|
||||
_make_map_writable(LEVEL)
|
||||
saved = unreal.EditorLoadingAndSavingUtils.save_map(
|
||||
unreal.EditorLevelLibrary.get_editor_world(), LEVEL)
|
||||
log(f"save_map {LEVEL}: {saved}")
|
||||
if not ue_common.require_saved(saved, LEVEL):
|
||||
log(f"FAILED to save {LEVEL} — staged content will not persist")
|
||||
|
||||
|
||||
try:
|
||||
|
||||
@@ -15,6 +15,9 @@ import os
|
||||
|
||||
import unreal
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import ue_common # noqa: E402
|
||||
|
||||
SRC = "/tmp/gigline/north"
|
||||
DEST = "/Game/Hollywood/North"
|
||||
LEVEL = "/Game/Hollywood/DemoMap"
|
||||
@@ -72,11 +75,27 @@ def stage():
|
||||
mn, mx = box.min, box.max
|
||||
log(f"{label}: mesh world-space X {mn.x:.0f}..{mx.x:.0f} Y {mn.y:.0f}..{mx.y:.0f} size "
|
||||
f"{mx.x-mn.x:.0f}x{mx.y-mn.y:.0f} cm")
|
||||
_make_map_writable(LEVEL)
|
||||
saved = unreal.EditorLoadingAndSavingUtils.save_map(
|
||||
unreal.EditorLevelLibrary.get_editor_world(), LEVEL)
|
||||
log(f"save_map {LEVEL}: {saved}")
|
||||
if not ue_common.require_saved(saved, LEVEL):
|
||||
log(f"FAILED to save {LEVEL} — staged content will not persist")
|
||||
|
||||
|
||||
def _make_map_writable(level: str) -> None:
|
||||
"""Clear the read-only bit an LFS .umap checks out with.
|
||||
|
||||
Without this `save_map()` returns False and the staged actor is lost on the
|
||||
next load — no exception, no non-zero exit (see MEMORY: 0.1.x trap).
|
||||
"""
|
||||
content = unreal.Paths.project_content_dir()
|
||||
rel = level[len("/Game/"):] if level.startswith("/Game/") else level
|
||||
umap = os.path.join(content, rel + ".umap")
|
||||
note = ue_common.make_writable(umap)
|
||||
if note:
|
||||
log(f"map writable: {note}")
|
||||
|
||||
def main():
|
||||
for fn, asset_name in IMPORTS:
|
||||
task = unreal.AssetImportTask()
|
||||
|
||||
Reference in new issue
Block a user