3 Commits
Author SHA1 Message Date
zwitschi bbb373ec06 memory: detail-plate toolchain, prompt-refusal gotcha, floating-window fix 2026-11-08 17:27:32 +01:00
zwitschi abace2ad05 feat(tools): AI detail plates from the reference photography; fix floating windows
Adds Tools/facade_detail_concepts.py: generates architectural detail plates from a
local reference photograph through OpenRouter's image models. Spec-driven (eight
studies: door recess, entrance, column order, entablature/cornice, parapet/motto,
window rhythm, street wall, banner), never overwrites (writes -vN), writes a
.refgen.json sidecar (model, prompt, source photo, SHA-256) per image, and keeps
all output under the gitignored Art/reference/facade-details/ - local only, never
shipped. Stdlib only; http 429/5xx back-off; refuses to write a sidecar containing
the token. 11 tests cover the study table, prompt assembly, next-free naming,
data-URL decoding (both response shapes plus a refusal) and the local-only rule.

Measurement pass on the plates: the hero window panes were at Y -1.98, i.e. 1.44 m
in FRONT of the wall (Y -0.50) and ahead of the column line, so the glazing floated
in the colonnade instead of reading as openings behind it. Moved to Y -0.56; hero
rebuilt, re-imported, DemoMap rebuilt, full screenshot set re-rendered.

Docs: Tools/README row, Screenshots README known-issues entry now records the fix.
2026-11-08 17:27:27 +01:00
zwitschi 8851b2272e fix(hero): lift the colonnade into the photographed 50-60% band
Measured from the built mesh, the colonnade (columns + capitals) was 6.90 m of the
14.50 m elevation = 47.6 %: the entablature (3.40 m) and parapet (2.20 m) were
using 5.60 m of the height. Thinned both to 3.00 m and 1.80 m in
hero_facade_build.py, which lifts the colonnade to 7.70 m = 53.1 % - inside the
50-60 % band - with the envelope, motto band and medallions unchanged relative to
the parapet.

Hero rebuilt, re-imported (bounds 2604 x 1257 x 1450 cm), DemoMap rebuilt and the
screenshot set re-rendered for review. Hero part report updated (columns 7.45 ->
8.25 m, entablature 3.40 -> 3.00 m).

TODO: colonnade item closed. MEMORY records the FBX-vs-blend --out gotcha.
2026-11-08 16:46:30 +01:00
68 changed files with 499 additions and 973 deletions

No files matched your search

