Compare commits
9
Commits
v0.1.4
..
304c1e9afd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
304c1e9afd | ||
|
|
05c1488c00 | ||
|
|
96238bdb6a | ||
|
|
817313aabd | ||
|
|
73aca9db80 | ||
|
|
743a290ac9 | ||
|
|
71c7ca734b | ||
|
|
b8209ce82b | ||
|
|
a1d52aa583 |
No files matched your search
@@ -67,6 +67,7 @@ Rules for `TODO.md`:
|
||||
- When **all** steps in a version section are done: write a short descriptive entry to `CHANGELOG.md`, remove the version section from `TODO.md`, and leave its goal in `ROADMAP.md`.
|
||||
- Release checks for each version are listed at the end of its `TODO.md` section; `Docs/release-checks.md` defines the checks themselves. If a check fails, do not tag the release; open a patch task in `TODO.md` and report the failing check in `CHANGELOG.md`.
|
||||
- Finish each milestone's release checks **before** expanding scope. Cut map size and simulation complexity before compromising the core walking/driving experience.
|
||||
- **Realism leads, performance is a floor.** When a realism improvement costs frame time, realism wins and the cost is paid — but 60 FPS at 1440p without frame generation must still be met at every release. If realism cannot fit the budget, reduce the realism scope deliberately and record it in `CHANGELOG.md`; never ship below the floor without the owner's approval. Walkability outranks realism in one direction only: a block the player falls through is not a demo.
|
||||
|
||||
## Git workflow
|
||||
|
||||
@@ -209,8 +210,8 @@ Any change that affects the game's appearance — meshes, materials, lighting, l
|
||||
|
||||
1. Run `Tools/render_screenshots.sh` (rebuilds `Game/GigLine/Content/Test/ReviewMap.umap` from current content, runs the game on it, captures the fixed camera list, copies the PNGs into `Screenshots/`).
|
||||
2. Optional pre-check with a vision model: `Tools/judge_screenshot.py --image <shot.png> --expect "<what it should show>"` (or `--shots` to run every shot in the README) flags likely-missing content before a human looks. It reads `OPENROUTER_API_KEY` from `.env` (ignored). Small helper only — it never replaces the human verdict.
|
||||
3. Update `Screenshots/README.md`: one row per screenshot with what it shows and an approval state; describe any new shots or scene changes in the same change.
|
||||
3. Update `Screenshots/README.md` **in the same change as any visual work** — every change that affects appearance, not only new shots: the image is **embedded inline** (``) with its view, expected content, verification and approval state, plus one row in the status table at the top. The review happens in the file; a path alone is not a review, and a shot whose README section is stale is a defect.
|
||||
4. The PNGs **are committed** so reviewers see exactly what the build rendered (`.gitignore` does not exclude them). Keep them in sync with this README: drop retired shots in the same change.
|
||||
5. Wait for the user's approval before treating a visual change as done; record approval by ticking the table in `Screenshots/README.md`.
|
||||
5. Wait for the user's approval before treating a visual change as done; record approval by ticking both the shot's section and its row in the status table in `Screenshots/README.md`.
|
||||
|
||||
New content that needs a different camera angle gets a new entry in the shot list in `Tools/ue_build_review_map.py`.
|
||||
Binary file not shown.
@@ -37,6 +37,15 @@
|
||||
],
|
||||
"evidence": "6 columns at 4.96 m centres (sibling notes x measured frontage)"
|
||||
},
|
||||
{
|
||||
"object": "Hero_Flute_x120",
|
||||
"dims_m": [
|
||||
0.064,
|
||||
0.064,
|
||||
6.53
|
||||
],
|
||||
"evidence": "photographs: fluted shafts; 20 grooves per column modelled as shallow cylinders in the shadow tone rather than boolean cut grooves"
|
||||
},
|
||||
{
|
||||
"object": "Hero_ColumnBase_x6",
|
||||
"dims_m": [
|
||||
@@ -104,10 +113,10 @@
|
||||
"object": "Hero_MottoBand",
|
||||
"dims_m": [
|
||||
22.75,
|
||||
0.08,
|
||||
0.1,
|
||||
0.85
|
||||
],
|
||||
"evidence": "photographs: motto panel above the colonnade"
|
||||
"evidence": "photographs: motto panel above the colonnade, now an inset panel with a moulded surround rather than a flat stripe"
|
||||
},
|
||||
{
|
||||
"object": "Hero_Medallion_x2",
|
||||
|
||||
+38
-1
@@ -8,6 +8,42 @@ Format: [Keep a Changelog](https://keepachangelog.com/) style, versions follow [
|
||||
|
||||
Nothing yet.
|
||||
|
||||
## [0.1.5] — 2026-11-09
|
||||
|
||||
Façade finishing. The kit gets the hero detail it was missing, and a class of silent defect is found and gated: the map had been rendering with **604 of 620 mesh actors unmaterialed** without a single error.
|
||||
|
||||
### Added
|
||||
|
||||
- **Scene audit** (`Tools/ue_audit_scene.py`): fails when any staged mesh has no material, when a frontage building is more than ±1 m off its verified footprint, or when two actors share a label. Wired into `Tools/render_screenshots.sh`, which now runs the whole staging chain in the correct order and stops on an audit failure.
|
||||
- **`Tools/ue_placement.py`**: one source of truth for staged positions and the verified frontage spans, with `span_error` / `describe_span_failure` validators.
|
||||
- **Hero fluting**: 20 grooves per column as shallow shadow-tone cylinders (120 pieces) rather than boolean cuts.
|
||||
- **Presence of references for the retail run**: `Art/reference/retail-neighbours/` (6800 Lee Drug and its neighbour). Local only, not committed.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Motto band is a real recess** now — moulded surround 6 cm proud, panel 12 cm behind — instead of an 8 cm dark stripe. Hero: 148 parts / 3 044 tris → 271 / 5 492, still 14.50 m.
|
||||
- **Kit layout for wide and tall buildings**: bays are capped at 9 with the module widened (6922 was 13 bays across 65.8 m — a window every 4.96 m on a 37 m block), and the storey spread is derived from the available height so the top band always lands inside the volume.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **604 of 620 mesh actors rendered untextured.** `ensure_material` deleted and recreated each site material on every run, orphaning every reference held by the kit pieces and the theatre trim; the map looked only slightly flat. Materials are now updated in place.
|
||||
- **The hero could be teleported 13 m east onto the wrong side of the boulevard.** `ue_hero_facade_import.py` carried its own copy of the pre-`0.1.0` hero position; running the importer alone moved the building, invisible in a render and visible only as a reference-score drop. Placement now has one definition and the import asserts the staged span (`span ok: X -14644..-12028`).
|
||||
- **53 duplicated kit actor labels** (both shop piers of a bay shared one name).
|
||||
|
||||
### Verified
|
||||
|
||||
- Scene audit: **0 failures** — every mesh materialed, all 7 verified spans within tolerance, no duplicate labels.
|
||||
- Packaged `-qaselftest`: walk, reset, pause/unpause, seam transect 1456 samples / 0 holes, wall probe 0/35, façade probe 0/9 — all PASS.
|
||||
- **1440p re-measured with the kit**: 2.76 ms average (**363 fps**), p99 3.64 ms, against `0.1.0`'s 2.42 ms (414 fps) — the 625 kit actors and 8 textures cost about 12 % and leave roughly six times the 60 fps floor. Series at `Docs/perf/0.1.5-kit-walk-frametimes.csv`.
|
||||
- 78 tool tests pass.
|
||||
|
||||
### Known limitations
|
||||
|
||||
- **Retail neighbours 6822 and 6806–6820 are not modelled**: Commons holds no usable reference for either, and substituting the El Capitan Theatre's photographs would misattribute their façades. Tracked in `TODO.md`.
|
||||
- Cornice dentil spacing, abacus and plinth dimensions remain recorded assumptions.
|
||||
- Textures are generated, not authored — no decals, signage text or weathering maps.
|
||||
- The vision pre-check stays advisory; it has rejected shots the owner approved.
|
||||
|
||||
## [0.1.4] — 2026-11-09
|
||||
|
||||
Building façades. The block goes from flat extruded boxes with flat colours to coursed, textured surfaces assembled from a reusable module kit, and the El Capitan Theatre gets the marquee and blade sign that make it read as itself.
|
||||
@@ -172,7 +208,8 @@ The first playable release: a walkable exterior demo of the **6840 Hollywood Blv
|
||||
- Kerb height (0.13 m as built vs 0.15 m assumed), kerb radii, driveway aprons and graded crossing ramps are deferred to `0.1.1`; crossing ramps are kerb flushes.
|
||||
- One block only; no vehicles, NPCs, audio, or settings UI yet.
|
||||
|
||||
[Unreleased]: https://git.allucanget.biz/zwitschi/gig-line/compare/v0.1.4...main
|
||||
[Unreleased]: https://git.allucanget.biz/zwitschi/gig-line/compare/v0.1.5...main
|
||||
[0.1.5]: https://git.allucanget.biz/zwitschi/gig-line/releases/tag/v0.1.5
|
||||
[0.1.4]: https://git.allucanget.biz/zwitschi/gig-line/releases/tag/v0.1.4
|
||||
[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
|
||||
|
||||
@@ -20,11 +20,14 @@ Full requirements and progress are in [TODO.md](../../TODO.md); version directio
|
||||
|
||||
| Priority | Quality Goal | Scenario |
|
||||
| --- | --- | --- |
|
||||
| 1 | **Performance efficiency** | 60 FPS at 2560×1440 on RTX 5070 Ti, documented preset, no frame generation |
|
||||
| 2 | **Terrain walkability** | Zero raycast holes across both sidewalks and the carriageway (84-sample transect passes) |
|
||||
| 3 | **Geographic fidelity** | Building placement, street scale, and façade identity match reference photography |
|
||||
| 1 | **Visual realism** | Renders read as the reference photographs: façade identity, proportion, material and light — judged side by side, shot against photo |
|
||||
| 2 | **Geographic fidelity** | Building placement and street scale match the measured data and the photographs |
|
||||
| 3 | **Terrain walkability** | Zero raycast holes across both sidewalks and the carriageway (84-sample transect passes) |
|
||||
| 4 | **Build reproducibility** | Fresh checkout + documented engine path produces a runnable Linux build |
|
||||
| 5 | **Provenance** | Every committed asset has recorded provenance |
|
||||
| 6 | **Performance efficiency** | 60 FPS at 2560×1440 on RTX 5070 Ti, documented preset, no frame generation |
|
||||
|
||||
**How the order is used.** It decides trade-offs. When realism and performance disagree, realism wins and the cost is paid — but never by silently shipping a frame rate below the floor. The 60 FPS figure is a **budget that must still be met**, so the response to "realism costs too much frame time" is to cut the realism feature deliberately, record the cut, and keep the floor — not to let frames slip. Walkability outranks realism in one direction only: a beautiful block the player falls through is not a demo.
|
||||
|
||||
## Stakeholders
|
||||
|
||||
|
||||
@@ -30,7 +30,8 @@ All modelling and geographic processing live outside the game project in `Art/`,
|
||||
|
||||
## Quality achievement
|
||||
|
||||
- **Performance**: dynamic-only lighting (no build step, no static shadow maps), world-space-noise materials (cheap procedural variation), minimal overdraw (simple street-level geometry).
|
||||
- **Realism first**: reference photography and measured data drive every façade decision; renders are compared against the photographs with `Tools/visual_loop.py`, and a façade is built from the shared 4.96 m module kit so the street wall reads consistently. Materials are authored in Unreal with world-aligned generated textures, and lighting is fixed daytime so a shot is comparable between runs.
|
||||
- **Performance as a budget, not a driver**: dynamic-only lighting (no build step, no static shadow maps), world-space-noise materials (cheap procedural variation), minimal overdraw for street-level geometry, and a 60 FPS at 1440p floor that every release must meet. It closes the quality list because it constrains how much realism fits, not what the block should look like.
|
||||
|
||||
- **Walkability**: complex-as-simple collision on terrain (C++ helper forces the flag on the asset), block-all collision on every site mesh, swept transect QA verifies zero raycast holes.
|
||||
|
||||
|
||||
@@ -25,7 +25,9 @@ C++ classes in `Game/GigLine/Source/GigLine/`:
|
||||
| `AGigLineShotActor` | Screenshot camera walker | Drives cameras in `-game` mode, `FScreenshotRequest`, quits |
|
||||
| `UGigLineAssetTools` | C++ collision helpers | `SetComplexCollisionAsSimple` on `StaticMesh` assets |
|
||||
|
||||
Input assets in `/Game/Input/`: `IA_Move`, `IA_MoveBack`, `IA_Strafe`, `IA_StrafeLeft`, `IA_Look`, `IA_Jump`, `IA_Reset`, `IA_Pause`, bound in `IMC_Default`.
|
||||
Input assets in `/Game/Input/`: `IA_Move`, `IA_MoveBack`, `IA_Strafe`, `IA_StrafeLeft`, `IA_Look`, `IA_Jump`, `IA_Reset`, `IA_Pause`, `IA_ToggleCamera`, bound in `IMC_Default`.
|
||||
|
||||
Player-facing bindings: **WASD** move, **mouse** look, **Space** jump, **R** reset, **Escape** pause, **V** swap first/third person. The camera and body work landed with `0.2.0` (see ADR-9): the shipped default view stays first person, and `-thirdperson` starts the pawn in the third-person rig — used by the screenshot run so the review is reproducible.
|
||||
|
||||
### Site
|
||||
|
||||
@@ -56,6 +58,7 @@ Python scripts in `Tools/` drive the content pipeline:
|
||||
| --- | --- |
|
||||
| `export_*`, `hero_facade_build` | Blender headless export (FBX) |
|
||||
| `ue_import_*`, `ue_hero_facade_import` | Unreal headless import + placement |
|
||||
| `character_anim_build`, `ue_character_anim_import` | Character animation clips (`GigLine_Idle`, `GigLine_Walk`) and their import |
|
||||
| `ue_build_demo_map`, `ue_build_review_map` | Map assembly (lighting, meshes, player start) |
|
||||
| `ue_create_input_assets` | Enhanced Input actions + mapping context |
|
||||
| `render_screenshots.sh` | Screenshot capture pipeline |
|
||||
|
||||
@@ -55,3 +55,11 @@
|
||||
- **Decision**: All Blender work driven by Python scripts in `Tools/`. Skill packs used as method guides (checklists, naming, proportions) only.
|
||||
|
||||
- **Consequences**: Scripts are version-controlled and repeatable. No live Blender session needed for CI.
|
||||
|
||||
## ADR-9: Third-person body is an attachment of the first-person pawn, not a second pawn
|
||||
|
||||
- **Context**: `0.2.0` needs a visible body and a third-person camera, while `0.1.0` shipped a first-person pawn with no body and the owner's decision was first person for that release. Two separate pawn classes would duplicate movement, input and reset logic, which is the code most likely to drift.
|
||||
|
||||
- **Decision**: one pawn, both rigs. `AGigLineCharacter` carries a skeletal mesh (hidden in first person), a collision-tested spring-arm camera, and a **V** toggle that swaps the active camera and the body's visibility. The shipped default view stays first person; `-thirdperson` starts the pawn in the third-person rig and is what the screenshot run passes, so the review shot is reproducible without a human pressing a key.
|
||||
|
||||
- **Consequences**: movement, reset, pause and the QA self-test stay in one place. The body's animation state is driven from `Tick` by speed rather than from input, so being pushed or falling animates correctly. The cost is that the camera swap is not a separate actor, so a future cinematic camera needs its own path rather than reusing the toggle.
|
||||
@@ -2,24 +2,34 @@
|
||||
|
||||
## Quality Requirements Overview
|
||||
|
||||
| Category | Requirement | Priority |
|
||||
Ordered by priority: realism first, performance last. The order decides design trade-offs; it does not license dropping a lower item silently (see the trade-off rule below the table).
|
||||
|
||||
| Priority | Category | Requirement |
|
||||
| --- | --- | --- |
|
||||
| Performance efficiency | 60 FPS at 2560×1440, documented preset, no frame generation | 1 |
|
||||
| Reliability / Terrain | Zero raycast holes across sidewalks and carriageway (84-sample transect) | 1 |
|
||||
| Accuracy / Fidelity | Building placement matches reference photography within ~1 m | 2 |
|
||||
| Accuracy / Fidelity | Street scale (lane width, sidewalk width) within ~10 cm of measured profile | 2 |
|
||||
| Usability | WASD + mouse look, jump, reset (R), pause (Escape) — no tutorial needed | 2 |
|
||||
| Maintainability | Fresh checkout builds with documented engine installation | 2 |
|
||||
| Portability | Linux build packages and runs; Windows build ships | 3 |
|
||||
| 1 | Realism / Fidelity | Façade identity, proportion, material and lighting read like the reference photographs when compared side by side |
|
||||
| 2 | Realism / Fidelity | Building placement matches the measured footprints and the photographs (within ~1 m) |
|
||||
| 2 | Realism / Fidelity | Street scale (lane width, sidewalk width) within ~10 cm of the measured profile |
|
||||
| 3 | Reliability / Terrain | Zero raycast holes across sidewalks and carriageway (84-sample transect) |
|
||||
| 3 | Usability | WASD + mouse look, jump, reset (R), pause (Escape) — no tutorial needed |
|
||||
| 4 | Maintainability | Fresh checkout builds with the documented engine installation |
|
||||
| 4 | Portability | Linux build packages and runs; Windows build ships |
|
||||
| 5 | Provenance | Every committed asset has a recorded source and licence |
|
||||
| 6 | Performance efficiency | 60 FPS at 2560×1440, documented preset, no frame generation |
|
||||
|
||||
**Trade-off rule.** Realism leads: when a realism improvement costs frame time, the improvement wins. Performance is still a **floor, not a preference** — 60 FPS at 1440p without frame generation must be met at every release. If realism cannot fit inside that budget, reduce the realism scope deliberately and record the reduction in `CHANGELOG.md`; never ship below the floor without the owner's explicit approval. Walkability outranks realism in one direction: a block the player falls through is not a demo, whatever it looks like.
|
||||
|
||||
## Quality Scenarios
|
||||
|
||||
| ID | Source/Stimulus | Response | Metric |
|
||||
| --- | --- | --- | --- |
|
||||
| QS-1 | Player walks both sides of the demo | No fall-through, character stays at ground Z | 0 raycast holes in 84-sample transect |
|
||||
| QS-2 | Player holds W for 2 s on flat road | Character moves forward | ≥ 200 cm displacement, Z constant within ±5 cm |
|
||||
| QS-3 | Player presses R | Character teleports to spawn | < 100 cm from `StartLocation` |
|
||||
| QS-4 | Player presses Escape | Game pauses, cursor appears | `IsGamePaused()` = true |
|
||||
| QS-5 | Player presses Escape while paused | Game unpauses, cursor hides | `IsGamePaused()` = false |
|
||||
| QS-6 | Game runs on dev workstation at 1440p | Frame rate stable | ≥ 60 FPS, 99th percentile ≥ 55 FPS |
|
||||
| QS-7 | Fresh checkout + engine install | Build succeeds | Exit code 0, runnable binary produced |
|
||||
| QS-1 | Screenshot is compared against its reference photograph | The render reads as the same building | Similarity score and visual match confirmed by the owner; `Tools/visual_loop.py` reports the deltas |
|
||||
| QS-2 | A façade is placed from measured data | It sits on its verified footprint | World span probed against `Geo/verification/` within ~1 m |
|
||||
| QS-3 | Player walks both sides of the demo | No fall-through, character stays at ground Z | 0 raycast holes in 84-sample transect |
|
||||
| QS-4 | Player holds W for 2 s on flat road | Character moves forward | ≥ 200 cm displacement, Z constant within ±5 cm |
|
||||
| QS-5 | Player presses R | Character teleports to spawn | < 100 cm from `StartLocation` |
|
||||
| QS-6 | Player presses Escape | Game pauses, cursor appears | `IsGamePaused()` = true |
|
||||
| QS-7 | Player presses Escape while paused | Game unpauses, cursor hides | `IsGamePaused()` = false |
|
||||
| QS-8 | Fresh checkout + engine install | Build succeeds | Exit code 0, runnable binary produced |
|
||||
| QS-9 | Game runs on dev workstation at 1440p | Frame rate holds the floor | ≥ 60 FPS, 99th percentile ≥ 55 FPS |
|
||||
|
||||
> QS-9 is last on purpose. Performance is a floor that every release must meet, not the goal that shapes the content: realism decides what gets built, the frame budget decides how much of it.
|
||||
@@ -27,11 +27,14 @@ Full requirements and progress are in [TODO.md](../../TODO.md); version directio
|
||||
|
||||
| Priority | Quality Goal | Scenario |
|
||||
| --- | --- | --- |
|
||||
| 1 | **Performance efficiency** | 60 FPS at 2560×1440 on RTX 5070 Ti, documented preset, no frame generation |
|
||||
| 2 | **Terrain walkability** | Zero raycast holes across both sidewalks and the carriageway (84-sample transect passes) |
|
||||
| 3 | **Geographic fidelity** | Building placement, street scale, and façade identity match reference photography |
|
||||
| 1 | **Visual realism** | Renders read as the reference photographs: façade identity, proportion, material and light — judged side by side, shot against photo |
|
||||
| 2 | **Geographic fidelity** | Building placement and street scale match the measured data and the photographs |
|
||||
| 3 | **Terrain walkability** | Zero raycast holes across both sidewalks and the carriageway (84-sample transect passes) |
|
||||
| 4 | **Build reproducibility** | Fresh checkout + documented engine path produces a runnable Linux build |
|
||||
| 5 | **Provenance** | Every committed asset has recorded provenance |
|
||||
| 6 | **Performance efficiency** | 60 FPS at 2560×1440 on RTX 5070 Ti, documented preset, no frame generation |
|
||||
|
||||
**How the order is used.** It decides trade-offs. When realism and performance disagree, realism wins and the cost is paid — but never by silently shipping a frame rate below the floor. The 60 FPS figure is a **budget that must still be met**, so the response to "realism costs too much frame time" is to cut the realism feature deliberately, record the cut, and keep the floor — not to let frames slip. Walkability outranks realism in one direction only: a beautiful block the player falls through is not a demo.
|
||||
|
||||
## Stakeholders
|
||||
|
||||
@@ -129,7 +132,9 @@ All modelling and geographic processing live outside the game project in `Art/`,
|
||||
|
||||
## Quality achievement
|
||||
|
||||
- **Performance**: dynamic-only lighting (no build step, no static shadow maps), world-space-noise materials (cheap procedural variation), minimal overdraw (simple street-level geometry).
|
||||
- **Realism**: reference photography drives every façade decision, measured data fixes placement and scale, and renders are compared against the photographs (`Tools/visual_loop.py`, `Screenshots/README.md`). One module kit at the 4.96 m bay keeps the street wall coherent; materials are world-aligned and lit by a fixed daytime rig so a shot is comparable between runs.
|
||||
|
||||
- **Performance holds the floor, it does not set the agenda**: dynamic-only lighting (no build step, no static shadow maps), cheap procedural materials, minimal overdraw for street-level geometry, and 60 FPS at 1440p as a budget every release must meet.
|
||||
|
||||
- **Walkability**: complex-as-simple collision on terrain (C++ helper forces the flag on the asset), block-all collision on every site mesh, swept transect QA verifies zero raycast holes.
|
||||
|
||||
@@ -164,7 +169,9 @@ C++ classes in `Game/GigLine/Source/GigLine/`:
|
||||
| `AGigLineShotActor` | Screenshot camera walker | Drives cameras in `-game` mode, `FScreenshotRequest`, quits |
|
||||
| `UGigLineAssetTools` | C++ collision helpers | `SetComplexCollisionAsSimple` on `StaticMesh` assets |
|
||||
|
||||
Input assets in `/Game/Input/`: `IA_Move`, `IA_MoveBack`, `IA_Strafe`, `IA_StrafeLeft`, `IA_Look`, `IA_Jump`, `IA_Reset`, `IA_Pause`, bound in `IMC_Default`.
|
||||
Input assets in `/Game/Input/`: `IA_Move`, `IA_MoveBack`, `IA_Strafe`, `IA_StrafeLeft`, `IA_Look`, `IA_Jump`, `IA_Reset`, `IA_Pause`, `IA_ToggleCamera`, bound in `IMC_Default`.
|
||||
|
||||
Player-facing bindings: **WASD** move, **mouse** look, **Space** jump, **R** reset, **Escape** pause, **V** swap first/third person. The camera and body work landed with `0.2.0` (see ADR-9): the shipped default view stays first person, and `-thirdperson` starts the pawn in the third-person rig — used by the screenshot run so the review is reproducible.
|
||||
|
||||
### Site
|
||||
|
||||
@@ -195,6 +202,7 @@ Python scripts in `Tools/` drive the content pipeline:
|
||||
| --- | --- |
|
||||
| `export_*`, `hero_facade_build` | Blender headless export (FBX) |
|
||||
| `ue_import_*`, `ue_hero_facade_import` | Unreal headless import + placement |
|
||||
| `character_anim_build`, `ue_character_anim_import` | Character animation clips (`GigLine_Idle`, `GigLine_Walk`) and their import |
|
||||
| `ue_build_demo_map`, `ue_build_review_map` | Map assembly (lighting, meshes, player start) |
|
||||
| `ue_create_input_assets` | Enhanced Input actions + mapping context |
|
||||
| `render_screenshots.sh` | Screenshot capture pipeline |
|
||||
@@ -450,27 +458,37 @@ Rule: place a building only against a neighbour or street mesh, never in isolati
|
||||
|
||||
## Quality Requirements Overview
|
||||
|
||||
| Category | Requirement | Priority |
|
||||
Ordered by priority: realism first, performance last. The order decides design trade-offs; it does not license dropping a lower item silently (see the trade-off rule below the table).
|
||||
|
||||
| Priority | Category | Requirement |
|
||||
| --- | --- | --- |
|
||||
| Performance efficiency | 60 FPS at 2560×1440, documented preset, no frame generation | 1 |
|
||||
| Reliability / Terrain | Zero raycast holes across sidewalks and carriageway (84-sample transect) | 1 |
|
||||
| Accuracy / Fidelity | Building placement matches reference photography within ~1 m | 2 |
|
||||
| Accuracy / Fidelity | Street scale (lane width, sidewalk width) within ~10 cm of measured profile | 2 |
|
||||
| Usability | WASD + mouse look, jump, reset (R), pause (Escape) — no tutorial needed | 2 |
|
||||
| Maintainability | Fresh checkout builds with documented engine installation | 2 |
|
||||
| Portability | Linux build packages and runs; Windows build ships | 3 |
|
||||
| 1 | Realism / Fidelity | Façade identity, proportion, material and lighting read like the reference photographs when compared side by side |
|
||||
| 2 | Realism / Fidelity | Building placement matches the measured footprints and the photographs (within ~1 m) |
|
||||
| 2 | Realism / Fidelity | Street scale (lane width, sidewalk width) within ~10 cm of the measured profile |
|
||||
| 3 | Reliability / Terrain | Zero raycast holes across sidewalks and carriageway (84-sample transect) |
|
||||
| 3 | Usability | WASD + mouse look, jump, reset (R), pause (Escape) — no tutorial needed |
|
||||
| 4 | Maintainability | Fresh checkout builds with the documented engine installation |
|
||||
| 4 | Portability | Linux build packages and runs; Windows build ships |
|
||||
| 5 | Provenance | Every committed asset has a recorded source and licence |
|
||||
| 6 | Performance efficiency | 60 FPS at 2560×1440, documented preset, no frame generation |
|
||||
|
||||
**Trade-off rule.** Realism leads: when a realism improvement costs frame time, the improvement wins. Performance is still a **floor, not a preference** — 60 FPS at 1440p without frame generation must be met at every release. If realism cannot fit inside that budget, reduce the realism scope deliberately and record the reduction in `CHANGELOG.md`; never ship below the floor without the owner's explicit approval. Walkability outranks realism in one direction: a block the player falls through is not a demo, whatever it looks like.
|
||||
|
||||
## Quality Scenarios
|
||||
|
||||
| ID | Source/Stimulus | Response | Metric |
|
||||
| --- | --- | --- | --- |
|
||||
| QS-1 | Player walks both sides of the demo | No fall-through, character stays at ground Z | 0 raycast holes in 84-sample transect |
|
||||
| QS-2 | Player holds W for 2 s on flat road | Character moves forward | ≥ 200 cm displacement, Z constant within ±5 cm |
|
||||
| QS-3 | Player presses R | Character teleports to spawn | < 100 cm from `StartLocation` |
|
||||
| QS-4 | Player presses Escape | Game pauses, cursor appears | `IsGamePaused()` = true |
|
||||
| QS-5 | Player presses Escape while paused | Game unpauses, cursor hides | `IsGamePaused()` = false |
|
||||
| QS-6 | Game runs on dev workstation at 1440p | Frame rate stable | ≥ 60 FPS, 99th percentile ≥ 55 FPS |
|
||||
| QS-7 | Fresh checkout + engine install | Build succeeds | Exit code 0, runnable binary produced |
|
||||
| QS-1 | Screenshot is compared against its reference photograph | The render reads as the same building | Similarity score and visual match confirmed by the owner; `Tools/visual_loop.py` reports the deltas |
|
||||
| QS-2 | A façade is placed from measured data | It sits on its verified footprint | World span probed against `Geo/verification/` within ~1 m |
|
||||
| QS-3 | Player walks both sides of the demo | No fall-through, character stays at ground Z | 0 raycast holes in 84-sample transect |
|
||||
| QS-4 | Player holds W for 2 s on flat road | Character moves forward | ≥ 200 cm displacement, Z constant within ±5 cm |
|
||||
| QS-5 | Player presses R | Character teleports to spawn | < 100 cm from `StartLocation` |
|
||||
| QS-6 | Player presses Escape | Game pauses, cursor appears | `IsGamePaused()` = true |
|
||||
| QS-7 | Player presses Escape while paused | Game unpauses, cursor hides | `IsGamePaused()` = false |
|
||||
| QS-8 | Fresh checkout + engine install | Build succeeds | Exit code 0, runnable binary produced |
|
||||
| QS-9 | Game runs on dev workstation at 1440p | Frame rate holds the floor | ≥ 60 FPS, 99th percentile ≥ 55 FPS |
|
||||
|
||||
> QS-9 is last on purpose. Performance is a floor that every release must meet, not the goal that shapes the content: realism decides what gets built, the frame budget decides how much of it.
|
||||
|
||||
# Risks and Technical Debts
|
||||
|
||||
|
||||
@@ -86,8 +86,13 @@ The hero block and the demo level are built by a fixed sequence. Each step write
|
||||
| North import | `Tools/ue_import_north_buildings.py` | `/Game/Hollywood/North/*` |
|
||||
| Hero import | `Tools/ue_hero_facade_import.py` | `/Game/Hollywood/ElCapitan/SM_HeroFacade` |
|
||||
| Demo map | `Tools/ue_build_demo_map.py` | `/Game/Hollywood/DemoMap` |
|
||||
| Façade kit assembly | `Tools/ue_apply_facade_kit.py` | kit actors in `DemoMap` |
|
||||
| Theatre trim | `Tools/ue_stage_theatre_trim.py` | marquee + blade sign actors |
|
||||
| Scene audit | `Tools/ue_audit_scene.py` | materials, placement spans, labels (gate) |
|
||||
| Review shots | `Tools/ue_build_review_map.py` + `Tools/render_screenshots.sh` | `Screenshots/` |
|
||||
|
||||
**Order matters.** `ue_build_demo_map.py` rebuilds the site materials, so any actor staged by another tool must be (re)staged _after_ it, or it keeps a reference to a material that no longer exists — that is how 604 actors once ended up untethered from their materials while the map only looked slightly flat. `Tools/render_screenshots.sh` runs the full chain in the right order and gates on `ue_audit_scene.py`.
|
||||
|
||||
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:
|
||||
|
||||
@@ -0,0 +1,728 @@
|
||||
frame,ms
|
||||
0,2.599
|
||||
1,2.877
|
||||
2,2.709
|
||||
3,2.646
|
||||
4,3.168
|
||||
5,2.729
|
||||
6,2.524
|
||||
7,2.726
|
||||
8,2.639
|
||||
9,2.814
|
||||
10,2.819
|
||||
11,2.708
|
||||
12,2.822
|
||||
13,2.703
|
||||
14,2.365
|
||||
15,2.796
|
||||
16,2.899
|
||||
17,2.972
|
||||
18,2.908
|
||||
19,2.356
|
||||
20,2.781
|
||||
21,2.574
|
||||
22,3.148
|
||||
23,2.306
|
||||
24,2.668
|
||||
25,2.855
|
||||
26,2.588
|
||||
27,3.321
|
||||
28,3.419
|
||||
29,3.121
|
||||
30,3.391
|
||||
31,2.355
|
||||
32,2.679
|
||||
33,3.463
|
||||
34,2.614
|
||||
35,2.922
|
||||
36,2.429
|
||||
37,2.830
|
||||
38,2.842
|
||||
39,2.782
|
||||
40,3.252
|
||||
41,2.731
|
||||
42,2.577
|
||||
43,2.363
|
||||
44,2.786
|
||||
45,2.547
|
||||
46,2.594
|
||||
47,2.652
|
||||
48,2.836
|
||||
49,2.792
|
||||
50,2.552
|
||||
51,2.792
|
||||
52,2.718
|
||||
53,3.333
|
||||
54,2.165
|
||||
55,2.880
|
||||
56,2.613
|
||||
57,2.460
|
||||
58,2.919
|
||||
59,2.770
|
||||
60,2.551
|
||||
61,2.622
|
||||
62,2.712
|
||||
63,2.818
|
||||
64,3.155
|
||||
65,2.472
|
||||
66,2.861
|
||||
67,2.651
|
||||
68,2.573
|
||||
69,2.892
|
||||
70,2.689
|
||||
71,2.729
|
||||
72,2.668
|
||||
73,2.753
|
||||
74,2.812
|
||||
75,2.606
|
||||
76,2.686
|
||||
77,2.842
|
||||
78,2.660
|
||||
79,2.903
|
||||
80,2.486
|
||||
81,2.591
|
||||
82,3.626
|
||||
83,2.455
|
||||
84,2.475
|
||||
85,2.922
|
||||
86,2.603
|
||||
87,2.728
|
||||
88,3.057
|
||||
89,2.731
|
||||
90,3.632
|
||||
91,1.950
|
||||
92,2.694
|
||||
93,3.020
|
||||
94,2.230
|
||||
95,2.796
|
||||
96,2.707
|
||||
97,2.807
|
||||
98,2.658
|
||||
99,2.697
|
||||
100,2.453
|
||||
101,3.051
|
||||
102,2.758
|
||||
103,2.662
|
||||
104,2.599
|
||||
105,2.733
|
||||
106,2.751
|
||||
107,2.894
|
||||
108,2.501
|
||||
109,2.753
|
||||
110,2.873
|
||||
111,2.439
|
||||
112,2.634
|
||||
113,2.682
|
||||
114,2.913
|
||||
115,2.692
|
||||
116,2.735
|
||||
117,2.758
|
||||
118,2.726
|
||||
119,2.907
|
||||
120,2.568
|
||||
121,2.764
|
||||
122,2.815
|
||||
123,2.530
|
||||
124,2.684
|
||||
125,2.719
|
||||
126,2.846
|
||||
127,2.879
|
||||
128,2.647
|
||||
129,2.770
|
||||
130,2.477
|
||||
131,2.657
|
||||
132,2.675
|
||||
133,2.917
|
||||
134,2.700
|
||||
135,2.493
|
||||
136,2.678
|
||||
137,2.786
|
||||
138,3.004
|
||||
139,2.697
|
||||
140,2.642
|
||||
141,2.624
|
||||
142,2.684
|
||||
143,2.717
|
||||
144,2.743
|
||||
145,2.740
|
||||
146,2.825
|
||||
147,2.849
|
||||
148,2.991
|
||||
149,2.297
|
||||
150,3.084
|
||||
151,2.459
|
||||
152,3.214
|
||||
153,2.260
|
||||
154,2.550
|
||||
155,2.621
|
||||
156,2.660
|
||||
157,2.739
|
||||
158,2.574
|
||||
159,3.078
|
||||
160,2.735
|
||||
161,2.607
|
||||
162,2.793
|
||||
163,2.798
|
||||
164,2.584
|
||||
165,2.637
|
||||
166,2.627
|
||||
167,2.726
|
||||
168,2.656
|
||||
169,2.788
|
||||
170,2.674
|
||||
171,2.782
|
||||
172,2.766
|
||||
173,2.613
|
||||
174,2.724
|
||||
175,2.770
|
||||
176,2.709
|
||||
177,2.700
|
||||
178,2.600
|
||||
179,2.686
|
||||
180,2.747
|
||||
181,2.841
|
||||
182,2.601
|
||||
183,2.666
|
||||
184,2.846
|
||||
185,2.599
|
||||
186,2.806
|
||||
187,3.126
|
||||
188,2.521
|
||||
189,2.634
|
||||
190,2.716
|
||||
191,2.584
|
||||
192,2.714
|
||||
193,2.923
|
||||
194,2.277
|
||||
195,2.706
|
||||
196,2.606
|
||||
197,2.758
|
||||
198,2.747
|
||||
199,2.958
|
||||
200,2.798
|
||||
201,2.387
|
||||
202,2.596
|
||||
203,2.861
|
||||
204,2.801
|
||||
205,2.521
|
||||
206,2.869
|
||||
207,2.884
|
||||
208,2.515
|
||||
209,2.592
|
||||
210,3.723
|
||||
211,2.474
|
||||
212,2.736
|
||||
213,2.774
|
||||
214,2.769
|
||||
215,3.007
|
||||
216,2.768
|
||||
217,2.716
|
||||
218,2.694
|
||||
219,2.722
|
||||
220,2.862
|
||||
221,2.587
|
||||
222,2.833
|
||||
223,2.792
|
||||
224,2.746
|
||||
225,2.585
|
||||
226,2.741
|
||||
227,2.585
|
||||
228,2.802
|
||||
229,2.674
|
||||
230,2.742
|
||||
231,2.718
|
||||
232,2.881
|
||||
233,2.707
|
||||
234,2.585
|
||||
235,2.810
|
||||
236,2.948
|
||||
237,2.718
|
||||
238,2.773
|
||||
239,2.571
|
||||
240,2.788
|
||||
241,2.702
|
||||
242,2.634
|
||||
243,2.788
|
||||
244,2.678
|
||||
245,2.914
|
||||
246,2.527
|
||||
247,2.637
|
||||
248,2.877
|
||||
249,2.693
|
||||
250,2.644
|
||||
251,2.670
|
||||
252,2.700
|
||||
253,2.877
|
||||
254,2.638
|
||||
255,2.617
|
||||
256,2.774
|
||||
257,2.660
|
||||
258,2.731
|
||||
259,2.671
|
||||
260,2.838
|
||||
261,2.726
|
||||
262,2.671
|
||||
263,2.744
|
||||
264,2.735
|
||||
265,2.684
|
||||
266,3.850
|
||||
267,2.120
|
||||
268,2.669
|
||||
269,2.729
|
||||
270,2.712
|
||||
271,2.817
|
||||
272,2.926
|
||||
273,2.678
|
||||
274,2.786
|
||||
275,2.927
|
||||
276,2.493
|
||||
277,2.846
|
||||
278,2.610
|
||||
279,2.644
|
||||
280,2.577
|
||||
281,2.939
|
||||
282,2.727
|
||||
283,2.595
|
||||
284,2.819
|
||||
285,2.778
|
||||
286,2.774
|
||||
287,2.628
|
||||
288,2.711
|
||||
289,2.699
|
||||
290,2.715
|
||||
291,2.763
|
||||
292,2.710
|
||||
293,2.892
|
||||
294,2.721
|
||||
295,2.575
|
||||
296,3.608
|
||||
297,2.811
|
||||
298,2.739
|
||||
299,2.751
|
||||
300,2.608
|
||||
301,2.913
|
||||
302,3.052
|
||||
303,2.862
|
||||
304,2.653
|
||||
305,2.727
|
||||
306,2.646
|
||||
307,2.888
|
||||
308,3.338
|
||||
309,2.743
|
||||
310,2.639
|
||||
311,2.681
|
||||
312,2.846
|
||||
313,2.766
|
||||
314,3.184
|
||||
315,2.834
|
||||
316,2.728
|
||||
317,2.581
|
||||
318,2.770
|
||||
319,3.351
|
||||
320,3.157
|
||||
321,2.508
|
||||
322,2.835
|
||||
323,2.754
|
||||
324,2.411
|
||||
325,3.398
|
||||
326,2.802
|
||||
327,2.611
|
||||
328,3.058
|
||||
329,2.407
|
||||
330,2.640
|
||||
331,3.645
|
||||
332,2.729
|
||||
333,2.580
|
||||
334,2.902
|
||||
335,2.523
|
||||
336,3.005
|
||||
337,3.173
|
||||
338,2.840
|
||||
339,2.610
|
||||
340,2.576
|
||||
341,2.706
|
||||
342,3.405
|
||||
343,2.849
|
||||
344,2.678
|
||||
345,2.796
|
||||
346,2.782
|
||||
347,2.636
|
||||
348,3.475
|
||||
349,2.798
|
||||
350,2.745
|
||||
351,2.564
|
||||
352,2.792
|
||||
353,2.714
|
||||
354,3.496
|
||||
355,2.841
|
||||
356,2.465
|
||||
357,2.708
|
||||
358,2.692
|
||||
359,2.934
|
||||
360,3.304
|
||||
361,2.561
|
||||
362,2.689
|
||||
363,2.749
|
||||
364,2.780
|
||||
365,2.940
|
||||
366,3.025
|
||||
367,2.706
|
||||
368,2.881
|
||||
369,2.619
|
||||
370,2.716
|
||||
371,3.133
|
||||
372,2.874
|
||||
373,2.816
|
||||
374,2.414
|
||||
375,2.737
|
||||
376,2.797
|
||||
377,2.651
|
||||
378,2.920
|
||||
379,3.032
|
||||
380,2.606
|
||||
381,2.710
|
||||
382,2.649
|
||||
383,2.700
|
||||
384,3.469
|
||||
385,2.224
|
||||
386,2.567
|
||||
387,2.733
|
||||
388,2.511
|
||||
389,2.950
|
||||
390,3.979
|
||||
391,2.798
|
||||
392,2.487
|
||||
393,2.740
|
||||
394,2.741
|
||||
395,2.718
|
||||
396,3.291
|
||||
397,2.769
|
||||
398,2.697
|
||||
399,2.796
|
||||
400,2.641
|
||||
401,2.652
|
||||
402,3.151
|
||||
403,2.673
|
||||
404,2.741
|
||||
405,2.606
|
||||
406,2.665
|
||||
407,2.859
|
||||
408,2.739
|
||||
409,2.586
|
||||
410,2.910
|
||||
411,2.745
|
||||
412,2.698
|
||||
413,2.670
|
||||
414,2.797
|
||||
415,2.829
|
||||
416,2.643
|
||||
417,2.776
|
||||
418,2.772
|
||||
419,2.530
|
||||
420,2.567
|
||||
421,2.826
|
||||
422,2.782
|
||||
423,2.738
|
||||
424,2.640
|
||||
425,2.967
|
||||
426,2.600
|
||||
427,3.020
|
||||
428,2.502
|
||||
429,2.642
|
||||
430,2.625
|
||||
431,2.770
|
||||
432,2.659
|
||||
433,2.717
|
||||
434,2.698
|
||||
435,2.780
|
||||
436,2.992
|
||||
437,2.686
|
||||
438,2.560
|
||||
439,3.048
|
||||
440,2.721
|
||||
441,2.507
|
||||
442,2.694
|
||||
443,2.695
|
||||
444,2.892
|
||||
445,3.443
|
||||
446,2.633
|
||||
447,2.908
|
||||
448,2.823
|
||||
449,2.346
|
||||
450,2.599
|
||||
451,2.914
|
||||
452,2.774
|
||||
453,3.391
|
||||
454,2.384
|
||||
455,2.585
|
||||
456,2.667
|
||||
457,2.704
|
||||
458,2.674
|
||||
459,2.755
|
||||
460,2.718
|
||||
461,2.804
|
||||
462,2.583
|
||||
463,2.694
|
||||
464,2.797
|
||||
465,3.079
|
||||
466,2.434
|
||||
467,2.723
|
||||
468,3.671
|
||||
469,2.211
|
||||
470,2.567
|
||||
471,2.701
|
||||
472,2.717
|
||||
473,2.657
|
||||
474,2.581
|
||||
475,2.889
|
||||
476,3.006
|
||||
477,2.595
|
||||
478,2.703
|
||||
479,2.498
|
||||
480,2.694
|
||||
481,2.915
|
||||
482,2.655
|
||||
483,2.686
|
||||
484,2.833
|
||||
485,2.717
|
||||
486,2.723
|
||||
487,2.451
|
||||
488,2.913
|
||||
489,2.784
|
||||
490,2.684
|
||||
491,2.651
|
||||
492,2.809
|
||||
493,2.819
|
||||
494,2.676
|
||||
495,2.851
|
||||
496,2.775
|
||||
497,2.638
|
||||
498,2.520
|
||||
499,2.824
|
||||
500,2.930
|
||||
501,2.757
|
||||
502,2.608
|
||||
503,2.808
|
||||
504,2.508
|
||||
505,2.819
|
||||
506,2.654
|
||||
507,2.748
|
||||
508,2.705
|
||||
509,2.798
|
||||
510,2.559
|
||||
511,2.694
|
||||
512,2.821
|
||||
513,2.706
|
||||
514,2.612
|
||||
515,2.928
|
||||
516,2.475
|
||||
517,2.830
|
||||
518,2.658
|
||||
519,2.784
|
||||
520,2.572
|
||||
521,2.935
|
||||
522,2.688
|
||||
523,2.600
|
||||
524,2.675
|
||||
525,2.828
|
||||
526,2.774
|
||||
527,2.848
|
||||
528,2.764
|
||||
529,2.379
|
||||
530,2.962
|
||||
531,2.989
|
||||
532,2.528
|
||||
533,2.639
|
||||
534,2.906
|
||||
535,2.543
|
||||
536,2.582
|
||||
537,2.949
|
||||
538,2.813
|
||||
539,2.504
|
||||
540,2.754
|
||||
541,2.638
|
||||
542,2.695
|
||||
543,2.662
|
||||
544,2.922
|
||||
545,2.616
|
||||
546,2.810
|
||||
547,2.675
|
||||
548,2.551
|
||||
549,2.894
|
||||
550,2.754
|
||||
551,2.632
|
||||
552,2.748
|
||||
553,2.827
|
||||
554,2.751
|
||||
555,2.759
|
||||
556,2.684
|
||||
557,2.766
|
||||
558,2.948
|
||||
559,2.461
|
||||
560,2.621
|
||||
561,2.772
|
||||
562,2.985
|
||||
563,2.582
|
||||
564,2.897
|
||||
565,2.574
|
||||
566,2.661
|
||||
567,2.742
|
||||
568,2.797
|
||||
569,2.632
|
||||
570,2.755
|
||||
571,2.878
|
||||
572,2.759
|
||||
573,3.812
|
||||
574,2.651
|
||||
575,2.862
|
||||
576,2.577
|
||||
577,2.634
|
||||
578,3.173
|
||||
579,2.418
|
||||
580,2.714
|
||||
581,2.746
|
||||
582,2.794
|
||||
583,2.774
|
||||
584,2.583
|
||||
585,3.659
|
||||
586,3.222
|
||||
587,2.489
|
||||
588,2.868
|
||||
589,2.558
|
||||
590,2.586
|
||||
591,2.599
|
||||
592,2.930
|
||||
593,2.401
|
||||
594,2.540
|
||||
595,2.889
|
||||
596,2.733
|
||||
597,2.664
|
||||
598,2.877
|
||||
599,2.588
|
||||
600,2.677
|
||||
601,2.897
|
||||
602,2.672
|
||||
603,2.780
|
||||
604,2.740
|
||||
605,2.780
|
||||
606,2.493
|
||||
607,2.794
|
||||
608,2.631
|
||||
609,2.768
|
||||
610,2.870
|
||||
611,2.626
|
||||
612,2.862
|
||||
613,2.602
|
||||
614,2.868
|
||||
615,2.627
|
||||
616,2.638
|
||||
617,2.702
|
||||
618,2.904
|
||||
619,2.918
|
||||
620,2.487
|
||||
621,3.225
|
||||
622,2.350
|
||||
623,2.820
|
||||
624,2.837
|
||||
625,2.408
|
||||
626,2.593
|
||||
627,3.526
|
||||
628,2.554
|
||||
629,2.746
|
||||
630,2.684
|
||||
631,2.729
|
||||
632,2.762
|
||||
633,2.768
|
||||
634,3.034
|
||||
635,2.514
|
||||
636,2.732
|
||||
637,2.739
|
||||
638,2.692
|
||||
639,2.832
|
||||
640,2.810
|
||||
641,2.484
|
||||
642,2.632
|
||||
643,3.284
|
||||
644,2.311
|
||||
645,3.079
|
||||
646,2.656
|
||||
647,2.683
|
||||
648,2.622
|
||||
649,2.668
|
||||
650,2.779
|
||||
651,2.796
|
||||
652,2.874
|
||||
653,2.427
|
||||
654,2.644
|
||||
655,2.872
|
||||
656,2.867
|
||||
657,2.404
|
||||
658,2.917
|
||||
659,2.773
|
||||
660,2.857
|
||||
661,2.789
|
||||
662,2.443
|
||||
663,2.651
|
||||
664,2.839
|
||||
665,2.814
|
||||
666,2.575
|
||||
667,2.796
|
||||
668,2.709
|
||||
669,2.873
|
||||
670,2.670
|
||||
671,2.936
|
||||
672,2.496
|
||||
673,2.608
|
||||
674,2.641
|
||||
675,2.793
|
||||
676,2.765
|
||||
677,2.736
|
||||
678,2.679
|
||||
679,2.987
|
||||
680,2.884
|
||||
681,2.469
|
||||
682,2.774
|
||||
683,2.948
|
||||
684,2.598
|
||||
685,3.478
|
||||
686,2.856
|
||||
687,4.623
|
||||
688,3.038
|
||||
689,2.507
|
||||
690,2.717
|
||||
691,2.682
|
||||
692,2.807
|
||||
693,2.778
|
||||
694,2.699
|
||||
695,2.802
|
||||
696,2.605
|
||||
697,2.650
|
||||
698,2.789
|
||||
699,2.791
|
||||
700,3.015
|
||||
701,2.539
|
||||
702,2.790
|
||||
703,2.698
|
||||
704,2.637
|
||||
705,2.657
|
||||
706,2.800
|
||||
707,2.720
|
||||
708,2.702
|
||||
709,2.845
|
||||
710,2.583
|
||||
711,2.780
|
||||
712,2.782
|
||||
713,2.593
|
||||
714,2.695
|
||||
715,2.747
|
||||
716,2.871
|
||||
717,2.671
|
||||
718,2.676
|
||||
719,2.991
|
||||
720,2.727
|
||||
721,2.576
|
||||
722,2.539
|
||||
723,2.770
|
||||
724,2.712
|
||||
725,2.818
|
||||
726,2.615
|
||||
|
+29
-11
@@ -13,7 +13,19 @@ Record results in the table at the end of this file, with the date, the exact bu
|
||||
3. **Licensing.** Every new asset has provenance in `THIRD_PARTY_NOTICES.md`, or is recorded as unresolved and kept out of the build.
|
||||
4. **Version discipline.** Patch releases contain fixes, optimisation, docs, or tuning only — no new scope.
|
||||
|
||||
## Performance check
|
||||
## `0.1.0` checks
|
||||
|
||||
1. **Realism first.** Building placement, street width, sidewalk depth, material read and site identity match the reference photography. Compare side by side and note every deliberate deviation. "Looks right" is not evidence.
|
||||
2. Player traverses both the south-side frontage and the north-side frontage of the demo without falling through geometry, including the designated crossing. Walk the route twice: once normally, once with collision debug visualisation enabled.
|
||||
3. No blocking collision, no invisible walls, no gaps at sidewalk/road seams.
|
||||
4. Fresh checkout builds against the documented external engine installation.
|
||||
5. Linux build packaged and launched; controls documented in `Docs/` and shown in-game.
|
||||
6. **Performance last.** 60 FPS at 1440p per the performance check below — a floor every release meets, not the goal that shapes the content.
|
||||
7. Tag `v0.1.0`, write the `CHANGELOG.md` entry, then remove the completed section from `TODO.md` (its goal stays in `ROADMAP.md`).
|
||||
|
||||
## Performance check (last)
|
||||
|
||||
Performance is the **last** check on purpose: realism leads the design and the frame budget only decides how much of it fits. It is still a floor — a release is not tagged below 60 FPS at 1440p without the owner's explicit approval.
|
||||
|
||||
Measured at 1440p on the development workstation: Bazzite, Ryzen 7 7700X, 64 GB RAM, RTX 5070 Ti 16 GB.
|
||||
|
||||
@@ -22,16 +34,6 @@ Measured at 1440p on the development workstation: Bazzite, Ryzen 7 7700X, 64 GB
|
||||
- Target: 60 FPS at 1440p without frame generation.
|
||||
- Capture a frame-time graph over a fixed traversal route, not a single sample. State the route.
|
||||
|
||||
## `0.1.0` checks
|
||||
|
||||
1. Player traverses both the south-side frontage and the north-side frontage of the demo without falling through geometry, including the designated crossing. Walk the route twice: once normally, once with collision debug visualisation enabled.
|
||||
2. Building placement, street width, sidewalk depth, and site identity match the reference photography. Compare side by side and note every deliberate deviation. "Looks right" is not evidence.
|
||||
3. No blocking collision, no invisible walls, no gaps at sidewalk/road seams.
|
||||
4. 60 FPS at 1440p per the performance check above.
|
||||
5. Fresh checkout builds against the documented external engine installation.
|
||||
6. Linux build packaged and launched; controls documented in `Docs/` and shown in-game.
|
||||
7. Tag `v0.1.0`, write the `CHANGELOG.md` entry, then remove the completed section from `TODO.md` (its goal stays in `ROADMAP.md`).
|
||||
|
||||
## Check-record template
|
||||
|
||||
| Version | Check | Result | Evidence | Date |
|
||||
@@ -125,6 +127,22 @@ Run on the packaged build (`GigLineQA`, Development, Vulkan SM5) with `-qaselfte
|
||||
|
||||
Committed frame-time series: `Docs/perf/0.1.0-walk-frametimes.csv`.
|
||||
|
||||
### `0.1.5` re-measure with the façade kit in the map
|
||||
|
||||
Same route, same preset, packaged Development build, **2560×1440**:
|
||||
|
||||
| Metric | `0.1.0` | `0.1.5` (kit + textures) |
|
||||
| ------- | ----------------: | -----------------------: |
|
||||
| Average | 2.42 ms (414 fps) | **2.76 ms (363 fps)** |
|
||||
| p50 | 2.38 ms | 2.73 ms |
|
||||
| p95 | 2.71 ms | 3.17 ms |
|
||||
| p99 | 3.01 ms | 3.64 ms |
|
||||
| Max | 4.00 ms | 4.62 ms |
|
||||
|
||||
The **625 kit actors and 8 generated textures cost about 12 %** of frame time and still leave **363 fps average, p99 3.64 ms** — roughly six times the 60 fps floor. Frame-time series: `Docs/perf/0.1.5-kit-walk-frametimes.csv`.
|
||||
|
||||
**Measurement trap, again:** runs that reported ~105 fps were two game instances overlapping (a previous launch had not exited). Confirm a single instance (`pgrep -af Binaries/Linux/GigLine`) before trusting a figure — 363 fps reproduced twice on a clean run.
|
||||
|
||||
## `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:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,9 @@
|
||||
#include "GigLineCharacter.h"
|
||||
|
||||
#include "Animation/AnimSequence.h"
|
||||
#include "Camera/CameraComponent.h"
|
||||
#include "Components/SkeletalMeshComponent.h"
|
||||
#include "GameFramework/SpringArmComponent.h"
|
||||
#include "Components/CapsuleComponent.h"
|
||||
#include "EnhancedInputComponent.h"
|
||||
#include "EnhancedInputSubsystems.h"
|
||||
@@ -28,7 +31,7 @@ static TAutoConsoleVariable<int32> CVarGigLineSetupLogs(
|
||||
|
||||
AGigLineCharacter::AGigLineCharacter()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
|
||||
// 1.75 m tall, 0.42 m wide, human scale in Unreal units (1 m = 100 uu).
|
||||
GetCapsuleComponent()->InitCapsuleSize(42.0f, 87.5f);
|
||||
@@ -51,9 +54,59 @@ AGigLineCharacter::AGigLineCharacter()
|
||||
FirstPersonCamera->SetRelativeLocation(FVector(0.0f, 0.0f, 72.5f)); // eye height 1.60 m
|
||||
FirstPersonCamera->bUsePawnControlRotation = true;
|
||||
|
||||
// An eye-level mesh is not needed for 0.1.0; the body is a capsule with a camera.
|
||||
GetMesh()->SetHiddenInGame(true);
|
||||
// Visible body (0.2.0). The animated import carries its own skeleton, so the
|
||||
// clips resolve without a retarget step; it is hidden while the first-person
|
||||
// camera is active and shown in third person.
|
||||
static ConstructorHelpers::FObjectFinder<USkeletalMesh> BodyFinder(
|
||||
TEXT("/Game/Test/Character/Character_Anim.Character_Anim"));
|
||||
if (BodyFinder.Succeeded())
|
||||
{
|
||||
GetMesh()->SetSkeletalMesh(BodyFinder.Object);
|
||||
}
|
||||
// The animated import carries no material of its own; without this the body is
|
||||
// grey clay. The warm skin material was created by the character import.
|
||||
static ConstructorHelpers::FObjectFinder<UMaterialInterface> SkinFinder(
|
||||
TEXT("/Game/Test/Character/M_Character_Export.M_Character_Export"));
|
||||
if (SkinFinder.Succeeded())
|
||||
{
|
||||
GetMesh()->SetMaterial(0, SkinFinder.Object);
|
||||
}
|
||||
static ConstructorHelpers::FObjectFinder<UAnimSequence> IdleFinder(
|
||||
TEXT("/Game/Test/Character/Character_AnimHuman_rig_GigLine_Idle.Character_AnimHuman_rig_GigLine_Idle"));
|
||||
if (IdleFinder.Succeeded())
|
||||
{
|
||||
IdleAnimation = IdleFinder.Object;
|
||||
}
|
||||
static ConstructorHelpers::FObjectFinder<UAnimSequence> WalkFinder(
|
||||
TEXT("/Game/Test/Character/Character_AnimHuman_rig_GigLine_Walk.Character_AnimHuman_rig_GigLine_Walk"));
|
||||
if (WalkFinder.Succeeded())
|
||||
{
|
||||
WalkAnimation = WalkFinder.Object;
|
||||
}
|
||||
GetMesh()->SetRelativeLocation(FVector(0.0f, 0.0f, -87.5f));
|
||||
GetMesh()->SetRelativeRotation(FRotator(0.0f, -90.0f, 0.0f));
|
||||
GetMesh()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||
GetMesh()->SetHiddenInGame(true);
|
||||
|
||||
// Third-person boom. bDoCollisionTest is the camera-collision feature: the arm
|
||||
// shortens when something is between the pivot and the desired camera position.
|
||||
ThirdPersonArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("ThirdPersonArm"));
|
||||
ThirdPersonArm->SetupAttachment(GetCapsuleComponent());
|
||||
ThirdPersonArm->SetRelativeLocation(FVector(0.0f, 0.0f, 60.0f));
|
||||
ThirdPersonArm->TargetArmLength = ThirdPersonBoomLength;
|
||||
ThirdPersonArm->bUsePawnControlRotation = true;
|
||||
ThirdPersonArm->bDoCollisionTest = true;
|
||||
ThirdPersonArm->ProbeSize = 12.0f;
|
||||
ThirdPersonArm->ProbeChannel = ECollisionChannel::ECC_Camera;
|
||||
ThirdPersonArm->bEnableCameraLag = true;
|
||||
ThirdPersonArm->CameraLagSpeed = 8.0f;
|
||||
ThirdPersonArm->SetAutoActivate(false);
|
||||
|
||||
ThirdPersonCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("ThirdPersonCamera"));
|
||||
ThirdPersonCamera->SetupAttachment(ThirdPersonArm, USpringArmComponent::SocketName);
|
||||
ThirdPersonCamera->bUsePawnControlRotation = false;
|
||||
ThirdPersonCamera->SetAutoActivate(false);
|
||||
ThirdPersonCamera->SetFieldOfView(90.0f);
|
||||
|
||||
// Hard-reference the Enhanced Input assets with ConstructorHelpers so the
|
||||
// cooker always includes them and they load during CDO construction. Runtime
|
||||
@@ -78,6 +131,8 @@ AGigLineCharacter::AGigLineCharacter()
|
||||
TEXT("/Game/Input/IA_Jump.IA_Jump"));
|
||||
static ConstructorHelpers::FObjectFinder<UInputAction> ResetF(
|
||||
TEXT("/Game/Input/IA_Reset.IA_Reset"));
|
||||
static ConstructorHelpers::FObjectFinder<UInputAction> CamF(
|
||||
TEXT("/Game/Input/IA_ToggleCamera.IA_ToggleCamera"));
|
||||
static ConstructorHelpers::FObjectFinder<UInputAction> PauseF(
|
||||
TEXT("/Game/Input/IA_Pause.IA_Pause"));
|
||||
|
||||
@@ -89,6 +144,7 @@ AGigLineCharacter::AGigLineCharacter()
|
||||
LookAction = LookF.Object;
|
||||
JumpAction = JumpF.Object;
|
||||
ResetAction = ResetF.Object;
|
||||
ToggleCameraAction = CamF.Object;
|
||||
PauseAction = PauseF.Object;
|
||||
|
||||
if (!MappingContext || !MoveAction || !MoveBackAction || !StrafeAction || !StrafeLeftAction
|
||||
@@ -138,6 +194,23 @@ void AGigLineCharacter::BeginPlay()
|
||||
StartLocation = GetActorLocation();
|
||||
StartRotation = GetActorRotation();
|
||||
|
||||
// `-thirdperson` starts the pawn in the third-person rig. The first-person view
|
||||
// stays the default (owner decision for 0.1.0), but the screenshot run needs the
|
||||
// body visible without a human pressing V, so the review is reproducible.
|
||||
if (CVarGigLineSetupLogs.GetValueOnGameThread())
|
||||
{
|
||||
UE_LOG(LogTemp, Log,
|
||||
TEXT("GigLine: BeginPlay thirdperson=%d body=%s idle=%s walk=%s"),
|
||||
FParse::Param(FCommandLine::Get(), TEXT("thirdperson")) ? 1 : 0,
|
||||
GetMesh()->GetSkeletalMeshAsset() ? TEXT("ok") : TEXT("nil"),
|
||||
IdleAnimation ? TEXT("ok") : TEXT("nil"),
|
||||
WalkAnimation ? TEXT("ok") : TEXT("nil"));
|
||||
}
|
||||
if (FParse::Param(FCommandLine::Get(), TEXT("thirdperson")))
|
||||
{
|
||||
ToggleCamera();
|
||||
}
|
||||
|
||||
// The PlayerStart sits above grade so the capsule cannot spawn embedded, so
|
||||
// the BeginPlay position is mid-air. Re-capture once the pawn has landed,
|
||||
// otherwise Reset drops the player 1.4 m back into the air every press.
|
||||
@@ -557,6 +630,10 @@ void AGigLineCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputCo
|
||||
{
|
||||
Input->BindAction(PauseAction, ETriggerEvent::Started, this, &AGigLineCharacter::TogglePause);
|
||||
}
|
||||
if (ToggleCameraAction)
|
||||
{
|
||||
Input->BindAction(ToggleCameraAction, ETriggerEvent::Started, this, &AGigLineCharacter::ToggleCamera);
|
||||
}
|
||||
if (JumpAction)
|
||||
{
|
||||
Input->BindAction(JumpAction, ETriggerEvent::Started, this, &AGigLineCharacter::StartJump);
|
||||
@@ -644,6 +721,68 @@ void AGigLineCharacter::Look(const FInputActionValue& Value)
|
||||
AddControllerPitchInput(Axis.Y * LookSensitivity * (bInvertLookY ? -1.0f : 1.0f));
|
||||
}
|
||||
|
||||
void AGigLineCharacter::Tick(float DeltaSeconds)
|
||||
{
|
||||
Super::Tick(DeltaSeconds);
|
||||
// Speed-driven animation state: correct whether the player walked, was pushed,
|
||||
// or fell, and independent of which input produced the movement.
|
||||
UpdateBodyAnimation();
|
||||
}
|
||||
|
||||
void AGigLineCharacter::ToggleCamera()
|
||||
{
|
||||
// Both rigs exist from construction; the toggle only swaps which one is active
|
||||
// and whether the body is drawn. Deactivating the other camera matters: with
|
||||
// two active cameras the view is whichever was registered last.
|
||||
bThirdPerson = !bThirdPerson;
|
||||
FirstPersonCamera->SetActive(!bThirdPerson);
|
||||
ThirdPersonCamera->SetActive(bThirdPerson);
|
||||
GetMesh()->SetHiddenInGame(!bThirdPerson);
|
||||
if (bThirdPerson)
|
||||
{
|
||||
UpdateBodyAnimation();
|
||||
}
|
||||
if (CVarGigLineSetupLogs.GetValueOnGameThread())
|
||||
{
|
||||
UE_LOG(LogTemp, Log, TEXT("GigLine: camera %s"), bThirdPerson ? TEXT("third person") : TEXT("first person"));
|
||||
}
|
||||
}
|
||||
|
||||
void AGigLineCharacter::UpdateBodyAnimation()
|
||||
{
|
||||
if (!bThirdPerson || !GetMesh())
|
||||
{
|
||||
return;
|
||||
}
|
||||
const bool bMoving = GetVelocity().Size2D() > WalkAnimationThreshold;
|
||||
UAnimSequence* Wanted = bMoving ? WalkAnimation.Get() : IdleAnimation.Get();
|
||||
if (!Wanted)
|
||||
{
|
||||
// Logged once: a missing clip presents as an A-posed body, which looks like
|
||||
// an animation problem rather than a missing asset reference.
|
||||
static bool bWarnedMissingClip = false;
|
||||
if (!bWarnedMissingClip)
|
||||
{
|
||||
bWarnedMissingClip = true;
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("GigLine: body clip missing (idle=%s walk=%s) - body stays in bind pose"),
|
||||
IdleAnimation ? TEXT("ok") : TEXT("nil"),
|
||||
WalkAnimation ? TEXT("ok") : TEXT("nil"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (Wanted == PlayingAnimation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
PlayingAnimation = Wanted;
|
||||
if (CVarGigLineSetupLogs.GetValueOnGameThread())
|
||||
{
|
||||
UE_LOG(LogTemp, Log, TEXT("GigLine: playing body clip %s"), *Wanted->GetName());
|
||||
}
|
||||
GetMesh()->PlayAnimation(Wanted, true);
|
||||
}
|
||||
|
||||
void AGigLineCharacter::ResetToStart()
|
||||
{
|
||||
// Reset is a level-restart in miniature: put the body back where it spawned,
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
#include "InputActionValue.h"
|
||||
#include "GigLineCharacter.generated.h"
|
||||
|
||||
class UAnimSequence;
|
||||
class UCameraComponent;
|
||||
class UInputAction;
|
||||
class UInputMappingContext;
|
||||
class USpringArmComponent;
|
||||
|
||||
|
||||
/**
|
||||
@@ -29,6 +31,7 @@ public:
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
|
||||
virtual void Tick(float DeltaSeconds) override;
|
||||
|
||||
protected:
|
||||
/** Loads the Enhanced Input assets from /Game/Input; called from BeginPlay. */
|
||||
@@ -38,6 +41,23 @@ protected:
|
||||
void ResetToStart();
|
||||
void TogglePause();
|
||||
|
||||
/** Swaps between the first-person and third-person rigs (V). */
|
||||
void ToggleCamera();
|
||||
|
||||
/**
|
||||
* Plays the idle or walk clip on the visible body.
|
||||
*
|
||||
* Called from the movement tick rather than from input: an animation state
|
||||
* driven by speed is correct whether the player walked, was pushed, or fell.
|
||||
* Only switches when the clip changes, because PlayAnimation restarts a clip
|
||||
* and calling it every frame would freeze the cycle at its first key.
|
||||
*/
|
||||
void UpdateBodyAnimation();
|
||||
|
||||
/** Velocity above which the walk clip plays, in cm/s (0.1 m/s tolerance). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation")
|
||||
float WalkAnimationThreshold = 10.0f;
|
||||
|
||||
/**
|
||||
* TEMP QA: scripted walk / reset / pause verification.
|
||||
*
|
||||
@@ -109,6 +129,39 @@ protected:
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
|
||||
TObjectPtr<UCameraComponent> FirstPersonCamera;
|
||||
|
||||
/**
|
||||
* Third-person boom. Collision testing is on, which is what stops the camera
|
||||
* from ending up inside a wall: the arm shortens until the camera is clear
|
||||
* instead of clipping through geometry.
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
|
||||
TObjectPtr<USpringArmComponent> ThirdPersonArm;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
|
||||
TObjectPtr<UCameraComponent> ThirdPersonCamera;
|
||||
|
||||
/** Boom length in cm: far enough to see the body, close enough to read the street. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera")
|
||||
float ThirdPersonBoomLength = 340.0f;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UAnimSequence> IdleAnimation;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UAnimSequence> WalkAnimation;
|
||||
|
||||
/** True while the third-person rig is the active view. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Camera")
|
||||
bool bThirdPerson = false;
|
||||
|
||||
/** Clip currently playing on the body, so UpdateBodyAnimation can diff it. */
|
||||
UPROPERTY()
|
||||
TObjectPtr<UAnimSequence> PlayingAnimation;
|
||||
|
||||
/** Toggle-camera action (V). */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
|
||||
TObjectPtr<UInputAction> ToggleCameraAction;
|
||||
|
||||
/** Degrees per second per unit of mouse input; defaults to Unreal's usual 45. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera")
|
||||
float LookSensitivity = 45.0f;
|
||||
|
||||
@@ -420,3 +420,106 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO.
|
||||
- Owner reviewed the four demo captures locally and approved the set. Signoff recorded in `Docs/reviews/0.1.4-review.md` (7/7 items) and in the per-shot `Approved` column of `Screenshots/README.md`.
|
||||
- The three geometry items that `0.1.4` did not finish — retail neighbours from their own references, hero fluting/motto band, blockout cleaning — move to a new **`0.1.5` Façade finishing** patch rather than growing this one, plus a much-needed **1440p performance re-measure** (625 kit actors have never been measured; `0.1.0` measured 414 fps).
|
||||
- Decision recorded because it sets a precedent: finish and sign off a version when its _stated_ scope is done, and carry leftover scope into the next patch — do not let a patch accumulate unmeasured geometry.
|
||||
|
||||
## 2026-11-09 — quality priority flipped: realism first, performance last
|
||||
|
||||
- Owner decision: realism leads; performance closes the quality list. Applied in
|
||||
`Docs/Architecture/01_introduction_and_goals.md`, `10_quality_requirements.md`,
|
||||
`04_solution_strategy.md`, the combined `architecture.md`, `Docs/release-checks.md`
|
||||
(performance section now last, with the reason), `ROADMAP.md` (a "Quality priority"
|
||||
section) and `AGENTS.md` (the tie-break rule next to the scope-cutting rule).
|
||||
- Ordering used: 1 visual realism, 2 geographic fidelity, 3 walkability/usability,
|
||||
4 build reproducibility/portability, 5 provenance, 6 performance.
|
||||
- **Both revisions carry the same guard so the order cannot be used as an excuse:**
|
||||
performance is a *floor*, not a preference — 60 FPS at 1440p without frame
|
||||
generation must be met at every release, and if realism cannot fit, the realism
|
||||
scope is cut deliberately and recorded in `CHANGELOG.md`, never the frame rate.
|
||||
Walkability outranks realism in one direction: a block the player falls through is
|
||||
not a demo.
|
||||
|
||||
## 2026-11-09 — 0.1.5 start: retail reference + kit layout maths
|
||||
|
||||
- Commons holds exactly **one** usable reference for the retail run:
|
||||
`Art/reference/retail-neighbours/01-lee-drug-and-neighbor.jpg` (Gb321, CC BY-SA
|
||||
4.0, 1920×953) — 6800 Lee Drug **and** its neighbour at street level, shopfronts
|
||||
and parapets visible (vision pre-check: yes). Nothing for 6822 or 6806–6820, so
|
||||
those two façades still have no reference and stay blocked.
|
||||
- **Kit layout was wrong for tall/wide buildings**, not just ugly: bays were
|
||||
`round(width / 496)`, so 6922 (65.8 m wide, 37.1 m tall) got **13 bays** — a
|
||||
window every 4.96 m across the whole frontage — and the storey count came out at
|
||||
11 × 310 cm = 34.1 m against a 35.7 m window zone, leaving bare wall under the
|
||||
cornice. Fixed: `MAX_BAYS = 9` (module widens instead of subdividing), and the
|
||||
storey spread is derived from the available height (`usable / storeys`) so the top
|
||||
band always lands inside the building. Result: 6922 9 × 11, theatre 8 × 6,
|
||||
6806 7 × 3, 6800 4 × 3, TCL 9 × 5.
|
||||
|
||||
## 2026-11-09 — hero fluting + motto band; and a silent placement divergence
|
||||
|
||||
- Fluting: 20 grooves per column as thin shadow-tone cylinders set into the shaft
|
||||
(120 pieces) rather than boolean cuts — the eye reads them in silhouette and
|
||||
grazing light, booleans would cost far more for that. Motto band is now a real
|
||||
recess: surround 6 cm proud, panel 12 cm behind (was an 8 cm dark stripe).
|
||||
Hero 148 parts/3 044 tris -> 271/5 492, still 14.50 m.
|
||||
- **The real find:** `ue_hero_facade_import.py` had its own hero position
|
||||
`(-12024.5, -1450, 0)` — the pre-`0.1.0` value, before the frontage placement fix
|
||||
moved the hero to `(-13336, +1307)`. Running the importer on its own therefore
|
||||
teleported the hero **13 m east and onto the north side of the boulevard**, and
|
||||
nothing complained: a displaced building still renders. The only symptom was the
|
||||
reference score dropping 0.69 -> 0.56 with the 3x3 composition correlation
|
||||
flipping +0.54 -> -0.08.
|
||||
- Fix: `Tools/ue_placement.py` is now the single source of truth (hero position,
|
||||
verified frontage spans, `span_error`/`describe_span_failure`). Both
|
||||
`ue_hero_facade_import.py` and `ue_build_demo_map.py` import it, the import asserts
|
||||
the staged span and logs `PLACEMENT ERROR` when it does not match, and tests assert
|
||||
the stale literal cannot come back. Spawn-view score back to **0.72** (best yet).
|
||||
- Lesson to keep: **duplicated placement constants drift silently.** The metric that
|
||||
caught it was the composition correlation, not a visual review.
|
||||
|
||||
## 2026-11-09 — 1440p re-measured with the kit: 363 fps, ~12% cost
|
||||
|
||||
- 2560×1440, Epic preset, TSR, VSync off, no frame generation: **2.76 ms average
|
||||
(363 fps)**, p50 2.73, p95 3.17, p99 3.64, max 4.62 — against `0.1.0`'s 2.42 ms
|
||||
(414 fps). The 625 kit actors + 8 generated textures cost about **12 %** and leave
|
||||
~6× the 60 fps floor.
|
||||
- **Variance trap confirmed a third time:** runs reporting ~105 fps (9.5 ms) were
|
||||
two overlapping game instances — an earlier launch had not exited. `pgrep -af
|
||||
Binaries/Linux/GigLine` should show one wrapper + one binary; 363 fps reproduced
|
||||
twice on a clean run. Always count instances before believing a perf number.
|
||||
- Series committed at `Docs/perf/0.1.5-kit-walk-frametimes.csv`; comparison table in
|
||||
`Docs/release-checks.md`.
|
||||
|
||||
## 2026-11-09 — 604 unmaterialed actors: material rebuild was delete+recreate
|
||||
|
||||
- The "blockout cleaning pass" turned out to need **material and placement hygiene,
|
||||
not more geometry**. `Tools/ue_audit_scene.py` (new) found **604 of 620 mesh actors
|
||||
with no material**: kit pieces and theatre trim all untextured.
|
||||
- **Cause:** `ensure_material` in `ue_build_demo_map.py` deleted and recreated each
|
||||
`/Game/Hollywood/Site/M_*` asset every run ("so a look change cannot be masked by a
|
||||
stale asset"). That orphane every reference: actors staged by other tools pointed at
|
||||
deleted assets and fell back to the default material. The map looked only *slightly*
|
||||
flat — no error, no warning.
|
||||
- **Fix:** update the material **in place** — load it, `MaterialEditingLibrary.
|
||||
delete_all_material_expressions(mat)`, rebuild the graph, recompile, save. Note
|
||||
`Material.expressions` is protected from Python; use the library's deleter.
|
||||
- **Ordering rule now documented and enforced:** the demo builder rebuilds materials,
|
||||
so kit and theatre staging must run *after* it. `render_screenshots.sh` now runs
|
||||
demo map → kit assembly → theatre trim → scene audit → shot cameras → capture, and
|
||||
**fails on a non-zero audit count**.
|
||||
- Second find: 53 duplicated kit labels (both shop piers of a bay shared one name).
|
||||
- Scene audit after the fixes: **0 failures** (all materialed, all spans verified, no
|
||||
duplicate labels). Packaged QA still PASS; spawn-view reference score 0.72.
|
||||
|
||||
## 2026-11-09 — v0.1.5 released
|
||||
|
||||
- All actionable `0.1.5` work is done: hero fluting + recessed motto band, kit layout
|
||||
for wide/tall buildings, placement single-source-of-truth, 1440p re-measure
|
||||
(363 fps), and the material/placement hygiene that the "blockout cleaning" task
|
||||
actually needed (604 unmaterialed actors, 53 duplicate labels) with a new scene
|
||||
audit gate.
|
||||
- Only item carried forward is the **retail frontage (6822 / 6806–6820 / 6800)**,
|
||||
blocked because Commons holds no usable reference for two of the three buildings
|
||||
and substituting the theatre's photographs would misattribute the façades. It now
|
||||
lives in a `0.1.6` Retail frontage patch.
|
||||
- Precedent applied again: tag when the stated scope is done, carry blocked scope
|
||||
into the next patch rather than letting a release wait indefinitely on missing
|
||||
reference photography.
|
||||
+5
-1
@@ -2,9 +2,13 @@
|
||||
|
||||
Development roadmap for this project: milestone goals, scope, decisions, and release gates. Actionable tasks, claims, and progress marks live in [TODO.md](TODO.md).
|
||||
|
||||
## Quality priority
|
||||
|
||||
**Realism leads; performance is a floor.** Façade identity, proportion, material and light are judged against the reference photographs and drive design decisions. Performance closes the quality list: 60 FPS at 1440p without frame generation is a budget every release must meet, and if realism cannot fit inside it the realism scope is cut deliberately and recorded — never the frame rate.
|
||||
|
||||
## Status at a glance — 2026-11-08 15:30
|
||||
|
||||
Workstation, geo pipeline, and engine validation are done. The art pipeline has a first blockout, terrain, and street in metres, in one shared coordinate frame. **Project bootstrap is complete**: `Game/GigLine` builds, packages, and both content-acceptance tests (static-mesh scale, MPFB skeletal character) pass. **`v0.1.0` released 2026-11-08**: the walkable demo, hero façade, verified block frontage, first-person controls, ODbL attribution, and every release check pass (traversal, placement, collision, 60 fps at 1440p, fresh-checkout build, packaged build with controls). Next: the `0.1.1`–`0.1.4` patch series (collision detail, scale/façade corrections, packaging fixes, real building façades), then `0.2.0` (player body, third-person camera, animation).
|
||||
Workstation, geo pipeline, and engine validation are done. The art pipeline has a first blockout, terrain, and street in metres, in one shared coordinate frame. **Project bootstrap is complete**: `Game/GigLine` builds, packages, and both content-acceptance tests (static-mesh scale, MPFB skeletal character) pass. **`v0.1.0` released 2026-11-08**: the walkable demo, hero façade, verified block frontage, first-person controls, ODbL attribution, and every release check pass (traversal, placement, collision, 60 fps at 1440p, fresh-checkout build, packaged build with controls). Next: the `0.1.1`–`0.1.4` patch series (collision detail, scale/façade corrections, packaging fixes, real building façades), **Next:** `0.1.6` retail frontage (6822, 6806–6820, 6800 — blocked on reference photography for two of the three), then `0.2.0` (player body, third-person camera, animation).
|
||||
|
||||
| Area | State |
|
||||
| --- | --- |
|
||||
|
||||
+93
-14
@@ -12,18 +12,83 @@ Tools/render_screenshots.sh --verify
|
||||
|
||||
Stages the demo shot cameras, runs the game on `/Game/Hollywood/DemoMap`, copies the PNGs into `Screenshots/demo/` and then runs [`Tools/visual_loop.py`](../Tools/visual_loop.py) `--verify` to check that each shot still shows the content its expectation describes. About a minute.
|
||||
|
||||
## The shot list
|
||||
## Status at a glance
|
||||
|
||||
Each shot has an **expectation** in [`Tools/visual_targets.json`](../Tools/visual_targets.json) — the content the capture must show — and a verification state produced by `--verify`: it fails if the frame is blank, the wrong size, or has no tonal range.
|
||||
Every shot is embedded below, in order, so the whole review happens in this file.
|
||||
|
||||
| # | File | View | Expected content | Verified | Approved |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 1 | `demo/review-demo-spawn-view.png` | north sidewalk | The whole south block: the neoclassical hero façade between its neighbours, both sidewalks and the road in the foreground | ☑ 2026-11-09 | ☑ 2026-11-09 |
|
||||
| 2 | `demo/review-demo-street-level.png` | eye height, north side | Street level: the hero colonnade with the adjacent frontage buildings either side, shopfronts at pavement level | ☑ 2026-11-09 | ☑ 2026-11-09 |
|
||||
| 3 | `demo/review-demo-crossing.png` | along the crosswalk | The designated mid-block crossing (s=130 m, UE X −14956): zebra bars on the asphalt, south frontage and colonnade behind | ☑ 2026-11-09 | ☑ 2026-11-09 |
|
||||
| 4 | `demo/review-demo-north-buildings.png` | across the Boulevard | The two north-side buildings (TCL Chinese Theatre, Madame Tussaud's) from the south side, full height in frame | ☑ 2026-11-09 | ☑ 2026-11-09 |
|
||||
| # | Shot | Verified | Approved |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | [Spawn view](#1-spawn-view) | ☑ 2026-11-09 | ☑ 2026-11-09 |
|
||||
| 2 | [Street level](#2-street-level) | ☑ 2026-11-09 | ☑ 2026-11-09 |
|
||||
| 3 | [Crossing](#3-crossing) | ☑ 2026-11-09 | ☑ 2026-11-09 |
|
||||
| 4 | [North buildings](#4-north-buildings) | ☑ 2026-11-09 | ☑ 2026-11-09 |
|
||||
| 5 | [Third-person body](#5-third-person-body) | ☑ 2026-11-09 | ☐ pending |
|
||||
|
||||
Approval is per shot, in the table above (owner review of the `0.1.4` set, recorded in [`Docs/reviews/0.1.4-review.md`](../Docs/reviews/0.1.4-review.md)). Add a row when a shot is added; remove the file in the same change when a shot is retired.
|
||||
**Verified** is what `--verify` reports: the frame is not blank, is the right size, and has tonal range. **Approved** is the owner's verdict, recorded here. Each shot also has an **expectation** in [`Tools/visual_targets.json`](../Tools/visual_targets.json) describing the content it must show.
|
||||
|
||||
---
|
||||
|
||||
## 1. Spawn view
|
||||
|
||||

|
||||
|
||||
- **File:** `demo/review-demo-spawn-view.png`
|
||||
- **View:** north sidewalk, 14 m back, level horizon
|
||||
- **Expected content:** the whole south block — the neoclassical hero façade between its neighbouring buildings, both sidewalks and the road in the foreground
|
||||
- **Verified:** ☑ 2026-11-09 · **Approved:** ☑ 2026-11-09
|
||||
|
||||
---
|
||||
|
||||
## 2. Street level
|
||||
|
||||

|
||||
|
||||
- **File:** `demo/review-demo-street-level.png`
|
||||
- **View:** eye height, north side
|
||||
- **Expected content:** the hero colonnade with the adjacent frontage buildings either side, shopfronts at pavement level
|
||||
- **Verified:** ☑ 2026-11-09 · **Approved:** ☑ 2026-11-09
|
||||
|
||||
---
|
||||
|
||||
## 3. Crossing
|
||||
|
||||

|
||||
|
||||
- **File:** `demo/review-demo-crossing.png`
|
||||
- **View:** from the north sidewalk along the crosswalk
|
||||
- **Expected content:** the designated mid-block crossing (s=130 m, UE X −14956) — zebra bars on the asphalt, south frontage and colonnade behind
|
||||
- **Verified:** ☑ 2026-11-09 · **Approved:** ☑ 2026-11-09
|
||||
|
||||
---
|
||||
|
||||
## 4. North buildings
|
||||
|
||||

|
||||
|
||||
- **File:** `demo/review-demo-north-buildings.png`
|
||||
- **View:** across the Boulevard from the south side
|
||||
- **Expected content:** the two north-side buildings (TCL Chinese Theatre, Madame Tussaud's), full height in frame
|
||||
- **Verified:** ☑ 2026-11-09 · **Approved:** ☑ 2026-11-09
|
||||
|
||||
---
|
||||
|
||||
## 5. Third-person body
|
||||
|
||||

|
||||
|
||||
- **File:** `demo/review-demo-third-person.png`
|
||||
- **View:** third-person boom, ~6 m behind the pawn, captured with `-thirdperson` so the pawn starts in the third-person rig
|
||||
- **Expected content:** the player's own visible body on the north sidewalk, seen from behind, with the hero façade across the street
|
||||
- **Verified:** ☑ 2026-11-09 · **Approved:** ☐ pending
|
||||
|
||||
What this shot is evidence for, and what it is not:
|
||||
|
||||
- **Implemented and visible:** the pawn carries a visible skeletal body with the warm skin material, and a spring-arm camera with collision testing. The **V** toggle swaps first and third person; the shipped default stays first person and the review passes `-thirdperson`.
|
||||
- **Not working:** the figure is in **bind pose, arms out**. The idle clip is bound and `PlayAnimation` is called — the run logs `GigLine: playing body clip Character_AnimHuman_rig_GigLine_Idle` — so the call succeeds and the pose does not apply. Tracked in `TODO.md` under "Verify animation plays correctly on the imported rig" with that evidence.
|
||||
- **Camera collision is implemented but unproven:** the spawn point has open space behind it, so no shot exercises the arm shortening.
|
||||
- The skin material needed `used_with_skeletal_mesh=True`; without it Unreal silently substituted the default grey, which is what the first capture of this shot showed.
|
||||
|
||||
---
|
||||
|
||||
## The iteration loop (uncommitted)
|
||||
|
||||
@@ -40,12 +105,26 @@ Note the comparison is only meaningful when the shot and the reference frame the
|
||||
## Scene conventions
|
||||
|
||||
- **Demo map** (`/Game/Hollywood/DemoMap`): fixed daytime — sun pitch −62 / yaw 120 (intensity 4, warm), sky light 2.6, sky atmosphere, height fog, fixed exposure.
|
||||
- **Cameras**: elevation shots sit at their target height (pitch 0, so verticals stay parallel) and use a 42–55° lens to fill the frame from across the street.
|
||||
- **Cameras**: elevation shots sit at their target height (pitch 0, so verticals stay parallel) and use a 42–68° lens to fill the frame from across the street.
|
||||
- **Materials**: every surface carries a look-developed material from `Tools/ue_build_demo_map.py` — noise-blended base colour, masonry coursing, and a generated texture set (`Tools/generate_facade_textures.py`) sampled world-aligned. Explicit roughness and specular per surface type.
|
||||
|
||||
## Known cosmetic issues
|
||||
|
||||
- Hero façade renders pale: the stone material is a graph and a texture set, not a scanned surface (the `0.1.4` façade pass is in progress).
|
||||
- Banners sit on the column line rather than flush to the wall (they hang on the columns in the photographs, so this is acceptable).
|
||||
- Between kerb and building line the walkable surface is the terrain mesh, not a modelled pavement.
|
||||
- The El Capitan Theatre has no marquee or blade sign yet — the feature that makes it read as itself.
|
||||
Every issue is tracked in `TODO.md` under the version that will fix it; an issue with no ticket is a bug in the plan, not a note.
|
||||
|
||||
| Issue | Where it is tracked | State |
|
||||
| --- | --- | --- |
|
||||
| Hero stone renders pale — the material is a procedural graph and a generated texture set, not a measured surface | `0.9.0` → measured material library | Open |
|
||||
| Hero façade has no doors at street level left and right of the central bay, and the central-bay elements are not positioned as the photographs show | `0.9.0` → per-building models, hero façade | Open |
|
||||
| Third-person body renders in bind pose: the clip is bound and `PlayAnimation` is called, but the pose does not apply | `0.2.0` → verify animation plays correctly | Open (blocking `0.2.0`) |
|
||||
| Camera collision is implemented but never exercised against a wall | `0.2.0` → camera collision | Open |
|
||||
| Between kerb and building line the walkable surface is the terrain mesh, not a modelled pavement | `0.6.0` → visual polish | Open |
|
||||
|
||||
Accepted by the owner, not a defect:
|
||||
|
||||
- Banners hang on the column line rather than flush to the wall — that is how they hang in the photographs.
|
||||
|
||||
Resolved and removed:
|
||||
|
||||
- "The El Capitan Theatre has no marquee or blade sign" — `Tools/ue_stage_theatre_trim.py` stages all six trim actors; verified in `/tmp/gigline/theatre-import.txt`.
|
||||
- "Materials are not yet created in Blender or fully integrated" — contradicted by the scene conventions above.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -9,35 +9,31 @@ Actionable task list for this project. This file is the single, central place fo
|
||||
- When all steps in a version section are done: write a `CHANGELOG.md` entry, then remove the version section from this file.
|
||||
- Release gate: a version is tagged only on the owner's explicit go-ahead once its release checks pass, not automatically when the checkboxes close.
|
||||
|
||||
## 0.1.5 — Façade finishing (patch)
|
||||
## 0.1.6 — Retail frontage (patch)
|
||||
|
||||
Suggested blender skills: **references** (reference-image-match against the photographs), **historical-worlds** / **archviz** (1920s Hollywood Blvd neoclassical detail), **materials** and **lookdev** for the remaining surface work.
|
||||
|
||||
The `0.1.4` kit, materials and theatre trim are signed off; these are the geometry items that were carried forward rather than grown inside that patch.
|
||||
Suggested blender skills: **references** (reference-image-match), **archviz**, **historical-worlds** for the 1920s–1990s retail mix on this run.
|
||||
|
||||
### Patch tasks
|
||||
|
||||
- [ ] **Retail neighbours (6822 Budget, 6806–6820, 6800 Lee Drug) from their own references.** The 18-frame set is the El Capitan Theatre, so it cannot stand in for these façades. Fetch or shoot per-building references first, then model the shopfront line and upper storeys rather than relying on the kit alone.
|
||||
- [ ] **Hero façade: fluting and the motto band as geometry.** The colonnade is plain cylinders and the motto/medallions are flat panels; both are legible in the reference photography.
|
||||
- [ ] **Blosm blockout cleaning pass.** Roofs and facades remain untextured volumes and the `setupScript` styles stay unused; with the kit carrying the elevations, this is now cleanup rather than a second façade system.
|
||||
- [ ] **Re-measure 1440p performance** with 625 kit actors and 8 generated textures in the map. `0.1.0` measured 414 fps average, so there is headroom, but it has not been re-run since.
|
||||
- [ ] Tag `v0.1.5` and write `CHANGELOG.md` entry; then remove this section.
|
||||
- [ ] **Model 6822 Budget, 6806–6820 and 6800 Lee Drug from their own references.** `Art/reference/retail-neighbours/` holds 6800 and its neighbour (Gb321, CC BY-SA 4.0); Commons has nothing usable for 6822 or 6806–6820, so those two need photography before they can be modelled honestly. Do not substitute the El Capitan Theatre's 18-frame set — it would misattribute the façades.
|
||||
- [ ] Review every façade change with `Tools/render_screenshots.sh --verify` before tagging.
|
||||
- [ ] Tag `v0.1.6` and write `CHANGELOG.md` entry; then remove this section.
|
||||
|
||||
## 0.2.0 — MPFB player body, third-person camera, basic animation (weeks 7–9)
|
||||
|
||||
### Character setup
|
||||
### Character setup - active - session-5598ce7e-27e3-4c64-bb49-c04a4a31ee58 (branch `anim-body-0.2.0`)
|
||||
|
||||
- [x] Generate MPFB human base mesh; export baked materials and deform skeleton. Done in bootstrap: idle-pose 19 158-vert human with a 137-bone standard rig, `Tools/mpfb_character_export.py` (now `--pose idle` + warm `make_diffuse`).
|
||||
- [x] Import character into Unreal; validate skeleton, scale, and materials. Done: `/Game/Test/Character` — `SkeletalMesh` (138 bones), `Skeleton`, warm `M_Character_Export` material with baked `T_Character_Diffuse`.
|
||||
- [x] Promote the character asset into a reusable NPC prefab (visible body, collision capsule, idle pose) ready for pedestrians (`0.5.0`) once the third-person body lands. Done 2026-11-07: `AGigLineNPCPerson` (see the 0.1.0 Character-development item); `0.5.0` reuses it as the pedestrian base.
|
||||
- [ ] Create basic idle/walk animation set.
|
||||
- [ ] Verify animation plays correctly on the imported rig.
|
||||
- [x] Create basic idle/walk animation set. Done 2026-11-09: `Tools/character_anim_build.py` authors `GigLine_Idle` (60 f) and `GigLine_Walk` (30 f) at 30 fps from parametric cycles, and `Tools/ue_character_anim_import.py` imports them. Both AnimSequences verified in `/Game/Test/Character`: Idle 1.967 s, Walk 0.967 s. Limb axes were **measured** on the rig, not assumed — the probe rotated `upperleg01.L` 0.5 rad per axis and read `foot.L`, which showed X is the fore/aft swing (Y twists, Z abducts).
|
||||
- [A] Verify animation plays correctly on the imported rig. **Playback is broken and now measurable**: the pawn's mesh is bound, both clips resolve, and the run logs `GigLine: playing body clip Character_AnimHuman_rig_GigLine_Idle`, but shot 5 still renders the body in bind pose (arms out). So `PlayAnimation` is called and the pose is not applied — next step is to check whether the imported clip carries bone curves at all (the FBX bake may be sampling only keyed bones) or whether the component needs `SetAnimationMode(AnimationSingleNode)` before `SetAnimation`/`Play`.
|
||||
|
||||
### Camera and controls
|
||||
|
||||
- [ ] Add third-person camera following the player body.
|
||||
- [ ] Add first/third-person toggle.
|
||||
- [ ] Add camera collision to prevent clipping through geometry.
|
||||
- [x] Add third-person camera following the player body. Done 2026-11-09: `USpringArmComponent` + camera on the capsule, boom 340 cm, lag 8, and the body is the animated skeletal mesh with the skin material. Review shot 5 (`demo-third-person`, `Screenshots/README.md`) shows the rig; the shipped default view stays first person.
|
||||
- [x] Add first/third-person toggle. **V**, via `IA_ToggleCamera`; the toggle swaps the active camera and the body's visibility. `-thirdperson` on the command line starts in third person so the review is reproducible.
|
||||
- [x] Add camera collision to prevent clipping through geometry. The spring arm's collision test (`bDoCollisionTest`, probe 12 cm on `ECC_Camera`) shortens the boom instead of letting the camera pass through a wall. **Not yet exercised against a wall in a shot** — the spawn point has open space behind it, so this is implemented but unproven.
|
||||
- [ ] Document any input changes.
|
||||
|
||||
### Release checks
|
||||
|
||||
@@ -22,6 +22,8 @@ Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side b
|
||||
| `ue_stage_theatre_trim.py` | Imports the trim to `/Game/Hollywood/Theatre` and stages one actor per piece in the DemoMap with site materials; clears a previous run first. |
|
||||
| `ue_apply_facade_kit.py` | Lays the kit over a building's street face from its own bounding box: bay count = round(width / 4.96), storeys from the height, shopfronts on the ground floor, windows above, pilasters on bay boundaries, cornice and parapet at the top. Clears a previous application first; unique labels per building. |
|
||||
| `generate_facade_textures.py` | Deterministic stdlib PNG generator for the facade maps (`T_StoneAshlar`, `T_Stucco`, `T_Asphalt`, `T_Concrete`, each `_BC` base colour + `_R` roughness). Pure-zlib writer, hash-based value noise, identical bytes on every run; `--size` and `--out` (`/tmp/gigline/textures` by default). `ue_build_demo_map.py` imports them into `/Game/Hollywood/Textures` and samples them world-aligned. |
|
||||
| `ue_audit_scene.py` | Gate on the DemoMap: no unmaterialed actor, every verified frontage span within tolerance (`ue_placement`), no duplicate actor labels. Writes `/tmp/gigline/scene-audit.txt`; `render_screenshots.sh --verify` fails on a non-zero failure count. Catches the class of defect that still renders plausibly. |
|
||||
| `ue_placement.py` | Single source of truth for where staged actors belong in the DemoMap (hero position, verified frontage spans) plus `span_error`/`describe_span_failure` validators. Exists because the hero position was written twice and drifted — the importer kept the pre-`0.1.0` value and moved the hero 13 m east onto the wrong side of the boulevard. No `unreal` import, so it is unit-tested. |
|
||||
| `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 |
|
||||
@@ -40,6 +42,7 @@ Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side b
|
||||
| Tool | Purpose |
|
||||
| --- | --- |
|
||||
| `hero_facade_build.py` | Parametric hero façade; emits a JSON report naming the evidence behind every part |
|
||||
| `character_anim_build.py` | Authors the basic animation set on the MPFB rig — `GigLine_Idle` (60 f) and `GigLine_Walk` (30 f) sampled from parametric cycles at 30 fps — and exports one animated FBX with the `ue_character_v1` preset. Rotation axes are measured on the rig, not assumed (probe in the docstring). `--out`, `--report`. |
|
||||
| `mpfb_character_export.py` | MPFB human, standard rig, baked skin, FBX export |
|
||||
|
||||
## Unreal import and level building
|
||||
@@ -51,6 +54,7 @@ Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side b
|
||||
| `ue_build_demo_map.py` | Builds `/Game/Hollywood/DemoMap`: lighting, façade, site meshes, player start |
|
||||
| `ue_build_review_map.py` | Rebuilds `Test/ReviewMap`, stages the shot camera list, and stages the demo-map views |
|
||||
| `ue_create_input_assets.py` | Creates `/Game/Input` actions and the mapping context headlessly |
|
||||
| `ue_character_anim_import.py` | Imports the animated FBX into `/Game/Test/Character` and **verifies an AnimSequence exists** — an FBX exported without `bake_anim` imports a mesh and no clips, with no error. Writes `VERDICT: PASS/FAIL` to `/tmp/gigline/character-anim-import.txt`. |
|
||||
| `ue_character_import.py` | Skeletal import plus the material/texture chain Interchange skips |
|
||||
|
||||
## Visual review
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Author the basic character animation set (0.2.0): idle and walk.
|
||||
|
||||
Three clips on the MPFB standard rig, sampled from parametric cycles rather than
|
||||
hand-keyed poses, so the loop is closed by construction and every key can be
|
||||
re-derived from the numbers below:
|
||||
|
||||
GigLine_Idle 60 frames @ 30 fps — breathing sway, feet planted
|
||||
GigLine_Walk 30 frames @ 30 fps — one full stride, left and right contact
|
||||
|
||||
blender -b Art/blender/character/character-2026-11-07.blend \
|
||||
--python Tools/character_anim_build.py -- \
|
||||
--out /tmp/gigline/character-anim.fbx \
|
||||
--report /tmp/gigline/character-anim-report.json
|
||||
|
||||
Rotation axes are **measured, not assumed**. A probe on the actual rig rotated
|
||||
`upperleg01.L` by 0.5 rad on each axis and read the resulting `foot.L` head:
|
||||
|
||||
X -> foot Y -0.022 -> +0.362 (fore/aft swing: the axis used here)
|
||||
Y -> twist along the leg, foot barely moves
|
||||
Z -> foot swings sideways into the other leg
|
||||
|
||||
The arm probe gives the same answer for `upperarm01`, so limbs swing on local X
|
||||
and positive X sends the limb backwards. Everything else (spine lean, head nod)
|
||||
uses small X deltas for the same reason.
|
||||
|
||||
The rig is `pelvis.L → upperleg01 → upperleg02 → lowerleg01 → lowerleg02 → foot`,
|
||||
all deform bones, no constraints, so a plain pose-bone rotation deforms the mesh.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import bpy
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import fbx_export_preset as fbx_preset # noqa: E402
|
||||
|
||||
FPS = 30
|
||||
CLIPS = {"Idle": 60, "Walk": 30}
|
||||
|
||||
# Stride amplitude in radians at the hip. A human walk swings the thigh about
|
||||
# 25-30 deg forward and 15-20 back; 0.45 rad (~26 deg) is a normal-pace stride.
|
||||
HIP = 0.45
|
||||
# Knee flexion during swing, measured from the photographs of a normal gait.
|
||||
KNEE = 0.55
|
||||
# Arm swing counter-phases the leg; a relaxed walk swings about 20 deg.
|
||||
ARM = 0.32
|
||||
ELBOW = 0.18
|
||||
|
||||
PARTS: list[dict] = []
|
||||
|
||||
# The .blend stores the raw A-pose bind: `mpfb_character_export.py --pose idle`
|
||||
# bakes the arms down only in the exported FBX, not in the file. A clip that
|
||||
# animates only swing therefore leaves the arms out in an A — which is exactly how
|
||||
# the first third-person render looked. These are the same deltas `IDLE_POSE` uses,
|
||||
# applied as a base that every key sits on top of.
|
||||
BASE = {
|
||||
"shoulder01.L": (0.0, -0.15, -0.03),
|
||||
"shoulder01.R": (0.0, 0.15, 0.03),
|
||||
"upperarm01.L": (0.0, 0.0, -1.55),
|
||||
"upperarm01.R": (0.0, 0.0, 1.55),
|
||||
"lowerarm01.L": (0.0, 0.0, -0.10),
|
||||
"lowerarm01.R": (0.0, 0.0, 0.10),
|
||||
}
|
||||
|
||||
|
||||
def log(msg):
|
||||
PARTS.append(msg)
|
||||
print(f"[anim] {msg}")
|
||||
|
||||
|
||||
def clear_animation(arm):
|
||||
"""Drop any pre-existing actions so a rerun does not stack clips."""
|
||||
for action in list(bpy.data.actions):
|
||||
bpy.data.actions.remove(action)
|
||||
if arm.animation_data:
|
||||
arm.animation_data_clear()
|
||||
|
||||
|
||||
def pose_bones(arm):
|
||||
bpy.context.view_layer.objects.active = arm
|
||||
bpy.ops.object.mode_set(mode="POSE")
|
||||
for pb in arm.pose.bones:
|
||||
pb.rotation_mode = "XYZ"
|
||||
return arm.pose.bones
|
||||
|
||||
|
||||
def key_euler(pb, frame, x=0.0, y=0.0, z=0.0):
|
||||
"""Key a pose-bone rotation on top of the arms-down base."""
|
||||
bx, by, bz = BASE.get(pb.name, (0.0, 0.0, 0.0))
|
||||
pb.rotation_euler = (bx + x, by + y, bz + z)
|
||||
pb.keyframe_insert(data_path="rotation_euler", frame=frame)
|
||||
|
||||
|
||||
def clip_idle(arm, frames):
|
||||
"""Breathing sway: spine and head counter-rotate, arms drift, feet planted."""
|
||||
bones = pose_bones(arm)
|
||||
action = bpy.data.actions.new("GigLine_Idle")
|
||||
arm.animation_data_create()
|
||||
arm.animation_data.action = action
|
||||
|
||||
for frame in range(1, frames + 1):
|
||||
t = 2.0 * math.pi * (frame - 1) / frames
|
||||
breathe = math.sin(t)
|
||||
# Chest lifts and settles; the head counter-rotates so the gaze stays level.
|
||||
key_euler(bones["spine03"], frame, x=-0.020 * breathe)
|
||||
key_euler(bones["spine05"], frame, x=-0.012 * breathe)
|
||||
key_euler(bones["neck01"], frame, x=0.010 * breathe)
|
||||
key_euler(bones["head"], frame, x=0.008 * breathe)
|
||||
# Arms hang and drift a little; nothing swings.
|
||||
key_euler(bones["upperarm01.L"], frame, x=0.020 * breathe)
|
||||
key_euler(bones["upperarm01.R"], frame, x=-0.020 * breathe)
|
||||
|
||||
action.use_fake_user = True
|
||||
log(f"Idle: {frames} frames, spine/neck/head/arms, feet planted")
|
||||
return action
|
||||
|
||||
|
||||
def clip_walk(arm, frames):
|
||||
"""One full stride: left contact at frame 1, right contact at the half-cycle."""
|
||||
bones = pose_bones(arm)
|
||||
action = bpy.data.actions.new("GigLine_Walk")
|
||||
arm.animation_data.action = action
|
||||
|
||||
for frame in range(1, frames + 1):
|
||||
t = 2.0 * math.pi * (frame - 1) / frames
|
||||
swing = math.sin(t)
|
||||
# Hip swing, opposite phase per leg; negative X is forward.
|
||||
key_euler(bones["upperleg01.L"], frame, x=-HIP * swing)
|
||||
key_euler(bones["upperleg01.R"], frame, x=HIP * swing)
|
||||
# The knee only flexes while the leg is swinging forward, so it is driven
|
||||
# by the half-rectified swing rather than the raw sine.
|
||||
knee_l = KNEE * max(0.0, math.sin(t + math.pi))
|
||||
knee_r = KNEE * max(0.0, math.sin(t))
|
||||
key_euler(bones["lowerleg01.L"], frame, x=knee_l)
|
||||
key_euler(bones["lowerleg01.R"], frame, x=knee_r)
|
||||
# Ankles keep the foot roughly level: counter the hip plus half the knee.
|
||||
key_euler(bones["foot.L"], frame, x=0.25 * HIP * swing - 0.35 * knee_l)
|
||||
key_euler(bones["foot.R"], frame, x=-0.25 * HIP * swing - 0.35 * knee_r)
|
||||
# Arms counter-phase the legs, elbows carry a constant relaxed bend.
|
||||
key_euler(bones["upperarm01.L"], frame, x=ARM * swing)
|
||||
key_euler(bones["upperarm01.R"], frame, x=-ARM * swing)
|
||||
key_euler(bones["lowerarm01.L"], frame, x=ELBOW + 0.10 * swing)
|
||||
key_euler(bones["lowerarm01.R"], frame, x=ELBOW - 0.10 * swing)
|
||||
# Torso counter-rotation: the shoulders turn against the hips.
|
||||
key_euler(bones["spine04"], frame, z=0.08 * swing)
|
||||
key_euler(bones["neck01"], frame, z=-0.04 * swing)
|
||||
|
||||
action.use_fake_user = True
|
||||
log(f"Walk: {frames} frames, stride {HIP:.2f} rad hip / {KNEE:.2f} knee / {ARM:.2f} arm")
|
||||
return action
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--out", default="/tmp/gigline/character-anim.fbx")
|
||||
ap.add_argument("--report", default=None)
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
arm = next((o for o in bpy.data.objects if o.type == "ARMATURE"), None)
|
||||
if arm is None:
|
||||
raise SystemExit("no armature in the .blend")
|
||||
mesh = next((o for o in bpy.data.objects if o.type == "MESH"), None)
|
||||
log(f"rig: {arm.name}, {len(arm.pose.bones)} bones; mesh {mesh.name if mesh else 'none'} "
|
||||
f"{len(mesh.data.vertices) if mesh else 0} verts")
|
||||
|
||||
bpy.context.scene.render.fps = FPS
|
||||
clear_animation(arm)
|
||||
idle = clip_idle(arm, CLIPS["Idle"])
|
||||
walk = clip_walk(arm, CLIPS["Walk"])
|
||||
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
# Export armature + mesh together: the preset's character flags carry the
|
||||
# bake_anim settings that make Unreal create AnimSequences.
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
arm.select_set(True)
|
||||
if mesh:
|
||||
mesh.select_set(True)
|
||||
bpy.context.view_layer.objects.active = arm
|
||||
|
||||
os.makedirs(os.path.dirname(args.out), exist_ok=True)
|
||||
fbx_preset.export_character(args.out, selection=True)
|
||||
log(f"exported {args.out}")
|
||||
|
||||
if args.report:
|
||||
with open(args.report, "w", encoding="utf-8") as handle:
|
||||
json.dump({
|
||||
"fps": FPS,
|
||||
"rig": arm.name,
|
||||
"bones": len(arm.pose.bones),
|
||||
"clips": [
|
||||
{"action": idle.name, "frames": CLIPS["Idle"]},
|
||||
{"action": walk.name, "frames": CLIPS["Walk"]},
|
||||
],
|
||||
"axis_evidence": "probe: upperleg01.L X 0.5 rad moves foot.L "
|
||||
"Y -0.022 -> +0.362; Y twists, Z abducts",
|
||||
"amplitudes_rad": {"hip": HIP, "knee": KNEE, "arm": ARM, "elbow": ELBOW},
|
||||
"export": os.path.basename(args.out),
|
||||
"log": PARTS,
|
||||
}, handle, indent=2)
|
||||
print(f"[anim] report -> {args.report}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -49,12 +49,25 @@ _METRIC = {
|
||||
}
|
||||
|
||||
# Character exports differ only in the object set and the armature/path flags.
|
||||
#
|
||||
# Animation export (added for the 0.2.0 animation set): without `bake_anim` the
|
||||
# exporter silently writes a static bind pose even when the armature carries
|
||||
# actions, and Unreal then imports a character with no AnimSequence and no error.
|
||||
# `bake_anim_use_all_actions` exports every action as its own clip; NLA strips are
|
||||
# off because the project does not author NLA. `bake_anim_simplify_factor=0.0`
|
||||
# keeps the sampled keys exactly as authored - the clips are short, so the size
|
||||
# cost is irrelevant and a simplified curve would drift from the source poses.
|
||||
_CHARACTER = dict(
|
||||
_METRIC,
|
||||
object_types={"ARMATURE", "MESH"},
|
||||
add_leaf_bones=False,
|
||||
path_mode="COPY",
|
||||
embed_textures=False,
|
||||
bake_anim=True,
|
||||
bake_anim_use_all_actions=True,
|
||||
bake_anim_use_nla_strips=False,
|
||||
bake_anim_force_startend_keying=True,
|
||||
bake_anim_simplify_factor=0.0,
|
||||
)
|
||||
|
||||
PRESETS = {"ue_metric_v1": _METRIC, "ue_character_v1": _CHARACTER}
|
||||
|
||||
@@ -174,6 +174,20 @@ def build():
|
||||
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)
|
||||
|
||||
# Fluting: 20 shallow grooves around the shaft, as the photographs show on
|
||||
# the colonnade. Each flute is a thin cylinder set into the shaft surface;
|
||||
# modelling them as grooves would need booleans on 6 columns for detail the
|
||||
# eye reads only in silhouette and grazing light, so they are added mass in
|
||||
# the *shadow* direction instead — cheap, and it breaks the flat highlight.
|
||||
flutes = 20
|
||||
flute_r = 0.032
|
||||
for f in range(flutes):
|
||||
ang = (2.0 * math.pi * f) / flutes
|
||||
fx = x + (COLUMN_D / 2.0 - flute_r * 0.4) * math.cos(ang)
|
||||
fy = -0.55 + (COLUMN_D / 2.0 - flute_r * 0.4) * math.sin(ang)
|
||||
cylinder(f"Hero_Flute_{i + 1}_{f + 1}", fx, fy, shaft_z0 + 0.05,
|
||||
cap_z0 - 0.05, flute_r, stone_dark, vertices=6)
|
||||
# Capital block, then the square abacus that carries the architrave.
|
||||
# The abacus takes the top 0.14 m of the capital zone so the colonnade
|
||||
# still ends at HEIGHT - ENTABLATURE_H - PARAPET_H (proportion unchanged).
|
||||
@@ -185,6 +199,9 @@ def build():
|
||||
1.30, 1.30, abacus_h, stone)
|
||||
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)")
|
||||
note("Hero_Flute_x120", (0.064, 0.064, cap_z0 - shaft_z0),
|
||||
"photographs: fluted shafts; 20 grooves per column modelled as shallow "
|
||||
"cylinders in the shadow tone rather than boolean cut grooves")
|
||||
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),
|
||||
@@ -223,10 +240,22 @@ def build():
|
||||
FRONTAGE_W, 1.2, PARAPET_H, stone)
|
||||
note("Hero_Parapet", (FRONTAGE_W, 1.2, PARAPET_H), "assumption: parapet carries the motto band")
|
||||
|
||||
# Motto band: a recessed panel across the parapet, as the photographs show.
|
||||
cube("Hero_MottoBand", 0.0, -1.32, HEIGHT - PARAPET_H / 2.0,
|
||||
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")
|
||||
# Motto band: a recessed panel across the parapet with a moulded surround, as
|
||||
# the photographs show. The panel was a 8 cm stripe of dark stone; it is now a
|
||||
# real recess — surround frame standing 6 cm proud, panel set 12 cm behind it —
|
||||
# so it reads as an inscription panel from the street instead of a paint line.
|
||||
motto_z = HEIGHT - PARAPET_H / 2.0
|
||||
motto_w = FRONTAGE_W - 3.0
|
||||
for x_off in (-(motto_w / 2.0 + 0.16), (motto_w / 2.0 + 0.16)):
|
||||
cube(f"Hero_MottoSurround_v{int(x_off > 0)}", x_off, -1.13, motto_z,
|
||||
0.32, 0.26, 1.15, stone)
|
||||
for z_off in (-0.575, 0.575):
|
||||
cube(f"Hero_MottoSurround_h{int(z_off > 0)}", 0.0, -1.13, motto_z + z_off,
|
||||
motto_w + 0.64, 0.26, 0.32, stone)
|
||||
cube("Hero_MottoBand", 0.0, -1.06, motto_z, motto_w, 0.10, 0.85, stone_dark)
|
||||
note("Hero_MottoBand", (motto_w, 0.10, 0.85),
|
||||
"photographs: motto panel above the colonnade, now an inset panel with a "
|
||||
"moulded surround rather than a flat stripe")
|
||||
|
||||
# Medallions either side of the inscription: discs on the parapet face, not
|
||||
# pancakes on the coping (see disc_on_wall).
|
||||
|
||||
@@ -20,18 +20,32 @@ PROJ="$REPO/Game/GigLine/GigLine.uproject"
|
||||
VERIFY="no"
|
||||
[ "${1:-}" = "--verify" ] && VERIFY="yes"
|
||||
|
||||
echo "== 1/3 stage the demo shot cameras =="
|
||||
cd "$ENGINE"
|
||||
./Engine/Binaries/Linux/UnrealEditor-Cmd "$PROJ" \
|
||||
-run=pythonscript -script="$REPO/Tools/ue_build_review_map.py" \
|
||||
-nullrhi -unattended -nopause -stdout
|
||||
run_cmdlet() {
|
||||
local script="$1" label="$2"
|
||||
echo "== $label =="
|
||||
( cd "$ENGINE" && ./Engine/Binaries/Linux/UnrealEditor-Cmd "$PROJ" \
|
||||
-run=pythonscript -script="$REPO/Tools/$script" \
|
||||
-nullrhi -unattended -nopause -stdout )
|
||||
}
|
||||
|
||||
# Order matters. The demo builder rebuilds the site materials, so any actor staged
|
||||
# by another tool must be (re)staged after it or it keeps a reference to a material
|
||||
# that no longer exists - that is how 604 actors ended up untextured once.
|
||||
run_cmdlet ue_build_demo_map.py "1/5 rebuild the demo map (materials, site, frontage)"
|
||||
run_cmdlet ue_apply_facade_kit.py "2/5 assemble the facade kit"
|
||||
run_cmdlet ue_stage_theatre_trim.py "3/5 stage the theatre trim"
|
||||
run_cmdlet ue_audit_scene.py "4/6 audit the scene (materials, placement, labels)"
|
||||
run_cmdlet ue_build_review_map.py "5/6 stage the demo shot cameras"
|
||||
tail -3 /tmp/ue-review-map-result.txt 2>/dev/null || true
|
||||
|
||||
echo "== 2/3 capture the demo map =="
|
||||
./Engine/Binaries/Linux/UnrealEditor-Cmd "$PROJ" /Game/Hollywood/DemoMap \
|
||||
-game -windowed -ResX=1920 -ResY=1080 -nopause -nosplash -stdout
|
||||
echo "== 6/6 capture the demo map =="
|
||||
# Absolute engine path: this step runs from the repo, not from $ENGINE, and a bare
|
||||
# ./Engine/... failed with "No such file or directory" while every commandlet above
|
||||
# succeeded through run_cmdlet's cd. -thirdperson so the review shows the body.
|
||||
"$ENGINE/Engine/Binaries/Linux/UnrealEditor-Cmd" "$PROJ" /Game/Hollywood/DemoMap \
|
||||
-game -windowed -ResX=1920 -ResY=1080 -nopause -nosplash -thirdperson -stdout
|
||||
|
||||
echo "== 3/3 collect the demo screenshots =="
|
||||
echo "== collect the demo screenshots =="
|
||||
mkdir -p "$REPO/Screenshots/demo"
|
||||
rm -f "$REPO"/Screenshots/demo/review-*.png
|
||||
found=0
|
||||
@@ -47,6 +61,14 @@ done
|
||||
[ "$found" = "1" ] || { echo "ERROR: no demo screenshots found"; exit 1; }
|
||||
ls -1 "$REPO/Screenshots/demo"
|
||||
|
||||
# The scene audit is a gate: a wrong-material or misplaced actor is exactly the
|
||||
# kind of failure that still renders plausibly.
|
||||
if grep -q "failure(s) ==" /tmp/gigline/scene-audit.txt 2>/dev/null; then
|
||||
grep -E "FAIL|failure\(s\)" /tmp/gigline/scene-audit.txt | tail -5
|
||||
grep -q "== 0 failure(s) ==" /tmp/gigline/scene-audit.txt || {
|
||||
echo "ERROR: scene audit failed — see /tmp/gigline/scene-audit.txt"; exit 1; }
|
||||
fi
|
||||
|
||||
if [ "$VERIFY" = "yes" ]; then
|
||||
echo "== verify: does each shot show what it should? =="
|
||||
cd "$REPO"
|
||||
|
||||
@@ -52,6 +52,24 @@ class CharacterPresetTest(unittest.TestCase):
|
||||
self.assertEqual(kwargs["mesh_smooth_type"], "FACE")
|
||||
|
||||
|
||||
class CharacterAnimationTest(unittest.TestCase):
|
||||
"""The animation flags are the difference between a clip and a silent bind pose."""
|
||||
|
||||
def test_bakes_animation(self):
|
||||
kwargs = preset.preset_kwargs("ue_character_v1")
|
||||
self.assertTrue(kwargs["bake_anim"], "without bake_anim Unreal imports no AnimSequence")
|
||||
self.assertTrue(kwargs["bake_anim_use_all_actions"])
|
||||
self.assertFalse(kwargs["bake_anim_use_nla_strips"])
|
||||
|
||||
def test_does_not_simplify_authored_keys(self):
|
||||
kwargs = preset.preset_kwargs("ue_character_v1")
|
||||
self.assertEqual(kwargs["bake_anim_simplify_factor"], 0.0)
|
||||
|
||||
def test_static_preset_stays_animation_free(self):
|
||||
kwargs = preset.preset_kwargs("ue_metric_v1")
|
||||
self.assertNotIn("bake_anim", kwargs)
|
||||
|
||||
|
||||
class OverrideTest(unittest.TestCase):
|
||||
def test_overrides_win(self):
|
||||
kwargs = preset.preset_kwargs(mesh_smooth_type="OFF")
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Tests for Tools/ue_placement.py — the single source of truth for placement.
|
||||
|
||||
A regression test for a real bug: `ue_hero_facade_import.py` carried its own hero
|
||||
position (the pre-0.1.0 one) while `ue_build_demo_map.py` staged the verified one.
|
||||
Running the importer moved the hero 13 m east and onto the north side of the
|
||||
boulevard, and nothing complained because a displaced building still renders.
|
||||
"""
|
||||
|
||||
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 ue_placement # noqa: E402
|
||||
|
||||
|
||||
class HeroPlacementTest(unittest.TestCase):
|
||||
def test_hero_actor_name(self):
|
||||
self.assertEqual(ue_placement.HERO_ACTOR, "Hero_Facade")
|
||||
|
||||
def test_hero_location_is_the_verified_building_line(self):
|
||||
x, y, z = ue_placement.HERO_LOCATION_CM
|
||||
self.assertEqual((x, y, z), (-13336.0, 1307.0, 0.0))
|
||||
# North is -Y in Unreal, so the hero on the south side must have Y > 0.
|
||||
self.assertGreater(y, 0.0, "hero must be on the south side (Y > 0)")
|
||||
|
||||
def test_hero_span_is_verified(self):
|
||||
# The mesh spans -1308..+1308 cm in X about its origin.
|
||||
lo = ue_placement.HERO_LOCATION_CM[0] - 1308.0
|
||||
hi = ue_placement.HERO_LOCATION_CM[0] + 1308.0
|
||||
exp_min, exp_max, ok = ue_placement.span_error("hero", lo, hi)
|
||||
self.assertTrue(ok)
|
||||
# The mesh spans exactly the verified footprint, so the two agree.
|
||||
self.assertAlmostEqual(exp_min, lo, delta=ue_placement.SPAN_TOLERANCE_CM)
|
||||
self.assertAlmostEqual(exp_max, hi, delta=ue_placement.SPAN_TOLERANCE_CM)
|
||||
|
||||
|
||||
class SpanValidatorTest(unittest.TestCase):
|
||||
def test_misplaced_hero_is_reported(self):
|
||||
# The exact failure observed: the stale import position put the hero here.
|
||||
lo, hi = -13332.0, -10716.0
|
||||
exp_min, exp_max, ok = ue_placement.span_error("hero", lo, hi)
|
||||
self.assertFalse(ok)
|
||||
message = ue_placement.describe_span_failure("hero", lo, hi)
|
||||
self.assertIn("does not match the verified", message)
|
||||
self.assertIn(f"{exp_min:.0f}", message)
|
||||
|
||||
def test_correct_span_reports_nothing(self):
|
||||
self.assertEqual(ue_placement.describe_span_failure("hero", -14644, -12028), "")
|
||||
|
||||
def test_within_tolerance_passes(self):
|
||||
# 99 cm off is inside the ±100 cm tolerance.
|
||||
self.assertTrue(ue_placement.span_error("6922", -24276 + 99, -17700 - 99)[2])
|
||||
|
||||
def test_outside_tolerance_fails(self):
|
||||
self.assertFalse(ue_placement.span_error("6922", -24276 + 101, -17700)[2])
|
||||
|
||||
def test_unknown_building_raises(self):
|
||||
with self.assertRaises(KeyError):
|
||||
ue_placement.span_error("no-such-building", 0, 0)
|
||||
|
||||
def test_every_frontage_span_is_ordered(self):
|
||||
for name, (lo, hi) in ue_placement.FRONTAGE_SPANS_CM.items():
|
||||
self.assertLess(lo, hi, f"{name} span is inverted")
|
||||
|
||||
|
||||
class SingleSourceOfTruthTest(unittest.TestCase):
|
||||
"""The import script and the demo builder must not carry their own literals."""
|
||||
|
||||
def test_import_script_uses_the_shared_constant(self):
|
||||
src = open(os.path.join(ROOT, "Tools", "ue_hero_facade_import.py"),
|
||||
encoding="utf-8").read()
|
||||
self.assertIn("ue_placement.HERO_LOCATION_CM", src)
|
||||
self.assertNotIn("(-12024.5, -1450.0, 0.0)", src,
|
||||
"the stale pre-0.1.0 hero position must not come back")
|
||||
|
||||
def test_demo_builder_uses_the_shared_constant(self):
|
||||
src = open(os.path.join(ROOT, "Tools", "ue_build_demo_map.py"),
|
||||
encoding="utf-8").read()
|
||||
self.assertIn("ue_placement.HERO_LOCATION_CM", src)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -123,12 +123,28 @@ def apply_to_actor(actor, kit, materials=None, prefix=None):
|
||||
f"({width / 100:.1f} x {height / 100:.1f} m)")
|
||||
return 0
|
||||
|
||||
# Bays: the kit module is one window bay (4.96 m). A very wide building gets a
|
||||
# bay count that keeps the rhythm readable rather than one window per 4.96 m of
|
||||
# frontage — 6922 is 65.8 m wide, which is 13 bays, giving a window every 4.96 m
|
||||
# across a 37 m tall block: the rhythm reads as a wall of glass, not a building.
|
||||
# Above MAX_BAYS the module is widened instead, so the bay count stays legible.
|
||||
MAX_BAYS = 9
|
||||
bays = max(1, int(round(width / BAY_CM)))
|
||||
if bays > MAX_BAYS:
|
||||
bays = MAX_BAYS
|
||||
bay_w = width / bays # actual bay width for this building
|
||||
# Street face: the frontage runs along X, and the street is at smaller Y.
|
||||
face_y = mn.y
|
||||
base_z = mn.z
|
||||
storeys = max(1, int((height - SHOP_FLOOR_CM) // STOREY_CM))
|
||||
# Storeys never overrun the volume: cap the count so the top band (cornice +
|
||||
# parapet, 145 cm) always lands inside the building.
|
||||
storeys = max(1, int((height - SHOP_FLOOR_CM - 145.0) // STOREY_CM) + 1)
|
||||
top_z = base_z + height
|
||||
# Windows above the shopfront, spread over the available height rather than
|
||||
# assumed to be exactly STOREY_CM apart, so a 37 m block does not leave a gap
|
||||
# of bare wall under the cornice.
|
||||
usable = max(0.0, (top_z - 145.0) - (base_z + SHOP_FLOOR_CM))
|
||||
storey_step = (usable / storeys) if storeys else STOREY_CM
|
||||
|
||||
def kit_material(name):
|
||||
if materials is None:
|
||||
@@ -140,11 +156,13 @@ def apply_to_actor(actor, kit, materials=None, prefix=None):
|
||||
for bay in range(bays):
|
||||
cx = mn.x + bay_w * (bay + 0.5)
|
||||
# ground floor: shopfront
|
||||
for piece, dx in (("Kit_ShopPier_L", -bay_w / 2 + 30.0),
|
||||
("Kit_ShopPier_R", bay_w / 2 - 30.0)):
|
||||
for piece, dx, side in (("Kit_ShopPier_L", -bay_w / 2 + 30.0, "L"),
|
||||
("Kit_ShopPier_R", bay_w / 2 - 30.0, "R")):
|
||||
if piece in kit:
|
||||
# The side belongs in the label: both piers of a bay shared one
|
||||
# label, which left 53 ambiguous pairs in the outliner.
|
||||
if _spawn(kit[piece], unreal.Vector(cx + dx, face_y, base_z),
|
||||
f"{prefix}shop_pier_{bay}", kit_material(piece)):
|
||||
f"{prefix}shop_pier_{side}_{bay}", kit_material(piece)):
|
||||
placed += 1
|
||||
if "Kit_ShopGlass" in kit:
|
||||
if _spawn(kit["Kit_ShopGlass"], unreal.Vector(cx, face_y, base_z),
|
||||
@@ -153,7 +171,7 @@ def apply_to_actor(actor, kit, materials=None, prefix=None):
|
||||
# upper storeys: window bays
|
||||
for storey in range(storeys):
|
||||
if "Kit_WindowBay" in kit:
|
||||
z = base_z + SHOP_FLOOR_CM + STOREY_CM * storey + 65.0
|
||||
z = base_z + SHOP_FLOOR_CM + storey_step * storey + 65.0
|
||||
if _spawn(kit["Kit_WindowBay"], unreal.Vector(cx, face_y, z),
|
||||
f"{prefix}window_{bay}_{storey}", kit_material("Kit_WindowBay")):
|
||||
placed += 1
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Audit the DemoMap for staged actors that would render wrong.
|
||||
|
||||
Checks that are cheap and catch the failures this project actually had:
|
||||
|
||||
1. **No unmaterialed actors.** 604 actors once rendered with the default material
|
||||
because the demo builder deleted and recreated the site materials, orphaning
|
||||
every reference; the map looked merely "a bit flat".
|
||||
2. **Placement spans.** Each verified frontage building is probed against
|
||||
`Tools/ue_placement.py`; a building 13 m off its footprint still renders fine,
|
||||
which is exactly why it needs a number, not an eye.
|
||||
3. **No duplicate actor labels.** Two buildings' kit pieces sharing a label made
|
||||
the outliner ambiguous and hid one of them.
|
||||
|
||||
UnrealEditor-Cmd <uproject> -run=pythonscript \
|
||||
-script=Tools/ue_audit_scene.py -nullrhi -unattended -nopause -stdout
|
||||
|
||||
Result file: `/tmp/gigline/scene-audit.txt`. Exit code from the commandlet is
|
||||
always 0, so **read the file** — `visual_loop.py --verify` does not depend on it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import os
|
||||
import sys
|
||||
|
||||
import unreal
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import ue_placement # noqa: E402
|
||||
|
||||
MAP = "/Game/Hollywood/DemoMap"
|
||||
RESULT = "/tmp/gigline/scene-audit.txt"
|
||||
|
||||
lines: list[str] = []
|
||||
failures = 0
|
||||
|
||||
|
||||
def log(message: str) -> None:
|
||||
lines.append(message)
|
||||
print(message)
|
||||
|
||||
|
||||
def fail(message: str) -> None:
|
||||
global failures
|
||||
failures += 1
|
||||
log("FAIL " + message)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not unreal.EditorLevelLibrary.load_level(MAP):
|
||||
log(f"could not load {MAP}")
|
||||
return
|
||||
|
||||
actors = unreal.EditorLevelLibrary.get_all_level_actors()
|
||||
|
||||
# 1. materials
|
||||
unmaterialed = []
|
||||
for actor in actors:
|
||||
comp = actor.get_component_by_class(unreal.StaticMeshComponent)
|
||||
if not comp or not comp.static_mesh:
|
||||
continue
|
||||
if comp.get_material(0) is None:
|
||||
unmaterialed.append(actor.get_actor_label())
|
||||
log(f"mesh actors: {sum(1 for a in actors if a.get_component_by_class(unreal.StaticMeshComponent))}")
|
||||
if unmaterialed:
|
||||
fail(f"{len(unmaterialed)} actor(s) with no material, e.g. {unmaterialed[:6]}")
|
||||
else:
|
||||
log("OK every staged mesh has a material")
|
||||
|
||||
# 2. placement spans
|
||||
for actor in actors:
|
||||
label = actor.get_actor_label()
|
||||
key = None
|
||||
if label.startswith("Frontage_SM_"):
|
||||
name = label[len("Frontage_SM_"):]
|
||||
key = {"Hollywood_6922": "6922", "Hollywood_6902": "6902",
|
||||
"ElCapitanTheatre": "6838", "Budget_6822": "6822",
|
||||
"Hollywood_6806": "6806", "LeeDrug_6800": "6800"}.get(name)
|
||||
elif label == ue_placement.HERO_ACTOR:
|
||||
key = "hero"
|
||||
if not key:
|
||||
continue
|
||||
origin, extent = actor.get_actor_bounds(False)
|
||||
lo, hi = origin.x - extent.x, origin.x + extent.x
|
||||
problem = ue_placement.describe_span_failure(key, lo, hi)
|
||||
if problem:
|
||||
fail(problem)
|
||||
else:
|
||||
log(f"OK {key}: X {lo:.0f}..{hi:.0f}")
|
||||
|
||||
# 3. duplicate labels
|
||||
counts = collections.Counter(a.get_actor_label() for a in actors)
|
||||
duplicates = {k: v for k, v in counts.items() if v > 1}
|
||||
if duplicates:
|
||||
sample = list(duplicates.items())[:6]
|
||||
fail(f"{len(duplicates)} duplicated actor label(s), e.g. {sample}")
|
||||
else:
|
||||
log("OK no duplicate actor labels")
|
||||
|
||||
log(f"== {failures} failure(s) ==")
|
||||
|
||||
|
||||
try:
|
||||
main()
|
||||
except Exception as exc: # noqa: BLE001 - surface in the result file
|
||||
import traceback
|
||||
log(f"FAILED: {type(exc).__name__}: {exc}")
|
||||
log(traceback.format_exc())
|
||||
|
||||
os.makedirs(os.path.dirname(RESULT), exist_ok=True)
|
||||
with open(RESULT, "w", encoding="utf-8") as handle:
|
||||
handle.write("\n".join(lines) + "\n")
|
||||
@@ -23,6 +23,8 @@ import unreal
|
||||
MAP = "/Game/Hollywood/DemoMap"
|
||||
# Repository root, so the texture generator can be invoked by path.
|
||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import ue_placement # noqa: E402
|
||||
RESULT = "/tmp/gigline/demo-map.txt"
|
||||
lines = []
|
||||
|
||||
@@ -116,14 +118,23 @@ def ensure_material(mat_name, spec, textures=None):
|
||||
does not read as one flat specular value.
|
||||
"""
|
||||
path = f"/Game/Hollywood/Site/{mat_name}"
|
||||
if unreal.EditorAssetLibrary.does_asset_exist(path):
|
||||
# Rebuilt every run so a look change cannot be masked by a stale asset.
|
||||
unreal.EditorAssetLibrary.delete_asset(path)
|
||||
|
||||
mat = unreal.AssetToolsHelpers.get_asset_tools().create_asset(
|
||||
mat_name, "/Game/Hollywood/Site", unreal.Material, unreal.MaterialFactoryNew())
|
||||
lib = unreal.MaterialEditingLibrary
|
||||
|
||||
# Update the existing material IN PLACE rather than deleting and recreating it.
|
||||
# Deleting the asset to force a look rebuild silently orphaned every reference:
|
||||
# after one demo-builder run, all 598 kit actors and the theatre trim pointed at
|
||||
# deleted materials and rendered with the default one (604 actors with no
|
||||
# material), while the map itself looked merely "a bit flat".
|
||||
mat = unreal.EditorAssetLibrary.load_asset(path)
|
||||
if mat is None:
|
||||
mat = unreal.AssetToolsHelpers.get_asset_tools().create_asset(
|
||||
mat_name, "/Game/Hollywood/Site", unreal.Material, unreal.MaterialFactoryNew())
|
||||
else:
|
||||
# Clear the old graph so a removed node cannot survive a look change.
|
||||
# `Material.expressions` is protected from Python, so use the editing
|
||||
# library's own deleter rather than reading the array.
|
||||
lib.delete_all_material_expressions(mat)
|
||||
|
||||
def node(cls):
|
||||
return lib.create_material_expression(mat, cls)
|
||||
|
||||
@@ -401,7 +412,7 @@ def build():
|
||||
# the NORTH side of the street, which only looked right while it stood alone.
|
||||
hero = unreal.EditorAssetLibrary.load_asset("/Game/Hollywood/ElCapitan/SM_HeroFacade")
|
||||
if hero:
|
||||
actor = spawn(unreal.StaticMeshActor, unreal.Vector(-13336.0, 1307.0, 0.0), "Hero_Facade")
|
||||
actor = spawn(unreal.StaticMeshActor, unreal.Vector(*ue_placement.HERO_LOCATION_CM), ue_placement.HERO_ACTOR)
|
||||
if actor:
|
||||
actor.static_mesh_component.set_static_mesh(hero)
|
||||
actor.static_mesh_component.set_mobility(unreal.ComponentMobility.STATIC)
|
||||
|
||||
@@ -308,6 +308,11 @@ try:
|
||||
# (X -22750) and look NORTH toward the mass. The old camera was at
|
||||
# X -22800 Y +2600 aiming at its own X, giving an 85%-sky frame.
|
||||
("demo-north-buildings", unreal.Vector(-22750, 1600, 500), unreal.Vector(-22750, -5200, 350)),
|
||||
# 0.2.0 third-person body. Stands where the pawn's own boom camera sits —
|
||||
# 3.4 m behind the spawn point, 1 m above it — and looks at the pawn, so
|
||||
# the visible body and its idle clip are what the shot is for.
|
||||
# Demo_PlayerStart is at (-13336, -1150, 600), yaw 90 (facing +Y).
|
||||
("demo-third-person", unreal.Vector(-13336, -1750, 780), unreal.Vector(-13336, -1150, 560), 58.0),
|
||||
]
|
||||
seq = spawn(unreal.GigLineShotActor, unreal.Vector(0, 0, 300), "ShotSequencer")
|
||||
if seq:
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# Import the character animation clips into Unreal (0.2.0).
|
||||
#
|
||||
# blender -b Art/blender/character/character-2026-11-07.blend \
|
||||
# --python Tools/character_anim_build.py -- --out /tmp/gigline/character-anim.fbx
|
||||
# <engine>/Engine/Binaries/Linux/UnrealEditor-Cmd <uproject> -run=pythonscript \
|
||||
# -script=<repo>/Tools/ue_character_anim_import.py -nullrhi -unattended -nopause -stdout
|
||||
#
|
||||
# Imports the animated FBX next to the existing character assets and verifies the
|
||||
# result rather than assuming it: an FBX whose export omitted `bake_anim` imports
|
||||
# without error and produces a skeletal mesh but no AnimSequence, which is exactly
|
||||
# the failure this tool must catch.
|
||||
#
|
||||
# Destination: /Game/Test/Character. Result file: /tmp/gigline/character-anim-import.txt
|
||||
|
||||
import glob
|
||||
import os
|
||||
|
||||
import unreal
|
||||
|
||||
SRC = "/tmp/gigline/character-anim.fbx"
|
||||
DEST = "/Game/Test/Character"
|
||||
PREFIX = "Character_Anim"
|
||||
RESULT = "/tmp/gigline/character-anim-import.txt"
|
||||
lines = []
|
||||
|
||||
|
||||
def log(msg):
|
||||
lines.append(str(msg))
|
||||
unreal.log(str(msg))
|
||||
|
||||
|
||||
def main():
|
||||
if not os.path.exists(SRC):
|
||||
log(f"missing {SRC}; run Tools/character_anim_build.py first")
|
||||
return
|
||||
|
||||
task = unreal.AssetImportTask()
|
||||
task.filename = SRC
|
||||
task.destination_path = DEST
|
||||
task.destination_name = PREFIX
|
||||
task.automated = True
|
||||
task.save = True
|
||||
task.replace_existing = True
|
||||
|
||||
ui = unreal.FbxImportUI()
|
||||
ui.import_mesh = True
|
||||
ui.import_as_skeletal = True
|
||||
ui.import_animations = True
|
||||
ui.import_materials = True
|
||||
ui.import_textures = False
|
||||
# UE 5.8 has no SKELETON_ONLY content type (FBXICT_ALL/GEOMETRY/SKINNING_WEIGHTS
|
||||
# only), so the clips come in with the mesh and the verification below decides
|
||||
# whether that worked. The mesh is replaced at the same path by design.
|
||||
ui.skeletal_mesh_import_data.import_uniform_scale = 1.0
|
||||
task.options = ui
|
||||
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
|
||||
|
||||
# Verify: the clips are the deliverable, not the mesh.
|
||||
registry = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
found = []
|
||||
for asset in registry.get_assets_by_path(DEST, recursive=False):
|
||||
name = str(asset.asset_name)
|
||||
if not name.startswith(PREFIX):
|
||||
continue
|
||||
obj = unreal.EditorAssetLibrary.load_asset(f"{DEST}/{name}")
|
||||
if isinstance(obj, unreal.AnimSequence):
|
||||
length = obj.get_play_length()
|
||||
# UE 5.8 exposes only play length and sequence length here; the sampled
|
||||
# frame count is derived so the report still states the clip duration.
|
||||
frames = round(length * 30.0)
|
||||
found.append(name)
|
||||
log(f"AnimSequence {name:34} {length:.3f} s (~{frames} frames at 30 fps)")
|
||||
elif isinstance(obj, unreal.SkeletalMesh):
|
||||
skeleton = obj.get_editor_property("skeleton")
|
||||
log(f"SkeletalMesh {name:34} skeleton={'ok' if skeleton else 'MISSING'}")
|
||||
|
||||
if not found:
|
||||
log("FAILED: no AnimSequence was created — the FBX carried no baked animation")
|
||||
lines.append("VERDICT: FAIL")
|
||||
return
|
||||
log(f"{len(found)} AnimSequence(s) imported: {', '.join(sorted(found))}")
|
||||
lines.append("VERDICT: PASS")
|
||||
|
||||
|
||||
try:
|
||||
main()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
import traceback
|
||||
log(f"FAILED: {type(exc).__name__}: {exc}")
|
||||
log(traceback.format_exc())
|
||||
lines.append("VERDICT: FAIL")
|
||||
|
||||
unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True)
|
||||
os.makedirs(os.path.dirname(RESULT), exist_ok=True)
|
||||
with open(RESULT, "w", encoding="utf-8") as handle:
|
||||
handle.write("\n".join(lines) + "\n")
|
||||
@@ -127,6 +127,10 @@ def build():
|
||||
ia_reset.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
|
||||
save(ia_reset)
|
||||
|
||||
ia_cam = fresh("IA_ToggleCamera", unreal.InputAction, unreal.InputAction_Factory)
|
||||
ia_cam.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
|
||||
save(ia_cam)
|
||||
|
||||
ia_pause = fresh("IA_Pause", unreal.InputAction, unreal.InputAction_Factory)
|
||||
ia_pause.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
|
||||
save(ia_pause)
|
||||
@@ -136,7 +140,7 @@ def build():
|
||||
(ia_move, "Gamepad_LeftY"), (ia_strafe, "Gamepad_LeftX"),
|
||||
(ia_look, "MouseX"), (ia_look, "MouseY"), (ia_look, "Gamepad_Right2D"),
|
||||
(ia_jump, "SpaceBar"), (ia_jump, "Gamepad_FaceButton_Bottom"),
|
||||
(ia_reset, "R"), (ia_pause, "Escape"),
|
||||
(ia_reset, "R"), (ia_pause, "Escape"), (ia_cam, "V"),
|
||||
]
|
||||
|
||||
mappings = []
|
||||
|
||||
@@ -19,14 +19,19 @@ import unreal
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import ue_common # noqa: E402
|
||||
import ue_placement # noqa: E402
|
||||
|
||||
FBX = "/tmp/gigline/hero-facade.fbx"
|
||||
DEST = "/Game/Hollywood/ElCapitan"
|
||||
ACTOR_LABEL = "Hero_Facade"
|
||||
ACTOR_LABEL = ue_placement.HERO_ACTOR
|
||||
LEVEL = "/Game/Hollywood/DemoMap"
|
||||
|
||||
# Unreal centimetres: south-east corner of the façade, on the building line.
|
||||
PLACEMENT_CM = (-12024.5, -1450.0, 0.0)
|
||||
# Placement lives in Tools/ue_placement.py. It used to be a literal here, and it
|
||||
# drifted from ue_build_demo_map.py: this file kept the pre-0.1.0 position
|
||||
# (-12024.5, -1450) while the builder staged (-13336, 1307), so running the importer
|
||||
# on its own moved the hero 13 m east and onto the north side of the boulevard.
|
||||
PLACEMENT_CM = ue_placement.HERO_LOCATION_CM
|
||||
|
||||
RESULT = "/tmp/gigline/hero-facade-import.txt"
|
||||
lines = []
|
||||
@@ -118,6 +123,17 @@ def measure_and_place():
|
||||
comp.set_static_mesh(asset)
|
||||
comp.set_mobility(unreal.ComponentMobility.STATIC)
|
||||
log(f"placed {ACTOR_LABEL} at {PLACEMENT_CM} (cm) on {LEVEL}")
|
||||
|
||||
# Prove it landed on its verified footprint, not merely that it spawned.
|
||||
box = asset.get_bounding_box() if asset else None
|
||||
if box:
|
||||
lo = box.min.x + PLACEMENT_CM[0]
|
||||
hi = box.max.x + PLACEMENT_CM[0]
|
||||
problem = ue_placement.describe_span_failure("hero", lo, hi)
|
||||
if problem:
|
||||
log(f"PLACEMENT ERROR: {problem}")
|
||||
else:
|
||||
log(f"span ok: X {lo:.0f}..{hi:.0f}")
|
||||
_make_map_writable(LEVEL)
|
||||
saved = unreal.EditorLoadingAndSavingUtils.save_map(
|
||||
unreal.EditorLevelLibrary.get_editor_world(), LEVEL)
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Single source of truth for where staged actors belong in the DemoMap.
|
||||
|
||||
This exists because the same value was written in two places and they drifted:
|
||||
`ue_hero_facade_import.py` kept the pre-`0.1.0` position `(-12024.5, -1450)` while
|
||||
`ue_build_demo_map.py` staged the hero at `(-13336, 1307)`. Running the importer on
|
||||
its own therefore moved the hero **13 m east and onto the north side of the
|
||||
boulevard** — and nothing complained, because a displaced building still renders.
|
||||
|
||||
The values are Unreal centimetres, absolute world space. Every one of them is
|
||||
verified against `Geo/verification/2026-11-06-el-capitan-block.md` and probed by
|
||||
`Tools/visual_loop.py --verify` (via the demo screenshot expectations) and by the
|
||||
world-span probes recorded in `MEMORY.md`.
|
||||
|
||||
Keep this file boring: constants and one validator, no `unreal` import, so it can
|
||||
be unit-tested on the host.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# The hero façade sits INSIDE the frontage gap between 6902 (ends at -17749) and
|
||||
# the El Capitan Theatre (starts at -12082), so its actor origin is the measured
|
||||
# building line. The mesh is authored with its front plane 7 cm ahead of the actor
|
||||
# origin, which is why the X here is the frontage centre and Y is the building line.
|
||||
HERO_ACTOR = "Hero_Facade"
|
||||
HERO_LOCATION_CM = (-13336.0, 1307.0, 0.0)
|
||||
|
||||
# Tolerance for the span probe: the verified footprints are within ~1 m.
|
||||
SPAN_TOLERANCE_CM = 100.0
|
||||
|
||||
# Expected world span per frontage actor (min X, max X), from the block
|
||||
# verification notes. Used to prove a staged building is on its own footprint
|
||||
# rather than merely present.
|
||||
FRONTAGE_SPANS_CM = {
|
||||
"6922": (-24276.0, -17700.0),
|
||||
"6902": (-17749.0, -14591.0),
|
||||
"hero": (-14644.0, -12028.0),
|
||||
"6838": (-12082.0, -8210.0),
|
||||
"6822": (-7854.0, -7252.0),
|
||||
"6806": (-6608.0, -3307.0),
|
||||
"6800": (-3320.0, -1437.0),
|
||||
}
|
||||
|
||||
|
||||
def span_error(name: str, observed_min: float, observed_max: float):
|
||||
"""Return (expected_min, expected_max, ok) for an observed building span.
|
||||
|
||||
`name` is a key of `FRONTAGE_SPANS_CM`; `ok` is False when either edge is more
|
||||
than `SPAN_TOLERANCE_CM` from the verified value.
|
||||
"""
|
||||
if name not in FRONTAGE_SPANS_CM:
|
||||
raise KeyError(f"unknown building {name!r}; known: {sorted(FRONTAGE_SPANS_CM)}")
|
||||
exp_min, exp_max = FRONTAGE_SPANS_CM[name]
|
||||
ok = (abs(observed_min - exp_min) <= SPAN_TOLERANCE_CM
|
||||
and abs(observed_max - exp_max) <= SPAN_TOLERANCE_CM)
|
||||
return exp_min, exp_max, ok
|
||||
|
||||
|
||||
def describe_span_failure(name: str, observed_min: float, observed_max: float) -> str:
|
||||
"""A one-line message for a misplaced building, or "" when it is correct."""
|
||||
exp_min, exp_max, ok = span_error(name, observed_min, observed_max)
|
||||
if ok:
|
||||
return ""
|
||||
return (f"{name}: span X {observed_min:.0f}..{observed_max:.0f} does not match the "
|
||||
f"verified {exp_min:.0f}..{exp_max:.0f} (±{SPAN_TOLERANCE_CM:.0f} cm)")
|
||||
@@ -48,6 +48,18 @@
|
||||
"Art/reference/north-chinese/01-grauman-s-chinese-theatre-by-carol-highsmith.jpg"
|
||||
],
|
||||
"expect": "The two north-side buildings (TCL Chinese Theatre, Madame Tussaud's) seen from across Hollywood Boulevard, full height in frame, daylight"
|
||||
},
|
||||
{
|
||||
"name": "demo-third-person",
|
||||
"shot": "Screenshots/demo/review-demo-third-person.png",
|
||||
"size": [
|
||||
1920,
|
||||
1080
|
||||
],
|
||||
"references": [
|
||||
"Art/reference/hollywood/el-capitan-facade-day-wikimedia-2009-10-27.jpg"
|
||||
],
|
||||
"expect": "Third-person view of the player body standing on the north sidewalk, full figure visible from behind, the hero facade across the street, daylight"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user