Binary file not shown.
+8 -35
View File
@@ -3,18 +3,18 @@
"frame": "project frame, metres, X east Y north Z up", "frame": "project frame, metres, X east Y north Z up",
"bounds_m": { "bounds_m": {
"min": [ "min": [
-13.08, -13.02,
-12.5, -12.5,
0.0 0.0
], ],
"max": [ "max": [
13.08, 13.02,
0.13, 0.07,
14.5 14.5
], ],
"size": [ "size": [
26.16, 26.04,
12.63, 12.57,
14.5 14.5
] ]
}, },
@@ -37,24 +37,6 @@
], ],
"evidence": "6 columns at 4.96 m centres (sibling notes x measured frontage)" "evidence": "6 columns at 4.96 m centres (sibling notes x measured frontage)"
}, },
{
"object": "Hero_ColumnBase_x6",
"dims_m": [
1.36,
1.36,
0.22
],
"evidence": "assumption: Ionic base moulding; photographs show a wider base at the shaft foot"
},
{
"object": "Hero_Abacus_x6",
"dims_m": [
1.3,
1.3,
0.14
],
"evidence": "assumption: square abacus under the architrave, as Ionic capitals carry"
},
{ {
"object": "Hero_Architrave", "object": "Hero_Architrave",
"dims_m": [ "dims_m": [
@@ -73,15 +55,6 @@
], ],
"evidence": "assumption: projecting cornice line" "evidence": "assumption: projecting cornice line"
}, },
{
"object": "Hero_Dentil_xN",
"dims_m": [
0.18,
0.3,
0.22
],
"evidence": "52 dentils at 0.45 m centres under the corona; assumption: photographed cornice reads as a dentilled course"
},
{ {
"object": "Hero_Entablature", "object": "Hero_Entablature",
"dims_m": [ "dims_m": [
@@ -113,10 +86,10 @@
"object": "Hero_Medallion_x2", "object": "Hero_Medallion_x2",
"dims_m": [ "dims_m": [
1.1, 1.1,
0.04, 1.1,
1.1 0.02
], ],
"evidence": "photographs: two stone discs beside the motto, mounted on the parapet face" "evidence": "photographs: two stone discs beside the motto"
}, },
{ {
"object": "Hero_Stylobate", "object": "Hero_Stylobate",
+14 -4
View File
@@ -15,9 +15,6 @@ import os
import sys import sys
import bpy import bpy
sys.path.insert(0, os.path.join(os.path.join(os.path.dirname(__file__), "..", "..", ".."), "Tools"))
import fbx_export_preset as fbx_preset # noqa: E402
MESHES = ("Terrain_DEM", "Road_Hollywood", "Sidewalk_South", "Sidewalk_North", MESHES = ("Terrain_DEM", "Road_Hollywood", "Sidewalk_South", "Sidewalk_North",
"Kerb_South", "Kerb_North", "Crossing_Markings") "Kerb_South", "Kerb_North", "Crossing_Markings")
@@ -63,7 +60,20 @@ def main():
_ = bpy.context.view_layer _ = bpy.context.view_layer
bpy.context.view_layer.objects.active = obj bpy.context.view_layer.objects.active = obj
path = os.path.join(args.out, name + ".fbx") path = os.path.join(args.out, name + ".fbx")
fbx_preset.export_static(path, selection=True) bpy.ops.export_scene.fbx(
filepath=path,
use_selection=True,
object_types={"MESH"},
apply_unit_scale=True,
apply_scale_options="FBX_SCALE_NONE",
axis_forward="-Y",
axis_up="Z",
use_space_transform=True,
use_mesh_modifiers=True,
mesh_smooth_type="OFF",
path_mode="AUTO",
bake_space_transform=False,
)
if mirrored: if mirrored:
# The flip is an export correction, not a change to the source: undo it. # The flip is an export correction, not a change to the source: undo it.
obj.scale.y *= -1.0 obj.scale.y *= -1.0
+86 -88
View File
@@ -8,129 +8,127 @@ Format: [Keep a Changelog](https://keepachangelog.com/) style, versions follow [
Nothing yet. 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.
### Added
- **`Tools/facade_detail_concepts.py`** — generates frontal building plates (whole elevation, entrance in its façade, building with neighbours) from a local reference photograph through OpenRouter image models. Spec-driven, never overwrites, writes a provenance sidecar per image, output stays in the gitignored `Art/reference/` tree. Local working material only.
- **`Tools/fbx_export_preset.py`** — the single FBX export preset (`ue_metric_v1` / `ue_character_v1`). Replaces the argument block that had been copied into six exporters.
- **Façade occlusion probe** in `-qaselftest`: 40 m traces sideways from the road centre at walking height, required wherever a building is staged.
- **Ionic detail on the hero**: base moulding at every shaft foot, a square abacus over every capital, and a 52-dentil cornice course.
- **Tool tests**: `Tools/tests/` now covers the input driver, height classification, the Mapbox cross-check, the concept generator and the FBX preset (49 tests).
### Changed
- **Storey height 3.2 → 3.1 m**, calibrated against Mapbox building data, with explicit overrides for three rear outbuildings; the block cross-check now reports 66 matches and no disagreements.
- **Hero colonnade 47.6 % → 53.1 %** of the 14.50 m elevation, inside the photographed 50–60 % band (thinner entablature and parapet).
- Blockout regenerated at the new storey height; frontage re-exported and re-imported (6922 42.1 → 39.1 m, El Capitan Theatre 20.0 → 20.5 m).
- FBX exports now carry smoothing groups (`FACE` instead of `OFF`), so round geometry is no longer faceted and imports stop warning.
### Fixed
- **Windows floated 1.44 m in front of the wall**, ahead of the colonnade; they now sit on the wall plane behind the columns.
- **Parapet medallions were flat discs lying on the coping** instead of discs on the parapet face.
- **The hero façade had no usable collision** — the player could walk through it. It is now complex-as-simple; auto-generated convex hulls were tried and rejected because they bulged into the street.
- **The three "guessed" block heights were never buildings**: two `building=roof` canopies and a transit shelter. They are classified as non-building volumes and the block now reports zero guessed heights.
### Verified
- Packaged build `-qaselftest`: walk, reset and pause pass; seam transect 1456 samples / 0 holes; wall probe 0/35; façade probe 0/9.
- Screenshots re-rendered for every change and approved by the owner.
### Known limitations
- Cornice dentil spacing, the base and abacus dimensions are recorded assumptions: the reference photography does not resolve them at our scale.
- Façade surfaces remain flat blockout materials; the textured pass is `0.1.4`.
## [0.1.1] — 2026-11-08 ## [0.1.1] — 2026-11-08
Collision-fidelity patch. No new scope: street geometry corrections and the QA tooling that measures them. Collision-fidelity patch. No new scope: street geometry corrections and the QA
tooling that measures them.
### Fixed ### Fixed
- **Sidewalk outer edge ledge.** The sidewalk stood the full 0.15 m kerb height above the DEM at both edges, leaving a 15.8 cm lip where the walk route crossed from terrain onto the south sidewalk. The upstand now tapers from full at the road-side edge to zero at the building-side edge; the north route's worst step fell from 15.1 cm to 6.9 cm. - **Sidewalk outer edge ledge.** The sidewalk stood the full 0.15 m kerb height
- **Crossing ramps too steep.** The kerb-flush run was a fixed 1.5 m, giving ~8.7 % (measured 10–13 cm over 1.5 m). The run is now derived from a target grade (`--max-ramp-grade`, default 8 % ⇒ 1.63 m); the crossing steps 3.4 cm at 0.25 m sampling. above the DEM at both edges, leaving a 15.8 cm lip where the walk route crossed
from terrain onto the south sidewalk. The upstand now tapers from full at the
road-side edge to zero at the building-side edge; the north route's worst step
fell from 15.1 cm to 6.9 cm.
- **Crossing ramps too steep.** The kerb-flush run was a fixed 1.5 m, giving
~8.7 % (measured 10–13 cm over 1.5 m). The run is now derived from a target
grade (`--max-ramp-grade`, default 8 % ⇒ 1.63 m); the crossing steps 3.4 cm at
0.25 m sampling.
### Added ### Added
- **Walk pre-check improvements** (`Art/blender/site/site_assembly.py`): sampling coverage is independent of `--sample` (it previously shrank the route), and the report now prints the grade and the arc-length/offset **location** of the worst step, so a step can be located rather than guessed. - **Walk pre-check improvements** (`Art/blender/site/site_assembly.py`): sampling
coverage is independent of `--sample` (it previously shrank the route), and the
report now prints the grade and the arc-length/offset **location** of the worst
step, so a step can be located rather than guessed.
### Changed ### Changed
- **Kerb height recorded as final** at 0.15 m modelled / 0.13 m as built — a standard 6 in LA kerb; neither the 3DEP DEM nor the reference photography can resolve it further (±2 cm), which is far below the character's 45 cm step height. - **Kerb height recorded as final** at 0.15 m modelled / 0.13 m as built — a
standard 6 in LA kerb; neither the 3DEP DEM nor the reference photography can
resolve it further (±2 cm), which is far below the character's 45 cm step
height.
### Verified ### Verified
- Site meshes rebuilt and re-imported (7/7, collision flags intact), DemoMap rebuilt, packaged Development build `EXIT=0`. - Site meshes rebuilt and re-imported (7/7, collision flags intact), DemoMap
- `-qaselftest` on the packaged build: walk 761 cm in 2 s at steady Z 456, reset 0 cm, pause/unpause, transect 1456 samples across 16 rows / **0 holes**, wall probe **0/35** blocking hits. rebuilt, packaged Development build `EXIT=0`.
- `-qaselftest` on the packaged build: walk 761 cm in 2 s at steady Z 456, reset
0 cm, pause/unpause, transect 1456 samples across 16 rows / **0 holes**, wall
probe **0/35** blocking hits.
### Known limitations ### Known limitations
- One residual **13.4 cm surface step at s≈198 m** on the south sidewalk (8.0 cm at s≈251 m north). It is not a gap and is below the 45 cm `MaxStepHeight`, so it does not block traversal; the cause is a sidewalk-strip discontinuity near a centreline vertex, recorded in `Art/blender/README.md` as a smoothness refinement. - One residual **13.4 cm surface step at s≈198 m** on the south sidewalk (8.0 cm
- Kerb radii at junctions, driveway aprons, drainage inlets and pavement joints remain unmodelled (unchanged from `0.1.0`). at s≈251 m north). It is not a gap and is below the 45 cm `MaxStepHeight`, so it
does not block traversal; the cause is a sidewalk-strip discontinuity near a
centreline vertex, recorded in `Art/blender/README.md` as a smoothness
refinement.
- Kerb radii at junctions, driveway aprons, drainage inlets and pavement joints
remain unmodelled (unchanged from `0.1.0`).
Nothing yet. Nothing yet.
## [0.1.0] — 2026-11-08 ## [0.1.0] — 2026-11-08
The first playable release: a walkable exterior demo of the **6840 Hollywood Blvd** (El Capitan Entertainment Centre) block in Unreal Engine, built on OpenStreetMap layout data and USGS elevation, with the hero façade and the verified block frontage staged on a walkable street. The first playable release: a walkable exterior demo of the **6840 Hollywood Blvd**
(El Capitan Entertainment Centre) block in Unreal Engine, built on OpenStreetMap
layout data and USGS elevation, with the hero façade and the verified block
frontage staged on a walkable street.
### Added ### Added
- **Unreal project** (`Game/GigLine`) — C++ game module, Enhanced Input, packaged Linux build, and a `DemoMap` that a plain launch opens directly. - **Unreal project** (`Game/GigLine`) — C++ game module, Enhanced Input, packaged
- **Walkable demo level** — terrain from the 3DEP DEM, Hollywood Blvd carriageway, both sidewalks and kerbs with collision, the designated mid-block crossing, and fixed daytime lighting (sun, sky light, sky atmosphere, height fog, pinned exposure). Linux build, and a `DemoMap` that a plain launch opens directly.
- **Hero façade** for 6840 Hollywood Blvd — six-column neoclassical elevation with three genuine portal recesses, entablature, parapet and motto band (`Tools/hero_facade_build.py`, `Tools/ue_hero_facade_import.py`). - **Walkable demo level** — terrain from the 3DEP DEM, Hollywood Blvd carriageway,
- **Block frontage** — the five verified south-side neighbours (6922, 6902, 6838 El Capitan Theatre, 6822 Budget, 6806–6822, 6800 Lee Drug) and the two north-side buildings (TCL Chinese Theatre, Madame Tussaud's) placed at their OSM positions. both sidewalks and kerbs with collision, the designated mid-block crossing, and
- **First-person player** — `AGigLineCharacter`: 1.75 m capsule, 1.60 m eye camera, 4 m/s walk, mouse look, jump, reset (`R`) and pause (`Escape`); controls documented in `Docs/controls.md` and shown in-game. fixed daytime lighting (sun, sky light, sky atmosphere, height fog, pinned
- **In-game ODbL attribution** — `AGigLineHUD` draws the OpenStreetMap credit and licence line every frame. exposure).
- **QA harness** — `-qaselftest` (walk, reset, pause, seam transect, invisible-wall probe, frame-time capture) and `Tools/input_test.py` / `Tools/input_emulation.py` for OS-level virtual keyboard/mouse input. - **Hero façade** for 6840 Hollywood Blvd — six-column neoclassical elevation with
- **Documentation** — setup, build, toolchain, licensing, ODbL review, release checks, and arc42 architecture docs. three genuine portal recesses, entablature, parapet and motto band
(`Tools/hero_facade_build.py`, `Tools/ue_hero_facade_import.py`).
- **Block frontage** — the five verified south-side neighbours (6922, 6902, 6838
El Capitan Theatre, 6822 Budget, 6806–6822, 6800 Lee Drug) and the two north-side
buildings (TCL Chinese Theatre, Madame Tussaud's) placed at their OSM positions.
- **First-person player** — `AGigLineCharacter`: 1.75 m capsule, 1.60 m eye camera,
4 m/s walk, mouse look, jump, reset (`R`) and pause (`Escape`); controls
documented in `Docs/controls.md` and shown in-game.
- **In-game ODbL attribution** — `AGigLineHUD` draws the OpenStreetMap credit and
licence line every frame.
- **QA harness** — `-qaselftest` (walk, reset, pause, seam transect, invisible-wall
probe, frame-time capture) and `Tools/input_test.py` / `Tools/input_emulation.py`
for OS-level virtual keyboard/mouse input.
- **Documentation** — setup, build, toolchain, licensing, ODbL review, release
checks, and arc42 architecture docs.
### Fixed ### Fixed
- **All controls were dead in packaged and PIE builds.** The mapping-context generator wrote key bindings to the deprecated `mappings` array; UE 5.8 reads `DefaultKeyMappings.Mappings`, so the cooked game had zero runtime mappings and every key resolved to nothing. `Tools/ue_create_input_assets.py` now populates the current field. - **All controls were dead in packaged and PIE builds.** The mapping-context
- **"Fall-through" was an 8 m invisible box.** `Terrain_DEM` shipped only a bounding-box collision proxy; forcing complex-as-simple collision (`UGigLineAssetTools`) put the player on the actual street. generator wrote key bindings to the deprecated `mappings` array; UE 5.8 reads
- **Reset returned the player to mid-air** — the start position is now captured on the first grounded frame. `DefaultKeyMappings.Mappings`, so the cooked game had zero runtime mappings and
- **Plain launch opened the engine template** — the default map now lives in `DefaultEngine.ini` and points at `DemoMap`. every key resolved to nothing. `Tools/ue_create_input_assets.py` now populates
- **Game target would not compile** — removed editor-only API use (`UStaticMesh::PostEditChange`, `AActor::GetActorLabel`). the current field.
- **`demo-north-buildings` screenshot** framed 85 % sky (camera 9.5 km west of the buildings); re-pointed to the building mass. - **"Fall-through" was an 8 m invisible box.** `Terrain_DEM` shipped only a
bounding-box collision proxy; forcing complex-as-simple collision
(`UGigLineAssetTools`) put the player on the actual street.
- **Reset returned the player to mid-air** — the start position is now captured on
the first grounded frame.
- **Plain launch opened the engine template** — the default map now lives in
`DefaultEngine.ini` and points at `DemoMap`.
- **Game target would not compile** — removed editor-only API use
(`UStaticMesh::PostEditChange`, `AActor::GetActorLabel`).
- **`demo-north-buildings` screenshot** framed 85 % sky (camera 9.5 km west of the
buildings); re-pointed to the building mass.
### Verified ### Verified
- Both sides of the demo are traversable: 1456-sample seam transect, 0 holes, no blocking collision or invisible walls (`-qaselftest` on the packaged build). - Both sides of the demo are traversable: 1456-sample seam transect, 0 holes,
- Placement and street scale match the reference photography and Geo measurements (road 24.3 m, sidewalk offsets 12.5/13.2 m, hero frontage 26.0 m). no blocking collision or invisible walls (`-qaselftest` on the packaged build).
- **414 fps average / p99 3.01 ms at 2560×1440** (Epic preset, TSR, VSync off, no frame generation) — far above the 60 fps target. - Placement and street scale match the reference photography and Geo
- Fresh checkout builds and packages with the documented external engine (UE 5.8.4). measurements (road 24.3 m, sidewalk offsets 12.5/13.2 m, hero frontage 26.0 m).
- **414 fps average / p99 3.01 ms at 2560×1440** (Epic preset, TSR, VSync off, no
frame generation) — far above the 60 fps target.
- Fresh checkout builds and packages with the documented external engine
(UE 5.8.4).
### Known limitations ### Known limitations
- Buildings other than the hero are flat blockout volumes; façade detail, materials and Blosm style scripts are planned for `0.1.4`. - Buildings other than the hero are flat blockout volumes; façade detail,
- 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. materials and Blosm style scripts are planned for `0.1.4`.
- 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. - One block only; no vehicles, NPCs, audio, or settings UI yet.
[Unreleased]: https://git.allucanget.biz/zwitschi/gig-line/compare/v0.1.3...main [Unreleased]: https://git.allucanget.biz/zwitschi/gig-line/compare/v0.1.1...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.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 [0.1.0]: https://git.allucanget.biz/zwitschi/gig-line/releases/tag/v0.1.0
+1 -1
View File
@@ -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 | | 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) | | 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`) | | 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/GigLineQA/Linux` (packaged Development build, ~1.1 GiB; outside the repository) | | Build output | `/run/media/system/DATA/Builds/GigLine` (packaged Linux build, ~1.1 GiB) |
Motivation Motivation
Single-machine development. No build farm, no CI pipeline yet. The engine, toolchain, and GPU are all on the same host. Single-machine development. No build farm, no CI pipeline yet. The engine, toolchain, and GPU are all on the same host.
+1 -1
View File
@@ -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 | | 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) | | 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`) | | 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/GigLineQA/Linux` (packaged Development build, ~1.1 GiB; outside the repository) | | Build output | `/run/media/system/DATA/Builds/GigLine` (packaged Linux build, ~1.1 GiB) |
Motivation Motivation
Single-machine development. No build farm, no CI pipeline yet. The engine, toolchain, and GPU are all on the same host. Single-machine development. No build farm, no CI pipeline yet. The engine, toolchain, and GPU are all on the same host.
+37 -45
View File
@@ -1,6 +1,6 @@
# Build # Build
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). 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.
## Constraints ## Constraints
@@ -10,7 +10,8 @@ Fresh-checkout build, content-pipeline and packaging procedure for the walkable
## Engine ## 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 ## Fresh-checkout build
@@ -41,14 +42,20 @@ cd /run/media/system/DATA/Engines/UE_5.8.4
Engine/Build/BatchFiles/RunUAT.sh BuildCookRun \ Engine/Build/BatchFiles/RunUAT.sh BuildCookRun \
-project="/run/media/system/DATA/Documents/06-games/gig-line/Game/GigLine/GigLine.uproject" \ -project="/run/media/system/DATA/Documents/06-games/gig-line/Game/GigLine/GigLine.uproject" \
-noP4 -platform=Linux -clientconfig=Development \ -noP4 -platform=Linux -clientconfig=Development \
-cook -build -stage -pak \ -cook -allmaps -build -stage -pak \
-archive -archivedirectory="/run/media/system/DATA/Builds/GigLineQA" \ -archive -archivedirectory="/run/media/system/DATA/Builds/GigLine" \
-unattended -unattended
# Expected: "AutomationTool exiting with ExitCode=0 (Success)" and "EXIT=0", ~90 s # Expected: "BUILD SUCCESSFUL" (~1 min for the blank project; archive ~1.1 GiB)
# Launch test (plain, no -map): /run/media/system/DATA/Builds/GigLineQA/Linux/GigLine.sh # Launch test (plain, no -map): /run/media/system/DATA/Builds/GigLine/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 ## Headless content checks
@@ -69,7 +76,9 @@ Both content-acceptance tests run headless and are the template for all asset im
## Content pipeline (current) ## 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 | | Step | Tool | Output |
| --- | --- | --- | | --- | --- | --- |
@@ -77,23 +86,21 @@ The hero block and the demo level are built by a fixed sequence. Each step write
| Terrain | `Art/blender/terrain/terrain_from_dem.py` | `Art/blender/terrain/terrain-*.blend` | | 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` | | 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 assembly | `Art/blender/site/site_assembly.py` | `Art/blender/site/site-*.blend` |
| Site export | `Art/blender/site/export_site_fbx.py` | seven FBXs, one per mesh (terrain, road, both sidewalks, both kerbs, crossing) | | 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 part report | | Hero façade | `Tools/hero_facade_build.py` | `Art/blender/hero/hero-facade-*.blend` + JSON 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/*` | | 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` | | Hero import | `Tools/ue_hero_facade_import.py` | `/Game/Hollywood/ElCapitan/SM_HeroFacade` |
| Demo map | `Tools/ue_build_demo_map.py` | `/Game/Hollywood/DemoMap` | | Demo map | `Tools/ue_build_demo_map.py` | `/Game/Hollywood/DemoMap` |
| Review shots | `Tools/ue_build_review_map.py` + `Tools/render_screenshots.sh` | `Screenshots/` | | 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: 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. - **One conversion.** Metres to Unreal centimetres happens only in
- **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. `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) ## Known engine quirks (2026-11-07)
@@ -103,35 +110,20 @@ Two rules that the pipeline enforces rather than documents:
## Packaged Linux build ## Packaged Linux build
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. - 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).
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) ## 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: - Interpretation:
- PIE **works** -> the Enhanced Input wiring is fine and the standalone packaged build's SDL input intake is the environment-dependent bug. - PIE **works** -> the Enhanced Input wiring is fine and the standalone
- PIE **fails** too -> the Enhanced Input setup itself is broken; rebuild the input config / character from scratch. packaged build's SDL input intake is the environment-dependent bug.
- 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 **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`.
+12 -6
View File
@@ -1,6 +1,7 @@
# Controls (`0.1.0` walkable demo) # 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 | | Action | Key | Notes |
| --- | --- | --- | | --- | --- | --- |
@@ -15,16 +16,21 @@ Keyboard and mouse. The same list is shown in-game, bottom-right of the frame (`
## Notes ## Notes
- Movement is relative to where you are looking (controller yaw), so the capsule stays upright on the sloped street. - Movement is relative to where you are looking (controller yaw), so the capsule
- The mouse is captured on launch, so looking works without clicking first. `Escape` toggles the capture as part of pause. stays upright on the sloped street.
- Bindings are Enhanced Input actions in `/Game/Input` built by `Tools/ue_create_input_assets.py`; the mapping context is `IMC_Default`. - 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 ## 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 ```bash
python3 Tools/input_test.py --sequence move:2,look:3 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.
+26 -9
View File
@@ -37,12 +37,22 @@ Important nuance: ODbL governs the **database** (the OSM data as a collection an
## 4. Decision: posture for the derived geometry ## 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. - The **share-alike obligation is accepted for the data layer only**. The
- **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. release must carry: (a) the on-screen attribution (implemented), (b) the ODbL
- **No new obligation is created by the DEM**: 3DEP is US Government public domain and is independent of ODbL. link (implemented), and (c) an offer of the OSM source for the geometry —
- **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). 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: Release checklist items this lands:
@@ -53,13 +63,20 @@ Release checklist items this lands:
## 5. Remaining (tracked separately) ## 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). - **Blosm add-on licence** is still `pending` in THIRD_PARTY_NOTICES (tool-only,
- Façade signage/trademarks: separate rights from the photograph, own the geometry; not an ODbL question (see THIRD_PARTY_NOTICES "Reference photography" rules). 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 ## 6. Sources checked
- ODbL text: <https://opendatacommons.org/licenses/odbl/1-0/> - ODbL text: <https://opendatacommons.org/licenses/odbl/1-0/>
- OSM licence summary: <https://www.openstreetmap.org/copyright> - 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`.
+68 -26
View File
@@ -35,7 +35,7 @@ Measured at 1440p on the development workstation: Bazzite, Ryzen 7 7700X, 64 GB
## Check-record template ## Check-record template
| Version | Check | Result | Evidence | Date | | 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` | 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` | 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` | 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 |
@@ -45,7 +45,10 @@ Measured at 1440p on the development workstation: Bazzite, Ryzen 7 7700X, 64 GB
## `0.1.0` — Placement vs references — record (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) ### Placement and scale (in-engine ↔ reference)
@@ -77,33 +80,55 @@ Checked against the reference photography (`Art/reference/`) and the Geo measure
- **Crossing ramps are kerb flushes, not graded ramps** — the `0.1.1` Crossing ramp gradient item. - **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. - 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) ## `0.1.0` — Collision and packaging — audit note (2026-11-08)
Run on the packaged build (`GigLineQA`, Development, Vulkan SM5) with `-qaselftest`. 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.) - **Transect (holes/gaps):** 1456 samples over 16 rows — lane centres **plus the
- **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. kerb/road/sidewalk seam Ys** (−1382, −1053, +718, +1047 and neighbours) and
- **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.) X −20000..−2000. Result **0 holes**. (The old 84-sample check covered only 3
- **Walk / reset / pause:** 761 cm in 2 s (Z steady 456), reset 0 cm, pause `IsGamePaused()=1`, unpause 0 — all PASS. lane centres; the seam rows are what this check adds.)
- 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. - **Invisible-wall probe:** horizontal traces 30 m long, grounded at 160 cm above
- 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). 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.
(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) ## `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:** **Result at 2560×1440:**
| Metric | Value | | Metric | Value |
| --------------- | --------------------- | | --- | --- |
| Frames captured | 829 | | Frames captured | 829 |
| Average | **2.42 ms → 414 fps** | | Average | **2.42 ms → 414 fps** |
| p50 | 2.38 ms | | p50 | 2.38 ms |
@@ -111,9 +136,13 @@ Run on the packaged build (`GigLineQA`, Development, Vulkan SM5) with `-qaselfte
| p99 | 3.01 ms (≈332 fps) | | p99 | 3.01 ms (≈332 fps) |
| Max | 4.00 ms | | 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:** **Reproduce:**
@@ -127,7 +156,8 @@ Committed frame-time series: `Docs/perf/0.1.0-walk-frametimes.csv`.
## `0.1.0` — Fresh checkout build — record (2026-11-08) ## `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 ```bash
git clone https://git.allucanget.biz/zwitschi/gig-line.git /tmp/fresh-check git clone https://git.allucanget.biz/zwitschi/gig-line.git /tmp/fresh-check
@@ -140,23 +170,35 @@ Engine/Build/BatchFiles/Linux/Build.sh GigLine Linux Development \
-project=/tmp/fresh-check/Game/GigLine/GigLine.uproject -WaitMutex -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). - Clone of `main` at `4bc41e9`; LFS objects fetched (`DemoMap.umap` is real UE
- `GigLineEditor` → **Result: Succeeded** (28.2 s); produced `/tmp/fresh-check/Game/GigLine/Binaries/Linux/libUnrealEditor-GigLine.so`. 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). - `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.** **Verdict: PASS.**
## `0.1.0` — Packaged Linux build + documented controls — record (2026-11-08) ## `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:** **Controls documented and shown in-game:**
- `Docs/controls.md` — full binding table (WASD, mouse look, Space, R, Esc) plus
- `Docs/controls.md` — full binding table (WASD, mouse look, Space, R, Esc) plus how to test input headlessly. 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". - `AGigLineHUD` now draws a control legend bottom-right every frame:
- 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`. "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.** **Verdict: PASS.**
+2 -13
View File
@@ -83,24 +83,13 @@ 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. 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 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. **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.
### 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 ## Known blockers
| Blocker | Owner | Needed for | | 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 | | 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~~ | — | Done 2026-11-08 | | Fresh-checkout build exercised against the documented procedure | agent | `v0.1.0` release check |
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. 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.
Binary file not shown.
@@ -416,81 +416,6 @@ void AGigLineCharacter::StartQASelfTest()
TEXT("GigLine QA: wall probe %d/%d traces with blocking hit inside 21 m -> %s"), TEXT("GigLine QA: wall probe %d/%d traces with blocking hit inside 21 m -> %s"),
Walls, WallSamples, Walls, WallSamples,
Walls == 0 ? TEXT("PASS") : TEXT("FAIL")); Walls == 0 ? TEXT("PASS") : TEXT("FAIL"));
// Facade occlusion probe: walkable street is only half the story —
// the player must also NOT be able to walk through the buildings.
// From the road centre, fire 40 m sideways at walking height toward
// each frontage and require a blocking hit. A miss means the
// building has no collision (or only non-blocking collision) and the
// player would pass through the wall.
// Expect a wall only where the map actually stages a building.
// Spans below mirror the actors placed by Tools/ue_build_demo_map.py
// (frontage + hero) and the two north buildings; update them if
// that list changes. Gaps between buildings are legitimate misses.
struct FSpan { float Min, Max; };
static const FSpan SouthSpans[] = {
{-24276.f, -17700.f}, // 6922
{-17749.f, -14591.f}, // 6902
{-14638.f, -12034.f}, // 6840 hero
{-12082.f, -8210.f}, // 6838 El Capitan Theatre
{-7854.f, -7252.f}, // 6822 Budget
{-6608.f, -3307.f}, // 6806-6820
{-3320.f, -1437.f}, // 6800 Lee Drug
};
static const FSpan NorthSpans[] = {
{-27071.f, -22799.f}, // Madame Tussaud's
{-22889.f, -18442.f}, // TCL Chinese Theatre
};
auto InSpans = [](const FSpan* Spans, int32 Count, float X)
{
for (int32 i = 0; i < Count; ++i)
{
if (X >= Spans[i].Min && X <= Spans[i].Max) { return true; }
}
return false;
};
int32 Facades = 0, FacadeSamples = 0;
for (float X0 = -19000.0f; X0 <= -3000.0f; X0 += 2000.0f)
{
for (float Dir : {1.0f, -1.0f}) // +Y south frontage, -Y north side
{
const bool bExpectWall = (Dir > 0.0f)
? InSpans(SouthSpans, UE_ARRAY_COUNT(SouthSpans), X0)
: InSpans(NorthSpans, UE_ARRAY_COUNT(NorthSpans), X0);
if (!bExpectWall)
{
continue;
}
FHitResult GroundHit2;
FCollisionQueryParams GQ2(TEXT("GigLineFacadeGround"), false, this);
if (!W->LineTraceSingleByChannel(GroundHit2,
FVector(X0, 0.0f, 3000.f), FVector(X0, 0.0f, -1000.f),
ECC_Visibility, GQ2))
{
continue;
}
const float ProbeZ2 = GroundHit2.ImpactPoint.Z + 160.f;
FHitResult Hit2;
FCollisionQueryParams Q2(TEXT("GigLineFacadeProbe"), false, this);
const bool bHit2 = W->LineTraceSingleByChannel(Hit2,
FVector(X0, 0.0f, ProbeZ2),
FVector(X0, Dir * 4000.f, ProbeZ2),
ECC_Visibility, Q2);
++FacadeSamples;
const bool bBlocking = bHit2 && Hit2.GetActor() != nullptr;
if (!bBlocking)
{
++Facades;
UE_LOG(LogTemp, Log,
TEXT("GigLine QA: facade probe miss at X %.0f dir %.0f (no blocking hit in 40 m)"),
X0, Dir);
}
}
}
UE_LOG(LogTemp, Log,
TEXT("GigLine QA: facade probe %d/%d directions without a blocking wall -> %s"),
Facades, FacadeSamples,
Facades == 0 ? TEXT("PASS") : TEXT("FAIL"));
} }
// 7.0: verdict, then stop ticking. // 7.0: verdict, then stop ticking.
-85
View File
@@ -440,88 +440,3 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO.
`review-hero-facade-front`. `review-hero-facade-front`.
- Reminder: `.blend` is LFS read-only on checkout — `chmod u+w` or Blender fails - Reminder: `.blend` is LFS read-only on checkout — `chmod u+w` or Blender fails
with "Cannot save blend file … not writable" (this bit again here). with "Cannot save blend file … not writable" (this bit again here).
## 2026-11-09 — UE export hygiene: smoothing groups + facade collision
- **All our FBX exporters used `mesh_smooth_type="OFF"`**, which is why every Unreal
import logged "No smoothing group information was found for this mesh". The hero
now exports `"FACE"` and its round parts are shaded with
`shade_smooth_by_angle(35°)` before export: cylinders read as round instead of
faceted, and the import warning is gone (6 → 0).
- **The hero façade had no simple collision** (complex-only), so line traces and
capsule sweeps passed straight through it — the player could walk through the
wall of the demo. Caught by a new `-qaselftest` **facade occlusion probe** (40 m
sideways traces from the road centre at walking height).
- **Auto-generated convex collision on a 95-part 26 m façade is unusable**: it
produced coarse hulls that bulged into the street and the wall probe went
0/35 → 2/35. Rejected in favour of `UseComplexAsSimple` via
`UGigLineAssetTools.set_complex_collision_as_simple` (same reason the terrain
uses it), saved on the asset.
- Probe expectations must mirror the staged buildings: the facade probe now carries
explicit X spans per frontage (and the TCL/Tussaud's spans on the north side) so
a gap between buildings is not reported as a defect.
## 2026-11-09 — one FBX export preset instead of six copies
- The `bpy.ops.export_scene.fbx(...)` argument block was duplicated in six scripts
(hero, block frontage, north buildings, site, scale test, MPFB character), and
all six carried `mesh_smooth_type="OFF"` — hence the "No smoothing group
information was found" warning on every import.
- Now `Tools/fbx_export_preset.py` holds `ue_metric_v1` and `ue_character_v1`:
`preset_kwargs()` (bpy-free, unit-tested) plus `export_static()` /
`export_character()` wrappers that import `bpy` lazily. Every caller imports it
and passes `selection=True/False`; the seven duplicated blocks are gone.
- Callers in `Tools/` resolve the module with
`sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))`; the Blender-side
`Art/blender/site/export_site_fbx.py` computes the repo root
(`..`, `..`, `..`) and joins `Tools`. Getting that wrong silently inserts
`Tools/Tools` — check the import path when adding a caller.
- Verified end to end: scale test, hero, frontage (6/6), site (7/7) and north
buildings all re-exported through the preset, re-imported with **0 smoothing
warnings**, DemoMap rebuilt and `-qaselftest` all PASS (walk, transect 0 holes,
wall probe 0/35, facade probe 0/9).
- `mpfb_character_export.py` was not re-run: it needs the MPFB add-on on
`BLENDER_USER_SCRIPTS` ("Add-on not loaded: bl_ext.blender_org.mpfb"), which is
pre-existing and unrelated; the character is not in `0.1.x` gameplay.
## 2026-11-09 — v0.1.2 released
- Cornice finished: projecting corona over a **52-dentil course** (0.18 × 0.30 ×
0.22 m at 0.45 m centres, z 12.12–12.34, 0.25 m clear of the corona). Hero is now
3 044 tris across 148 parts, still 14.50 m. Verified: packaged `-qaselftest` all
PASS (transect 0 holes, wall probe 0/35, facade probe 0/9).
- `CHANGELOG.md` gets the `0.1.2` entry (added/changed/fixed/verified/limitations),
the `0.1.2` section is removed from `TODO.md`, `ROADMAP.md` status updated, tag
`v0.1.2` cut after the merge.
- Assumptions recorded rather than hidden: dentil spacing, base and abacus
dimensions are not resolvable in our reference photography.
## 2026-11-09 — elevation cameras levelled (straight verticals)
- archviz MUST: an elevation must not lean. `hero-facade-front` looked up ~4° from
4.2 m at a 7.6 m target; `demo-spawn-view` and `demo-street-level` were pitched
too. All three now put the camera at the target's height, so
`FindLookAtRotation` yields pitch 0 and roll 0 and verticals are parallel by
construction.
- Do not try to *measure* verticality from the rendered PNG with a naive edge
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.
+1 -1
View File
@@ -59,5 +59,5 @@ Rebuilds `Game/GigLine/Content/Test/ReviewMap.umap` from current content, runs t
- Floor speckle from the `Constant3Vector` wiring in `Tools/ue_build_review_map.py`. - Floor speckle from the `Constant3Vector` wiring in `Tools/ue_build_review_map.py`.
- Hero façade renders pale — Blender flat colours with no normal/roughness, so it reads as untextured stone (the `0.1.4` façade patch, not a lighting bug). - Hero façade renders pale — Blender flat colours with no normal/roughness, so it reads as untextured stone (the `0.1.4` façade patch, not a lighting bug).
- Banners sit on the column line rather than the wall (acceptable: they hang on the columns in the photographs). - Banners sit on the column line rather than the wall (acceptable: they hang on the columns in the photographs).
- ~~Banners and window panels float detached from the colonnade wall~~ — **window panes fixed 2026-11-09**: they sat at Y −1.98, 1.44 m in _front_ of the wall and ahead of the column line; they now sit on the wall plane at Y −0.56, behind the columns, and read as dark openings (see `review-hero-facade-front`). - ~~Banners and window panels float detached from the colonnade wall~~ — **window panes fixed 2026-11-09**: they sat at Y −1.98, 1.44 m in *front* of the wall and ahead of the column line; they now sit on the wall plane at Y −0.56, behind the columns, and read as dark openings (see `review-hero-facade-front`).
- Between kerb and building line, the walkable surface is the terrain mesh, not a modelled pavement. - Between kerb and building line, the walkable surface is the terrain mesh, not a modelled pavement.
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.
+22
View File
@@ -9,6 +9,28 @@ 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. - 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. - 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.2 — Scale and façade corrections (patch)
Suggested blender skills: **references** (reference-image-match and visual-match-checklist templates — measure the guessed heights and colonnade proportions against the photographs), **archviz** (real-world scale and façade composition discipline), **camera-cinematography** (reproduce the reference camera angles when comparing renders to photos).
### Patch tasks - active - 01a1b26b-3c94-707f-9665-2f6b4a79b564 (branch `scale-facade-0.1.2`)
- [x] Correct building scale and street proportions flagged against references. **Mapbox adopted 2026-11-09.** `Geo/tools/mapbox_buildings.py` merges our OSM heights with Mapbox's building data for the same footprints; after adopting its two findings the cross-check reports **66 match, 6 non-building, 0 disagreements**. Adopted: **`LEVEL_HEIGHT` 3.2 → 3.1 m** (`block_height_report.py`, and the Blosm `--level-height` default), plus explicit `HEIGHT_OVERRIDES` (3.0 m) for the three rear `building=yes`/`apartments` structures Mapbox gives as single-storey. Blockout regenerated, frontage re-exported/re-imported, DemoMap rebuilt; packaged `-qaselftest` all PASS. Measured effect: 6922 42.1 → 39.1 m, El Capitan Theatre 20.0 → 20.5 m (both now ~1.9 m above their table height = roof mass).
- [ ] Refine hero façade details where reference photography shows discrepancies. **Started 2026-11-09:** built `Tools/facade_detail_concepts.py`, which generates architectural detail plates from the local reference photographs with an image model (`--list` free, `--all` generates, sidecars + `-vN` no-overwrite, output gitignored). Eight studies generated locally under `Art/reference/facade-details/`: door recess, entrance, column order, entablature/cornice, parapet/motto, window rhythm, street wall, banner. **First fix from the measurement pass:** window panes were 1.44 m in front of the wall (ahead of the columns) and now sit on the wall plane behind the colonnade. Remaining: use the plates to correct the capital/base profiles and the medallion/cornice detail.
- [x] **Replace the placeholder heights.** **Resolved 2026-11-08 — they were never unknown buildings.** The three "guessed" ways are not building volumes at all: `1360755125` and `1360755126` are `building=roof` at `layer=1` (elevated canopies) and `1417995292` is `amenity=shelter` / `shelter_type=public_transport` (a transit shelter). The report defaulted them to "2 levels × 3.2 m" and produced the bogus 6.4 m. `Geo/tools/block_height_report.py` now classifies roof/shelter explicitly and reports **0 guessed heights**: all 15 block volumes have a real `height` tag, a `building:levels` derivation, or are correctly excluded as non-building volumes. No photograph measurement was needed, and no frontage building was affected.
- [x] Resolve the remaining unknown on the hero: the colonnade currently occupies about 45 % of the height against roughly 50–60 % in the photographs. Fix the vertical proportions. **Fixed 2026-11-09.** Measured baseline: colonnade (columns + capitals) 6.90 m of the 14.50 m envelope = **47.6 %**, because the entablature (3.40 m) and parapet (2.20 m) left only 7.90 m for the order. Thinned both to **3.00 m** and **1.80 m** (`Tools/hero_facade_build.py`), which lifts the colonnade to **7.70 m = 53.1 %** — inside the photographed 50–60 % band, with the 14.50 m envelope, the motto band and the medallions unchanged in position relative to the parapet. Hero rebuilt, re-imported (bounds 2604 × 1257 × 1450 cm) and re-rendered.
- [ ] Tag `v0.1.2` and write `CHANGELOG.md` entry; then remove this section.
## 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) ## 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). 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).
+2 -4
View File
@@ -16,8 +16,6 @@ Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side b
| Tool | Purpose | | Tool | Purpose |
| --- | --- | | --- | --- |
| `check-lfs.sh` | Fails if the `lfs` filter is missing or an LFS-tracked file was committed as a plain blob | | `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 | | `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 | | `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 |
@@ -59,14 +57,14 @@ Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side b
| Command | Purpose | | Command | Purpose |
| --- | --- | | --- | --- |
| `UnrealEditor <uproject> /Game/Hollywood/DemoMap -game -qaselftest` | Headless behaviour check: drives forward input for 2 s, then resets and pauses. Logs `GigLine QA: ... PASS/FAIL` lines and a final `RESULT`. Runs only with `-qaselftest`, so normal play is unaffected. | | `UnrealEditor <uproject> /Game/Hollywood/DemoMap -game -qaselftest` | Headless behaviour check: drives forward input for 2 s, then resets and pauses. Logs `GigLine QA: ... PASS/FAIL` lines and a final `RESULT`. Runs only with `-qaselftest`, so normal play is unaffected. |
| `-qaselftest` phases | Walk, reset, pause/unpause, a 16-row seam transect (0 holes), a ground-referenced wall probe (0/35) and a **facade occlusion probe** that fires sideways from the road centre and requires a blocking wall wherever a building is staged (0/9), plus a frame-time capture writing `Saved/Profiling/perf-walk-frametimes.csv` and logging `GigLine PERF:`. | | `-qaselftest` phases | Walk, reset, pause/unpause, a 16-row seam transect (0 holes) and a ground-referenced wall probe (0/35), plus a frame-time capture writing `Saved/Profiling/perf-walk-frametimes.csv` and logging `GigLine PERF:`. |
| `gigline.SetupLogs` cvar | Spawn/input setup logging, **off by default** (`DefaultEngine.ini [ConsoleVariables]`); enable at runtime with `gigline.SetupLogs 1`. | | `gigline.SetupLogs` cvar | Spawn/input setup logging, **off by default** (`DefaultEngine.ini [ConsoleVariables]`); enable at runtime with `gigline.SetupLogs 1`. |
## Reference and concept generation ## Reference and concept generation
| Tool | Purpose | | Tool | Purpose |
| --- | --- | | --- | --- |
| `facade_detail_concepts.py` | Generates **frontal building plates** (whole elevation, entrance in its facade, or building with neighbours) from a local reference photograph with an image model (OpenRouter); close-up detail crops are deliberately not requested because they cannot be measured for proportion. `--list` is free and shows which studies have their reference present; `--all`/`--only <name>` generates. Never overwrites (writes `-v2`, `-v3`), writes a `.refgen.json` sidecar per image (model, prompt, source photo, SHA-256), and puts everything under `Art/reference/facade-details/` — gitignored, local-only, never shipped. Costs money per image, so it is never automatic. | | `facade_detail_concepts.py` | Generates architectural detail plates from a local reference photograph with an image model (OpenRouter). `--list` is free and shows which studies have their reference present; `--all`/`--only <name>` generates. Never overwrites (writes `-v2`, `-v3`), writes a `.refgen.json` sidecar per image (model, prompt, source photo, SHA-256), and puts everything under `Art/reference/facade-details/` — gitignored, local-only, never shipped. Costs money per image, so it is never automatic. |
| `judge_screenshot.py` | (see Visual review) | | `judge_screenshot.py` | (see Visual review) |
## Input injection ## Input injection
+14 -4
View File
@@ -19,9 +19,6 @@ import os
import sys import sys
import bpy import bpy
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import fbx_export_preset as fbx_preset # noqa: E402
# blockout object name -> (asset name, note). Addresses from the verification doc. # blockout object name -> (asset name, note). Addresses from the verification doc.
BUILDINGS = { BUILDINGS = {
@@ -72,7 +69,20 @@ def main():
bpy.context.view_layer.objects.active = new_obj bpy.context.view_layer.objects.active = new_obj
path = os.path.join(args.out, asset_name + ".fbx") path = os.path.join(args.out, asset_name + ".fbx")
fbx_preset.export_static(path, selection=True) bpy.ops.export_scene.fbx(
filepath=path,
use_selection=True,
object_types={"MESH"},
apply_unit_scale=True,
apply_scale_options="FBX_SCALE_NONE",
axis_forward="-Y",
axis_up="Z",
use_space_transform=True,
use_mesh_modifiers=True,
mesh_smooth_type="OFF",
path_mode="AUTO",
bake_space_transform=False,
)
world = new_obj.matrix_world world = new_obj.matrix_world
zs = [(world @ v.co).z for v in new_mesh.vertices] zs = [(world @ v.co).z for v in new_mesh.vertices]
ys = [(world @ v.co).y for v in new_mesh.vertices] ys = [(world @ v.co).y for v in new_mesh.vertices]
+14 -4
View File
@@ -13,9 +13,6 @@ import os
import sys import sys
import bpy import bpy
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import fbx_export_preset as fbx_preset # noqa: E402
BUILDINGS = ("TCL Chinese Theatre", "Madame Tussaud's") BUILDINGS = ("TCL Chinese Theatre", "Madame Tussaud's")
@@ -53,7 +50,20 @@ def main():
bpy.context.view_layer.objects.active = new_obj bpy.context.view_layer.objects.active = new_obj
path = os.path.join(args.out, name.replace(" ", "_").replace("'", "") + ".fbx") path = os.path.join(args.out, name.replace(" ", "_").replace("'", "") + ".fbx")
fbx_preset.export_static(path, selection=True) bpy.ops.export_scene.fbx(
filepath=path,
use_selection=True,
object_types={"MESH"},
apply_unit_scale=True,
apply_scale_options="FBX_SCALE_NONE",
axis_forward="-Y",
axis_up="Z",
use_space_transform=True,
use_mesh_modifiers=True,
mesh_smooth_type="OFF",
path_mode="AUTO",
bake_space_transform=False,
)
world = new_obj.matrix_world world = new_obj.matrix_world
zs = [(world @ v.co).z for v in new_mesh.vertices] zs = [(world @ v.co).z for v in new_mesh.vertices]
print(f"[north] {name:22} verts {len(new_mesh.vertices):5} z {min(zs):+.2f}..{max(zs):+.2f} m " print(f"[north] {name:22} verts {len(new_mesh.vertices):5} z {min(zs):+.2f}..{max(zs):+.2f} m "
+60 -69
View File
@@ -1,14 +1,10 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Generate frontal building plates from the local reference photography. """Generate architectural detail studies from the local reference photography.
Each study takes one photograph we already hold (`Art/reference/`, gitignored) as Each study takes one photograph we already hold (`Art/reference/`, gitignored) as
the geometric anchor and asks an image model to re-stage it as a clean **frontal the geometric anchor and asks an image model to re-stage it as a clean
view of the building** — the whole elevation, the entrance in its facade, or the architectural detail plate — no crowd, no staging, straight-on, evenly lit — so
building with its neighbours — evenly lit, no crowd, no staging, so the modelling the modelling pass has something to measure against instead of a tourist photo.
pass has something measurable instead of a tourist photo.
Deliberately **not** close-up detail crops: a doorway or cornice crop cannot be
measured for proportion, which is what the modelling pass actually needs.
python3 Tools/facade_detail_concepts.py --list python3 Tools/facade_detail_concepts.py --list
python3 Tools/facade_detail_concepts.py --only door-recess,parapet-motto python3 Tools/facade_detail_concepts.py --only door-recess,parapet-motto
@@ -55,75 +51,70 @@ DEFAULT_MODEL = "google/gemini-2.5-flash-image"
# project's convention so the two sets stay comparable: the photograph fixes the # project's convention so the two sets stay comparable: the photograph fixes the
# geometry, the prompt only restages light, weather, people and surface. # geometry, the prompt only restages light, weather, people and surface.
ANCHOR_SUFFIX = ( ANCHOR_SUFFIX = (
"Use the attached reference photograph as the source of the architecture: keep " "Use the attached reference photograph as the fixed source of geometry and "
"the building's proportions, storey count, column rhythm, band heights and " "composition: keep the building's proportions, column rhythm, band heights, "
"signage placement exactly as photographed. You may REFRAME — step back across " "signage placement and camera angle exactly as photographed. Only restage "
"the street, zoom out or widen the field of view — so that the whole building " "lighting, weather, people and surface condition where the request asks for "
"fits in the frame from the pavement to the parapet; the reference is often " "it. Do not invent extra storeys, wings or ornaments that the photo does not "
"cropped and the plate must not be. Keep the view exterior and frontal (or " "show."
"three-quarter where asked). Restage only lighting, weather, people and surface "
"condition. Do not invent extra storeys, wings or ornaments, and do not change "
"the architecture to fit the frame."
) )
# name -> (reference photo relative to Art/reference/, what the plate must show) # name -> (reference photo relative to Art/reference/, what the study must show)
#
# Verified framing (vision pre-check, 2026-11-09): the model follows the reference
# crop closely, so a plate is only as wide as its source photo. `building-entrance`
# and `building-street-front` come back as full-elevation frontal views; the studies
# anchored on the tightly cropped 2009 facade photo stay cropped, because the model
# will not invent the parapet and sky that the photograph does not contain. A full
# building plate for 6840 needs a full-elevation photograph, which the local set
# does not have (see Docs/reference-notes-hollywood.md).
#
# Every study asks for a *frontal view of a building*, not a crop of one detail:
# the whole elevation, the entrance in its facade, or the building together with
# its neighbours. Close-up framing is deliberately absent — a detail crop cannot
# be measured for proportion, which is what the modelling pass needs.
STUDIES: dict[str, tuple[str, str]] = { STUDIES: dict[str, tuple[str, str]] = {
"building-front": ( "door-recess": (
"hollywood/el-capitan-facade-day-wikimedia-2009-10-27.jpg", "el-capitan-entrance/01-hollywood-el-capitan-entrance-detail.jpg",
"A frontal view of the complete building shown in the reference " "A clean architectural detail plate of exactly the subject in the "
"photograph: the whole elevation from the pavement to the parapet fits in " "reference photograph. Render it as an even-daylight stone study: crisp "
"the frame with clear sky above the roofline and pavement below the ground " "surface carving, readable depth of every recess and moulding, no people, "
"floor, shot straight on and centred. Nothing may be cut off at the top or " "no signage or banners in front of it, no shop clutter, straight-on to the "
"bottom. Even daylight, no crowd, no vehicles.", "subject.",
), ),
"building-entrance": ( "entrance-wide": (
"el-capitan-entrance/01-hollywood-el-capitan-entrance.jpg", "el-capitan-entrance/01-hollywood-el-capitan-entrance.jpg",
"A frontal view of the building in the reference photograph, framed so its " "A straight-on architectural study of the street entrance in the "
"street entrance is clearly visible as part of the facade — the doors and " "reference photograph, the portal group centred and filling the frame, "
"the wall around them in proportion, not a close-up of the doorway. " "threshold and pavement readable, no pedestrians or vehicles, even "
"Include the pavement in front. Even daylight, no crowd.", "overcast daylight, stone surface legible.",
), ),
"building-with-neighbours": ( "column-order": (
"hollywood/el-capitan-theatre-blade-sign-day-wikimedia-2008-10.jpg",
"A straight-on view of the street wall in the reference photograph: the "
"building and its neighbouring buildings either side, each at full height "
"from pavement to parapet with sky above, the joins between them visible, "
"nothing cut off. Shot from across the road, parallel to the frontage. "
"Even daylight, no crowd.",
),
"building-street-front": (
"hollywood/walk-of-fame-terrazzo-ovation-day-cc0-2019-06-20.jpg",
"A frontal view along the pavement of the building in the reference "
"photograph: the building's frontage seen from street level with its "
"neighbours continuing either side and the pavement in the foreground. "
"Straight on, even daylight, no crowd.",
),
"building-front-oblique": (
"hollywood/el-capitan-facade-day-wikimedia-2009-10-27.jpg", "hollywood/el-capitan-facade-day-wikimedia-2009-10-27.jpg",
"A three-quarter view of the complete building in the reference " "A close architectural study of the column order visible in the "
"photograph: the frontal elevation and one side visible together so the " "reference photograph: the base, shaft and capital read clearly, "
"building's depth and mass read, with its neighbouring building at one " "neighbouring columns framing it. Straight-on, evenly lit, stone surface "
"edge. The whole building stays in frame. Even daylight, no crowd.", "and carving legible, no people or banners.",
), ),
"theatre-front-with-neighbours": ( "entablature-cornice": (
"el-capitan-theater-detail/01-el-capitan-marquee.jpg", "hollywood/el-capitan-facade-day-wikimedia-2009-10-27.jpg",
"A frontal view of the theatre in the reference photograph together with " "An architectural detail study of the horizontal stone bands above the "
"the buildings either side of it, the whole frontage and its full height " "columns in the reference photograph: every band edge and the projecting "
"in frame, shot from across the road. Even daylight, no crowd, marquee " "cornice with its shadow line. The layered depth of the stone must be "
"lettering blocked out as unreadable texture.", "readable, flat even light, no people.",
),
"parapet-motto": (
"hollywood/el-capitan-facade-day-wikimedia-2009-10-27.jpg",
"An architectural study of the parapet above the colonnade in the "
"reference photograph: the inset panel, any round medallions beside it "
"and the coping at the top. Lettering on the panel is blocked out as "
"unreadable texture. Flat even light, no people.",
),
"window-rhythm": (
"hollywood/el-capitan-facade-day-wikimedia-2009-10-27.jpg",
"An architectural study of the wall surface behind and between the "
"columns in the reference photograph: the opening or window rhythm and "
"the reveal depth. Straight-on, no people, no banners covering the wall.",
),
"street-wall": (
"hollywood/el-capitan-theatre-blade-sign-day-wikimedia-2008-10.jpg",
"A clean street-wall study from the opposite pavement, as framed in the "
"reference photograph: the building line, the parapet heights of the "
"neighbouring buildings, the shopfront line and the pavement depth. No "
"crowd, even daylight.",
),
"banner-detail": (
"el-capitan-premiere/02-premiere-of-spielberg-s-west-side-story---el-capitan---holly.jpg",
"An architectural study of the vertical banner in the reference "
"photograph: how it is fixed, its proportions against the column behind "
"it, and how it sits against the stone. Daylight, no crowd.",
), ),
} }
-94
View File
@@ -1,94 +0,0 @@
#!/usr/bin/env python3
"""The one FBX export preset for this project ("ue_metric_v1"/"ue_character_v1").
Before this module the same `bpy.ops.export_scene.fbx(...)` argument block was
copied into six scripts (hero, frontage, north buildings, site, scale test, MPFB
character). They had already drifted only by accident, and every one of them
carried `mesh_smooth_type="OFF"` — which is why Unreal logged "No smoothing group
information was found for this mesh" on every import and round parts rendered
faceted. One preset, one place to fix that.
Use from inside Blender:
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "Tools"))
import fbx_export_preset as preset
preset.export_static(path) # whole scene, MESH only
preset.export_static(path, selection=True) # selected objects only
preset.export_character(path) # armature + mesh, character flags
`preset_kwargs()` is pure (no `bpy`), so the argument set can be unit-tested
without Blender — see Tools/tests/test_fbx_export_preset.py.
Why these values:
* `FBX_SCALE_NONE` + `apply_unit_scale` keeps Blender metres → Unreal centimetres
at exactly ×100, verified by the three-cube scale test (1 m → 100.00 uu).
* `axis_forward="-Y"`, `axis_up="Z"` is the project's recorded mapping: east → +X,
north → −Y, up → +Z.
* `mesh_smooth_type="FACE"` exports smoothing groups. `"OFF"` (the old value)
drops them, so Unreal rebuilds flat normals and every cylinder is faceted.
* `use_mesh_modifiers=True` bakes the bevel/modifier stack at export; the source
`.blend` stays non-destructive.
"""
from __future__ import annotations
# Shared by every mesh export. Values are the recorded ue_metric_v1 preset.
_METRIC = {
"object_types": {"MESH"},
"apply_unit_scale": True,
"apply_scale_options": "FBX_SCALE_NONE",
"axis_forward": "-Y",
"axis_up": "Z",
"use_space_transform": True,
"use_mesh_modifiers": True,
"mesh_smooth_type": "FACE",
"path_mode": "AUTO",
"bake_space_transform": False,
}
# Character exports differ only in the object set and the armature/path flags.
_CHARACTER = dict(
_METRIC,
object_types={"ARMATURE", "MESH"},
add_leaf_bones=False,
path_mode="COPY",
embed_textures=False,
)
PRESETS = {"ue_metric_v1": _METRIC, "ue_character_v1": _CHARACTER}
def preset_kwargs(preset: str = "ue_metric_v1", selection: bool = False, **overrides) -> dict:
"""Return the `bpy.ops.export_scene.fbx` argument dict (no `bpy` needed).
`preset` selects the base set, `selection` decides whether only selected
objects are exported, and `overrides` wins last for the rare special case.
"""
if preset not in PRESETS:
raise KeyError(f"unknown preset {preset!r}; known: {sorted(PRESETS)}")
kwargs = dict(PRESETS[preset])
kwargs["use_selection"] = bool(selection)
kwargs.update(overrides)
if "filepath" in kwargs:
raise ValueError("pass the path to export_static/export_character, not as an override")
return kwargs
def _export(path: str, preset: str, selection: bool, overrides: dict) -> str:
import bpy # imported here so `preset_kwargs` stays usable without Blender
kwargs = preset_kwargs(preset, selection=selection, **overrides)
bpy.ops.export_scene.fbx(filepath=path, **kwargs)
return path
def export_static(path: str, selection: bool = False, **overrides) -> str:
"""Export meshes with the ue_metric_v1 preset. Returns the path."""
return _export(path, "ue_metric_v1", selection, overrides)
def export_character(path: str, selection: bool = False, **overrides) -> str:
"""Export armature + mesh with the ue_character_v1 preset. Returns the path."""
return _export(path, "ue_character_v1", selection, overrides)
+26 -77
View File
@@ -33,9 +33,6 @@ import os
import sys import sys
import bpy import bpy
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import fbx_export_preset as fbx_preset # noqa: E402
# --- measured / derived ------------------------------------------------------ # --- measured / derived ------------------------------------------------------
FRONTAGE_W = 25.75 FRONTAGE_W = 25.75
@@ -77,7 +74,7 @@ def cube(name, x, y, z, dx, dy, dz, material=None):
return obj return obj
def cylinder(name, x, y, z0, z1, radius, material, vertices=24, smooth=True): def cylinder(name, x, y, z0, z1, radius, material, vertices=24):
bpy.ops.mesh.primitive_cylinder_add( bpy.ops.mesh.primitive_cylinder_add(
vertices=vertices, vertices=vertices,
radius=radius, radius=radius,
@@ -88,30 +85,6 @@ def cylinder(name, x, y, z0, z1, radius, material, vertices=24, smooth=True):
obj.name = name obj.name = name
if material: if material:
obj.data.materials.append(material) obj.data.materials.append(material)
if smooth:
# Round parts must shade smoothly *around* the axis while keeping the
# caps creased. Without this the columns read as faceted prisms, and the
# FBX carries no smoothing groups at all (Unreal warns about it).
bpy.ops.object.shade_smooth_by_angle(angle=math.radians(35.0))
return obj
def disc_on_wall(name, x, y, z, radius, thickness, material=None, vertices=24):
"""A round disc mounted vertically on a wall face.
`cylinder()` builds along Z, so a "medallion" made with it is a pancake lying
flat on top of the parapet rather than a disc on its face — which is what the
first build did (1.1 m across in Y, 0.02 m thick in Z, sitting at the coping).
This rotates the cylinder 90 degrees about X so the axis points along Y and
the disc faces the street.
"""
obj = cylinder(name, x, y, z - thickness / 2.0, z + thickness / 2.0,
radius, material, vertices=vertices)
obj.rotation_euler = (math.radians(90.0), 0.0, 0.0)
bpy.ops.object.select_all(action="DESELECT")
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
return obj return obj
@@ -163,32 +136,14 @@ def build():
col_xs.append(x) col_xs.append(x)
cube(f"Hero_Pedestal_{i + 1}", x, -0.62, PEDESTAL_H + 0.27, cube(f"Hero_Pedestal_{i + 1}", x, -0.62, PEDESTAL_H + 0.27,
1.24, 1.24, 0.54, stone) 1.24, 1.24, 0.54, stone)
# Base moulding: a taller, wider cylinder where the shaft meets the cylinder(f"Hero_Column_{i + 1}", x, -0.55, PEDESTAL_H + 0.55,
# pedestal. Without it the shaft starts straight off a square plinth, PEDESTAL_H + 0.55 + (HEIGHT - ENTABLATURE_H - PARAPET_H - PEDESTAL_H - 0.55 - CAPITAL_H),
# which is not how an Ionic base reads in silhouette.
base_z0 = PEDESTAL_H + 0.55
base_h = 0.22
cylinder(f"Hero_ColumnBase_{i + 1}", x, -0.55, base_z0, base_z0 + base_h,
0.68, stone, vertices=20)
shaft_z0 = base_z0 + base_h
cap_z0 = HEIGHT - ENTABLATURE_H - PARAPET_H - CAPITAL_H
cylinder(f"Hero_Column_{i + 1}", x, -0.55, shaft_z0, cap_z0,
COLUMN_D / 2.0, stone) COLUMN_D / 2.0, stone)
# Capital block, then the square abacus that carries the architrave. cylinder(f"Hero_Capital_{i + 1}", x, -0.55,
# The abacus takes the top 0.14 m of the capital zone so the colonnade HEIGHT - ENTABLATURE_H - PARAPET_H - CAPITAL_H,
# still ends at HEIGHT - ENTABLATURE_H - PARAPET_H (proportion unchanged). HEIGHT - ENTABLATURE_H - PARAPET_H, 0.62, stone, vertices=16)
abacus_h = 0.14
cylinder(f"Hero_Capital_{i + 1}", x, -0.55, cap_z0,
HEIGHT - ENTABLATURE_H - PARAPET_H - abacus_h, 0.62, stone, vertices=16)
cube(f"Hero_Abacus_{i + 1}", x, -0.55,
HEIGHT - ENTABLATURE_H - PARAPET_H - abacus_h / 2.0,
1.30, 1.30, abacus_h, stone)
note("Hero_Column_x6", (COLUMN_D, COLUMN_D, HEIGHT - ENTABLATURE_H - PARAPET_H - PEDESTAL_H), note("Hero_Column_x6", (COLUMN_D, COLUMN_D, HEIGHT - ENTABLATURE_H - PARAPET_H - PEDESTAL_H),
f"6 columns at {COLUMN_BAY} m centres (sibling notes x measured frontage)") f"6 columns at {COLUMN_BAY} m centres (sibling notes x measured frontage)")
note("Hero_ColumnBase_x6", (1.36, 1.36, 0.22),
"assumption: Ionic base moulding; photographs show a wider base at the shaft foot")
note("Hero_Abacus_x6", (1.30, 1.30, 0.14),
"assumption: square abacus under the architrave, as Ionic capitals carry")
# --- entablature and parapet ------------------------------------------ # --- entablature and parapet ------------------------------------------
ent_z0 = HEIGHT - ENTABLATURE_H - PARAPET_H ent_z0 = HEIGHT - ENTABLATURE_H - PARAPET_H
@@ -196,25 +151,9 @@ def build():
# elevation reads as layered stone rather than one flat plane. # elevation reads as layered stone rather than one flat plane.
cube("Hero_Architrave", 0.0, -1.55, ent_z0 + 0.42, FRONTAGE_W, 1.65, 0.84, stone) cube("Hero_Architrave", 0.0, -1.55, ent_z0 + 0.42, FRONTAGE_W, 1.65, 0.84, stone)
note("Hero_Architrave", (FRONTAGE_W, 1.65, 0.84), "assumption: stacked entablature bands") note("Hero_Architrave", (FRONTAGE_W, 1.65, 0.84), "assumption: stacked entablature bands")
# Cornice under the parapet: a projecting corona carried on a dentil course, # Cornice under the parapet.
# so the top of the order reads as layered stone with a shadow line rather cube("Hero_Cornice", 0.0, -1.6, HEIGHT - PARAPET_H - 0.18, FRONTAGE_W, 1.75, 0.36, stone)
# than one flat box. Dentils hang under the corona and stand 0.25 m clear of
# it, which is what gives the elevation its cornice shadow at street level.
cornice_z = HEIGHT - PARAPET_H - 0.18
cube("Hero_Cornice", 0.0, -1.6, cornice_z, FRONTAGE_W, 1.75, 0.36, stone)
note("Hero_Cornice", (FRONTAGE_W, 1.75, 0.36), "assumption: projecting cornice line") note("Hero_Cornice", (FRONTAGE_W, 1.75, 0.36), "assumption: projecting cornice line")
DENTIL_W, DENTIL_D, DENTIL_H, DENTIL_GAP = 0.18, 0.30, 0.22, 0.27
dentil_y = -2.30
dentil_z = cornice_z - 0.18 - DENTIL_H / 2.0
dentil_span = FRONTAGE_W - 2.0
dentil_count = int(dentil_span / (DENTIL_W + DENTIL_GAP))
for i in range(dentil_count):
dx = -dentil_span / 2.0 + (DENTIL_W + DENTIL_GAP) * i + DENTIL_W / 2.0
cube(f"Hero_Dentil_{i + 1}", dx, dentil_y, dentil_z,
DENTIL_W, DENTIL_D, DENTIL_H, stone)
note("Hero_Dentil_xN", (DENTIL_W, DENTIL_D, DENTIL_H),
f"{dentil_count} dentils at {DENTIL_W + DENTIL_GAP:.2f} m centres under the corona; "
"assumption: photographed cornice reads as a dentilled course")
cube("Hero_Entablature", 0.0, -0.75, ent_z0 + ENTABLATURE_H / 2.0, cube("Hero_Entablature", 0.0, -0.75, ent_z0 + ENTABLATURE_H / 2.0,
FRONTAGE_W, 1.5, ENTABLATURE_H, stone) FRONTAGE_W, 1.5, ENTABLATURE_H, stone)
note("Hero_Entablature", (FRONTAGE_W, 1.5, ENTABLATURE_H), "assumption: entablature depth") note("Hero_Entablature", (FRONTAGE_W, 1.5, ENTABLATURE_H), "assumption: entablature depth")
@@ -228,13 +167,11 @@ def build():
FRONTAGE_W - 3.0, 0.08, 0.85, stone_dark) FRONTAGE_W - 3.0, 0.08, 0.85, stone_dark)
note("Hero_MottoBand", (FRONTAGE_W - 3.0, 0.08, 0.85), "photographs: motto panel above the colonnade") note("Hero_MottoBand", (FRONTAGE_W - 3.0, 0.08, 0.85), "photographs: motto panel above the colonnade")
# Medallions either side of the inscription: discs on the parapet face, not # Medallions either side of the inscription.
# pancakes on the coping (see disc_on_wall).
for i, x in enumerate((-6.2, 6.2), start=1): for i, x in enumerate((-6.2, 6.2), start=1):
disc_on_wall(f"Hero_Medallion_{i}", x, -1.30, HEIGHT - PARAPET_H / 2.0, cylinder(f"Hero_Medallion_{i}", x, -1.36, HEIGHT - PARAPET_H / 2.0 + 0.42,
0.55, 0.04, stone_dark, vertices=20) HEIGHT - PARAPET_H / 2.0 + 0.44, 0.55, stone_dark, vertices=20)
note("Hero_Medallion_x2", (1.1, 0.04, 1.1), note("Hero_Medallion_x2", (1.1, 1.1, 0.02), "photographs: two stone discs beside the motto")
"photographs: two stone discs beside the motto, mounted on the parapet face")
# --- entrances --------------------------------------------------------- # --- entrances ---------------------------------------------------------
# Three portals between column pairs, each with its own short flight. The # Three portals between column pairs, each with its own short flight. The
@@ -325,10 +262,22 @@ def build():
def export_fbx(path): def export_fbx(path):
# Preset lives in Tools/fbx_export_preset.py (ue_metric_v1).
os.makedirs(os.path.dirname(path), exist_ok=True) os.makedirs(os.path.dirname(path), exist_ok=True)
# Named export preset "ue_metric_v1" - same settings as Tools/scale_test_export.py. # Named export preset "ue_metric_v1" - same settings as Tools/scale_test_export.py.
fbx_preset.export_static(path) bpy.ops.export_scene.fbx(
filepath=path,
use_selection=False,
object_types={"MESH"},
apply_unit_scale=True,
apply_scale_options="FBX_SCALE_NONE",
axis_forward="-Y",
axis_up="Z",
use_space_transform=True,
use_mesh_modifiers=True,
mesh_smooth_type="OFF",
path_mode="AUTO",
bake_space_transform=False,
)
print(f"[hero] exported FBX preset ue_metric_v1 -> {path}") print(f"[hero] exported FBX preset ue_metric_v1 -> {path}")
+16 -4
View File
@@ -19,9 +19,6 @@ import sys
import zlib import zlib
import bpy import bpy
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import fbx_export_preset as fbx_preset # noqa: E402
import numpy import numpy
from mathutils import Euler from mathutils import Euler
@@ -152,7 +149,22 @@ def make_diffuse(human, size=1024):
def export_fbx(human, path): def export_fbx(human, path):
os.makedirs(os.path.dirname(path), exist_ok=True) os.makedirs(os.path.dirname(path), exist_ok=True)
# Named export preset "ue_character_v1" — change only via a new preset name. # Named export preset "ue_character_v1" — change only via a new preset name.
fbx_preset.export_character(path) bpy.ops.export_scene.fbx(
filepath=path,
use_selection=False,
object_types={"ARMATURE", "MESH"},
apply_unit_scale=True,
apply_scale_options="FBX_SCALE_NONE",
axis_forward="-Y",
axis_up="Z",
use_space_transform=True,
use_mesh_modifiers=True,
add_leaf_bones=False,
path_mode="COPY",
embed_textures=False,
mesh_smooth_type="OFF",
bake_space_transform=False,
)
log(f"export: FBX preset ue_character_v1 -> {path}") log(f"export: FBX preset ue_character_v1 -> {path}")
+14 -4
View File
@@ -16,9 +16,6 @@ import os
import sys import sys
import bpy import bpy
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import fbx_export_preset as fbx_preset # noqa: E402
def build_scene(): def build_scene():
@@ -47,7 +44,20 @@ def build_scene():
def export_fbx(path): def export_fbx(path):
os.makedirs(os.path.dirname(path), exist_ok=True) os.makedirs(os.path.dirname(path), exist_ok=True)
# Named export preset "ue_metric_v1" - change only via a new preset name. # Named export preset "ue_metric_v1" - change only via a new preset name.
fbx_preset.export_static(path) bpy.ops.export_scene.fbx(
filepath=path,
use_selection=False,
object_types={"MESH"},
apply_unit_scale=True,
apply_scale_options="FBX_SCALE_NONE",
axis_forward="-Y",
axis_up="Z",
use_space_transform=True,
use_mesh_modifiers=True,
mesh_smooth_type="OFF",
path_mode="AUTO",
bake_space_transform=False,
)
print(f"[scale_test] exported FBX preset ue_metric_v1 -> {path}") print(f"[scale_test] exported FBX preset ue_metric_v1 -> {path}")
-70
View File
@@ -1,70 +0,0 @@
"""Tests for Tools/fbx_export_preset.py.
The preset module keeps its argument set in plain dicts and builds it in
`preset_kwargs()`, so the export contract can be checked without starting
Blender. Only `export_static`/`export_character` import `bpy`.
"""
import os
import sys
import unittest
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
sys.path.insert(0, os.path.join(ROOT, "Tools"))
import fbx_export_preset as preset # noqa: E402
class MetricPresetTest(unittest.TestCase):
def test_scale_and_axis_contract(self):
# These three are the project's recorded conversion contract: metres ->
# centimetres by a factor of exactly 100, east +X, north -Y, up +Z.
kwargs = preset.preset_kwargs()
self.assertTrue(kwargs["apply_unit_scale"])
self.assertEqual(kwargs["apply_scale_options"], "FBX_SCALE_NONE")
self.assertEqual(kwargs["axis_forward"], "-Y")
self.assertEqual(kwargs["axis_up"], "Z")
def test_smoothing_groups_are_exported(self):
# "OFF" (the pre-refactor value) drops smoothing groups and every Unreal
# import warns about it, with round parts faceted.
self.assertEqual(preset.preset_kwargs()["mesh_smooth_type"], "FACE")
def test_selection_flag(self):
self.assertFalse(preset.preset_kwargs()["use_selection"])
self.assertTrue(preset.preset_kwargs(selection=True)["use_selection"])
def test_object_types_are_meshes_only(self):
self.assertEqual(preset.preset_kwargs()["object_types"], {"MESH"})
class CharacterPresetTest(unittest.TestCase):
def test_character_exports_armature_and_mesh(self):
kwargs = preset.preset_kwargs("ue_character_v1")
self.assertEqual(kwargs["object_types"], {"ARMATURE", "MESH"})
self.assertFalse(kwargs["add_leaf_bones"])
self.assertEqual(kwargs["path_mode"], "COPY")
self.assertFalse(kwargs["embed_textures"])
def test_character_keeps_the_scale_contract(self):
kwargs = preset.preset_kwargs("ue_character_v1")
self.assertEqual(kwargs["apply_scale_options"], "FBX_SCALE_NONE")
self.assertEqual(kwargs["mesh_smooth_type"], "FACE")
class OverrideTest(unittest.TestCase):
def test_overrides_win(self):
kwargs = preset.preset_kwargs(mesh_smooth_type="OFF")
self.assertEqual(kwargs["mesh_smooth_type"], "OFF")
def test_unknown_preset_raises(self):
with self.assertRaises(KeyError):
preset.preset_kwargs("ue_imperial_v9")
def test_filepath_must_not_be_an_override(self):
with self.assertRaises(ValueError):
preset.preset_kwargs(filepath="/tmp/x.fbx")
if __name__ == "__main__":
unittest.main()
-70
View File
@@ -1,70 +0,0 @@
"""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()
+3 -9
View File
@@ -236,11 +236,7 @@ try:
# the plinth, which is what "portal detail" means here. # the plinth, which is what "portal detail" means here.
("hero-portals", unreal.Vector(hero_x + 100, -2250, 170), unreal.Vector(hero_x + 100, hero_y, 230)), ("hero-portals", unreal.Vector(hero_x + 100, -2250, 170), unreal.Vector(hero_x + 100, hero_y, 230)),
# straight on from the north sidewalk; the building body spans Y -1443..+250 # straight on from the north sidewalk; the building body spans Y -1443..+250
# Level horizon: camera height == target height, so the elevation's ("hero-facade-front", unreal.Vector(hero_x + 200, -3200, 420), unreal.Vector(hero_x, hero_y + 400, 760)),
# 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)),
# oblique from the north-west, reading the colonnade in depth # oblique from the north-west, reading the colonnade in depth
("hero-facade-oblique", unreal.Vector(hero_x - 2200, -3400, 620), unreal.Vector(hero_x + 300, hero_y, 700)), ("hero-facade-oblique", unreal.Vector(hero_x - 2200, -3400, 620), unreal.Vector(hero_x + 300, hero_y, 700)),
] ]
@@ -280,14 +276,12 @@ try:
demo_shots = [ demo_shots = [
# 14 m back so the 14.5 m elevation fits: at 2 m the camera sees only # 14 m back so the 14.5 m elevation fits: at 2 m the camera sees only
# the colonnade's underside. # the colonnade's underside.
# Level horizon for the same reason: a block elevation is read by its ("demo-spawn-view", unreal.Vector(-13336, -3100, 420), unreal.Vector(-13336, 1300, 620)),
# verticals, and a pitched camera makes them lean.
("demo-spawn-view", unreal.Vector(-13336, -3100, 620), unreal.Vector(-13336, 1300, 620)),
# Street level, from the north sidewalk: the old camera sat at Y -400, # 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 # 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 # the unlit back face. Y -2600 is on the sidewalk in front of the lit
# elevation. # elevation.
("demo-street-level", unreal.Vector(-13420, -1650, 170), unreal.Vector(-13336, 1300, 170)), ("demo-street-level", unreal.Vector(-13420, -1650, 170), unreal.Vector(-13336, 1300, 400)),
# The designated mid-block crossing. Street arc length runs from the # The designated mid-block crossing. Street arc length runs from the
# west end (s=0 at X -27966 cm), so s=130.1 m is UE X -14956, not the # west end (s=0 at X -27966 cm), so s=130.1 m is UE X -14956, not the
# -24291 the old camera used (that is the s=36.6 crossing). Camera sits # -24291 the old camera used (that is the s=36.6 crossing). Camera sits
-52
View File
@@ -1,52 +0,0 @@
#!/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)
-32
View File
@@ -12,14 +12,8 @@
# centreline, from the street profile), and the hero's east edge at # centreline, from the street profile), and the hero's east edge at
# X = -12024.5 cm (projected control point in local-origin.md). # X = -12024.5 cm (projected control point in local-origin.md).
import os
import sys
import unreal import unreal
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import ue_common # noqa: E402
FBX = "/tmp/gigline/hero-facade.fbx" FBX = "/tmp/gigline/hero-facade.fbx"
DEST = "/Game/Hollywood/ElCapitan" DEST = "/Game/Hollywood/ElCapitan"
ACTOR_LABEL = "Hero_Facade" ACTOR_LABEL = "Hero_Facade"
@@ -62,34 +56,11 @@ def import_fbx():
unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True) 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(): def measure_and_place():
asset = unreal.EditorAssetLibrary.load_asset(f"{DEST}/SM_HeroFacade") asset = unreal.EditorAssetLibrary.load_asset(f"{DEST}/SM_HeroFacade")
if asset is None: if asset is None:
log(f"MISSING {DEST}/SM_HeroFacade") log(f"MISSING {DEST}/SM_HeroFacade")
return return
# Collision: complex-as-simple, not auto-generated convex hulls.
# The facade is 95 separate parts across 26 m; Unreal's auto hull generation
# wraps them in a few coarse hulls that bulged into the street (the QA wall
# probe went 0/35 -> 2/35 after trying it). Complex-as-simple makes the real
# triangles the simple collision, so line traces and capsule sweeps block on
# the actual wall — the same approach used for the terrain, for the same
# reason. The flag lives on the asset, so save it explicitly.
unreal.GigLineAssetTools.set_complex_collision_as_simple(asset, True)
saved = unreal.EditorAssetLibrary.save_loaded_asset(asset)
log(f"collision: {unreal.GigLineAssetTools.get_collision_complexity(asset)} (saved={saved})")
box = asset.get_bounding_box() box = asset.get_bounding_box()
mn, mx = box.min, box.max mn, mx = box.min, box.max
log(f"SM_HeroFacade bounds cm: size=({mx.x - mn.x:.1f}, {mx.y - mn.y:.1f}, {mx.z - mn.z:.1f}) " log(f"SM_HeroFacade bounds cm: size=({mx.x - mn.x:.1f}, {mx.y - mn.y:.1f}, {mx.z - mn.z:.1f}) "
@@ -118,12 +89,9 @@ def measure_and_place():
comp.set_static_mesh(asset) comp.set_static_mesh(asset)
comp.set_mobility(unreal.ComponentMobility.STATIC) comp.set_mobility(unreal.ComponentMobility.STATIC)
log(f"placed {ACTOR_LABEL} at {PLACEMENT_CM} (cm) on {LEVEL}") log(f"placed {ACTOR_LABEL} at {PLACEMENT_CM} (cm) on {LEVEL}")
_make_map_writable(LEVEL)
saved = unreal.EditorLoadingAndSavingUtils.save_map( saved = unreal.EditorLoadingAndSavingUtils.save_map(
unreal.EditorLevelLibrary.get_editor_world(), LEVEL) unreal.EditorLevelLibrary.get_editor_world(), LEVEL)
log(f"save_map {LEVEL}: {saved}") 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: try:
-19
View File
@@ -15,9 +15,6 @@ import os
import unreal import unreal
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import ue_common # noqa: E402
SRC = "/tmp/gigline/north" SRC = "/tmp/gigline/north"
DEST = "/Game/Hollywood/North" DEST = "/Game/Hollywood/North"
LEVEL = "/Game/Hollywood/DemoMap" LEVEL = "/Game/Hollywood/DemoMap"
@@ -75,27 +72,11 @@ def stage():
mn, mx = box.min, box.max 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 " 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") f"{mx.x-mn.x:.0f}x{mx.y-mn.y:.0f} cm")
_make_map_writable(LEVEL)
saved = unreal.EditorLoadingAndSavingUtils.save_map( saved = unreal.EditorLoadingAndSavingUtils.save_map(
unreal.EditorLevelLibrary.get_editor_world(), LEVEL) unreal.EditorLevelLibrary.get_editor_world(), LEVEL)
log(f"save_map {LEVEL}: {saved}") 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(): def main():
for fn, asset_name in IMPORTS: for fn, asset_name in IMPORTS:
task = unreal.AssetImportTask() task = unreal.AssetImportTask()