Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9f31f271e | ||
|
|
03af8146a6 | ||
|
|
30e4d65743 | ||
|
|
bc2e9c3b8e | ||
|
|
12272d6b96 | ||
|
|
56e104f5f3 | ||
|
|
8e880ed09d | ||
|
|
9ce7011389 | ||
|
|
f5e7e66127 | ||
|
|
dbb177da6f | ||
|
|
eeeabe56b1 | ||
|
|
f84093adba | ||
|
|
0e0aed02d6 | ||
|
|
7ea3c1999c | ||
|
|
46b84884b8 | ||
|
|
8993fd83c4 | ||
|
|
a1f5989cbb | ||
|
|
d0f94dff4e | ||
|
|
50e023f15d | ||
|
|
e487655e2f | ||
|
|
304c1e9afd | ||
|
|
05c1488c00 | ||
|
|
96238bdb6a | ||
|
|
817313aabd | ||
|
|
744bf8f357 | ||
|
|
c72bc41af1 | ||
|
|
803e77473c | ||
|
|
3942e9a215 | ||
|
|
4132f42ca1 | ||
|
|
c53386fcf4 | ||
|
|
2614d259aa | ||
|
|
4e703fcfd9 | ||
|
|
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",
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# Reference analysis — retail frontage, 0.1.6
|
||||
|
||||
Phase 0 of the `references` pack (reference-image-match). Written before any geometry, because the pass that produced the hero façade skipped it and cost two rebuilds.
|
||||
|
||||
## Source
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| Image | `Art/reference/retail-neighbours/01-lee-drug-and-neighbor.jpg` (1920×953) |
|
||||
| Author / licence | Gb321, own work, **CC BY-SA 4.0** — attribution required |
|
||||
| Captured | 2024-07-19 11:11 local |
|
||||
| Description | "Lee Drug and 6806 Hollywood Buildings in Hollywood" |
|
||||
| Coverage | Both target buildings in one frame, mid-morning sun from the south-east |
|
||||
|
||||
**One photograph is the whole evidence base for 6800 and 6806–6820.** It is a street-level oblique, not an elevation: verticals converge, the ground floor of 6806–6820 is partly behind a parked car, and the Lee Drug shopfront is behind a bus. Everything below that the photo does not resolve is marked _assumed_ and is carried into the part report.
|
||||
|
||||
**6822 Budget has no photograph.** It is a 6.0 m × 5.5 m single-storey sliver west of 6806–6820, and the TODO item already records that Commons has nothing usable. It stays a blockout extrusion and the item is deferred rather than guessed.
|
||||
|
||||
## Measured (not from the photo)
|
||||
|
||||
From `Geo/raw/osm-api-map-2026-11-06.osm` and the blockout, in the shared frame (metres, X east, Y north, Z from the project datum):
|
||||
|
||||
| Building | OSM way | X | Front face Y | Height | Depth |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 6800 Lee Drug | 424024149 | −33.20 … −14.37 | −14.63 | 12.10 m | 33.5 m |
|
||||
| 6806–6820 | 424024150 | −66.08 … −33.07 | −12.52 | 12.10 m | 30.8 m |
|
||||
| 6822 Budget | 424024144 | −78.54 … −72.52 | −12.92 | 5.50 m | 15.4 m |
|
||||
|
||||
Both target buildings are 12.10 m tall, which makes the _relative_ proportions in the photo directly usable: the parapet line, the cornice and the shopfront line can be read as fractions of the frame height.
|
||||
|
||||
## 6800 Lee Drug — reading
|
||||
|
||||
| Category | Observation |
|
||||
| --- | --- |
|
||||
| Subject | Corner retail block at Hollywood × Highland; the street face and the corner return are both visible |
|
||||
| Silhouette | Long low box, broken by a **rounded corner tower** that rises above the frontage parapet; the parapet is **stepped**, not flat |
|
||||
| Storeys | Two zones, not storeys: a tall shopfront zone and a single tall display wall above it |
|
||||
| Proportion (vertical) | Shopfront ≈ 0 → 0.36 H; bronze cornice ≈ 0.36 → 0.43 H; mural + lettering wall ≈ 0.43 → 0.82 H; stepped parapet ≈ 0.82 → 1.0 H (_assumed split; read off the frame, ±0.03 H_) |
|
||||
| Proportion (horizontal) | Corner tower occupies ≈ 4 m of the east end; eight mural panels at roughly 2.2 m centres along the frontage |
|
||||
| Colour | Cream stucco wall `#EDE6D8`, bronze/brown cornice `#6B4A2F`, near-black mural plates `#1C1A18`, dark glazing `#20262A` (_sampled from the frame_) |
|
||||
| Unique features | Stepped Deco parapet with 4 horizontal reveal lines; projecting bronze cornice; row of black celebrity-portrait panels; large dark "SOUVENIRS OF …" lettering on the upper wall; shopfront glazing with slim mullions |
|
||||
| Mood | 1930s Deco retail, freshly painted, bright midday |
|
||||
|
||||
## 6806–6820 — reading
|
||||
|
||||
| Category | Observation |
|
||||
| --- | --- |
|
||||
| Silhouette | Flat-fronted two-storey block; parapet steps up in fins at the west end |
|
||||
| Proportion (vertical) | Shopfront ≈ 0 → 0.32 H; burgundy fascia band ≈ 0.32 → 0.38 H; window storey ≈ 0.38 → 0.62 H; blank wall + parapet ≈ 0.62 → 1.0 H (_assumed_) |
|
||||
| Horizontal | Brown pilaster strips divide the frontage into roughly 3 m bays; seven upper windows across 33 m |
|
||||
| Colour | Same cream stucco; brown pilasters `#6B4A2F`; **burgundy-green** fascia `#5B3A3A`; white window frames |
|
||||
| Unique features | Banded parapet with 3 reveal lines; white-framed upper windows with security bars; shopfront glazing behind a burgundy fascia |
|
||||
|
||||
## Camera match (for the review shot)
|
||||
|
||||
The photo is a street-level oblique from the **north** side looking south-east, subject filling ~80 % of the frame width. The demo review shot must reproduce that: camera at 1.7 m on the north sidewalk, yaw toward the south frontage, 35–42° lens, both buildings in frame. A wide block view will not compare usefully against this crop.
|
||||
|
||||
## Assumptions to carry into the model report
|
||||
|
||||
1. Vertical proportions, as above — the photo's convergence makes a pixel-perfect read impossible.
|
||||
2. Mural panel size and spacing; the lettering is modelled as a recessed plate, not as text.
|
||||
3. Shopfront bay spacing on both buildings; the bus hides most of Lee Drug's ground floor.
|
||||
4. Parapet reveal line count (4 on Lee Drug, 3 on 6806–6820).
|
||||
5. Corner tower radius; only the outer curve is visible.
|
||||
+131
-1
@@ -8,6 +8,135 @@ Format: [Keep a Changelog](https://keepachangelog.com/) style, versions follow [
|
||||
|
||||
Nothing yet.
|
||||
|
||||
## [0.2.1] — 2026-11-09
|
||||
|
||||
Rig fixes. Two defects from the `0.2.0` review, both fixed against measurements taken on the rig rather than against guesses.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **The fingers splayed.** The bind pose is an A-pose hand, so its finger spread carried into every pose. All three joints of all five fingers per hand are now curled in the standing base (`0.28 / 0.34 / 0.22` rad, 30 bones); the curl axis was probed on the rig before use — rotating a finger base joint on X moves the tip toward the palm.
|
||||
- **The arms-down rotation was hard-coded.** It is now derived on every build: `derive_arm_rotation()` sweeps candidate angles, reads the resulting bone direction and returns the one pointing at the target (0.60 rad, direction dot 1.000 in this build). The old 0.55 was a hand-swept constant that would have rotted on any rig change.
|
||||
- **Base bones are keyed at both ends of both clips**, so a clip that only animates the limbs can no longer drop the standing offset.
|
||||
|
||||
### Known limitations
|
||||
|
||||
- **The stride capture regressed.** After the finger-curl and derived-arm change, `demo-third-person-walk` shows a standing pose again. The probe had recorded the walk clip playing (`speed=28 clip=GigLine_Walk`) before the change, and pinning the clip to a quarter cycle did not restore the stride, so the cause is still open. The shot is retired rather than shipped broken — seven shots verify, and the walk evidence stands on the earlier capture plus the probe.
|
||||
- Camera collision remains implemented but unexercised against a wall.
|
||||
|
||||
## [0.2.0] — 2026-11-09
|
||||
|
||||
Third-person body. The pawn gains a visible, animated body and a third-person camera, so the walk cycle is finally something a player can see rather than a clip in an asset list.
|
||||
|
||||
### Added
|
||||
|
||||
- **`Tools/character_anim_build.py`**: `GigLine_Idle` (60 frames) and `GigLine_Walk` (30 frames) at 30 fps, authored as parametric cycles so the loop closes by construction. Limb axes were **measured** on the rig — a probe rotated `upperleg01.L` by 0.5 rad on each axis and read `foot.L`, which showed X swings the limb fore/aft while Y twists and Z abducts.
|
||||
- **Third-person rig** (`ADR-9`): a visible skeletal body, a `USpringArmComponent` with collision testing, and a **V** toggle. One pawn carries both rigs; first person stays the shipped default.
|
||||
- **Walk capture harness**: a per-shot `ShotFollow` mode that re-places the review camera every frame in the pawn's frame, plus `SetReviewWalk` / `ReturnToStartForReview`, so a stride is captured deterministically and the fixed-camera shots still find the pawn at spawn.
|
||||
- **Review shots 6, 7 and 8** — third-person back, front and mid-stride — embedded in `Screenshots/README.md`.
|
||||
|
||||
### Changed
|
||||
|
||||
- Character export preset now **bakes animation** (`bake_anim`, all actions). Without it the exporter wrote a static bind pose and Unreal imported a mesh with no clips and no error.
|
||||
- `Docs/controls.md` documents the V toggle and the review switches; the in-game HUD line reads `WASD move · Mouse look · Space jump · V view · R reset · Esc pause`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`PlayAnimation` on a hidden component never took effect.** The body is hidden at construction and only unhidden by the toggle, so the first call could land before the component was ready; the code now sets `AnimationSingleNode`, calls `SetAnimation` + `Play`, and re-asserts while the instance is missing.
|
||||
- **The animation switch restarted the clip every frame**, freezing the body on frame 0.
|
||||
- **The importer silently refused to replace the existing assets.** Changing the arm rotation and rebuilding returned the identical bone quaternion every time; `replace_existing` and `delete_asset` both failed because the pawn's constructor hard-references the clips. Importing under a new name (`CharacterV2`) finally landed the change — that is what unblocked the pose after a dozen failed rebuilds.
|
||||
- **The review runner copied stale screenshots** from `Saved/`, so a shot retired from the camera list reappeared with byte-identical content from an older run. The folder is cleared before each capture.
|
||||
- **Walking was never enabled for the stride shot**: the call sat in the shot sequencer's `BeginPlay`, which runs before the player pawn exists, so the cast silently found nothing and `speed` stayed at 0.
|
||||
|
||||
### Verified
|
||||
|
||||
- **8 review shots verify**; shots 6, 7 and 8 approved by the owner.
|
||||
- **1440p, no frame generation, VSync off**: 759 frames, avg **2.64 ms (379 fps)**, p50 2.54, p95 3.39, p99 3.76, max 4.05 ms — about 2 % over `0.1.5` and 6.3× the 60 fps floor. Series: `Docs/perf/0.2.0-walk-frametimes.csv`.
|
||||
- `-qaselftest`: walk, reset 0 cm from spawn, pause/unpause, transect 1456 samples / 0 holes, wall probe 0/35, façade probe 0/9 — all PASS.
|
||||
- 82 tool tests pass.
|
||||
|
||||
### Known limitations
|
||||
|
||||
- **The fingers splay** — the bind's finger spread carries into the pose. Deferred to `0.2.2`.
|
||||
- **The arms-down rotation was found by sweeping**, not derived: 0.55 rad puts the arms at the sides, 0.65 against the hip, 0.75 into the torso. A derived pose is a `0.2.2` job.
|
||||
- **Camera collision is implemented but unproven** — the spawn point has open space behind it, so no shot exercises the boom shortening.
|
||||
- The third-person body has no head or hand IK and no turn-in-place; it is a first playable, not a finished character.
|
||||
|
||||
## [0.1.6] — 2026-11-09
|
||||
|
||||
Retail frontage. The two retail buildings beside the hero are modelled from their own photograph instead of the generic kit, and the photograph is treated as evidence: an analysis is written before any geometry, and every dimension the image does not resolve is carried into the part report as an assumption.
|
||||
|
||||
### Added
|
||||
|
||||
- **`Tools/retail_frontage_build.py`**: 6800 Lee Drug and 6806–6820 built from the one usable reference (`Art/reference/retail-neighbours/`, Gb321, CC BY-SA 4.0). Lee Drug gets a bronze cornice, storefront glazing with mullions, framed portrait panels, a painted lettering band, two-tier parapet with reveals and a fluted corner tower; 6806–6820 gets a burgundy fascia, brown pilasters, white-framed windows with security bars and its own banded parapet. Built in world coordinates from the blockout bounds (Lee Drug X −33.20…−14.37, face Y −14.63, H 12.10 m; 6806–6820 X −66.08…−33.07), joined to one mesh per building with a JSON part report.
|
||||
- **`Tools/ue_import_retail_detail.py`**: imports the two meshes with complex-as-simple collision — trim decorates a solid the player already collides with and must not add snag points.
|
||||
- **`Art/blender/retail/ref-analysis.md`**: the reference analysis the `references` skill pack requires, written before modelling.
|
||||
- **`Tools/_common.py`** and `Tools/tests/test_retail_frontage.py`: the bay-layout arithmetic, extracted so it is testable without Blender (4 tests).
|
||||
- **Review shot 5**, `demo/review-demo-retail-frontage`, embedded in `Screenshots/README.md`.
|
||||
|
||||
### Changed
|
||||
|
||||
- **The generic façade kit skips the two modelled buildings** (`MODELLED` list in `ue_apply_facade_kit.py`). It applies to every `Frontage_*`/`North_*` actor, so it was pasting a second, contradicting façade language over the new trim.
|
||||
- **Retail trim stages as `RetailTrim_*`** and is spawned through the demo builder, so it is regenerable rather than hand-placed.
|
||||
- Trim stucco, white and glass slots are mapped onto the demo's look-dev materials so the walls sit in the same noise and texture family as the rest of the block; the tones sampled from the photograph (bronze, burgundy, mural, lettering) stay as authored.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`Tools/render_screenshots.sh` never captured anything.** The capture step called `./Engine/...` while every commandlet above it went through `run_cmdlet`'s `cd "$ENGINE"`. Under `set -e` the script died at exit 127 _after_ rebuilding the maps, so the PNGs silently stayed a day old and `--verify` never ran.
|
||||
- **Duplicate `RetailTrim_*` actors on every rebuild**: the demo builder's clear list did not include the new label, so each run stacked another copy. Caught by the scene audit, not by eye.
|
||||
- **Mural plates were placed 0.20 m inside the solid blockout volume** and never rendered; the inset is now faked with a proud frame and a near-flush plate.
|
||||
- **Trim detail sat 0.05 m off the wall**, under a pixel of depth at 40 m, so the wall won the depth test. Trim planes now stand 0.10–0.20 m proud.
|
||||
- **6806–6820 parapet started at 0.62 H**, leaving a 4.6 m blank band; the window storey and parapet moved up to follow the photograph.
|
||||
- **Mural panels, parapet and lettering were sized from the photograph's proportions, not the inherited defaults**: the first pass left a parapet that read as one blank white band.
|
||||
|
||||
### Verified
|
||||
|
||||
- `render_screenshots.sh --verify`: **5/5 shots OK**; scene audit **0 failures** (every mesh materialed, all spans in tolerance, no duplicate labels).
|
||||
- Owner approved `demo-retail-frontage` (second pass) 2026-11-09; all five demo shots now carry approval.
|
||||
- 82 tool tests pass.
|
||||
|
||||
### Known limitations
|
||||
|
||||
- **Wall lettering and the painted band do not read at 40 m.** A plate at 0.10 m still vanishes; this is a decal/texture problem, not geometry. Moved to `0.9.0 — Realistic buildings`.
|
||||
- **The parapet has no moulded profile** and the corner tower is a fluted cylinder with two cap rings — both need the façade system in `0.9.0`.
|
||||
- **6822 Budget is untouched**: no photograph of it exists. Deferred and blocked in `0.9.0`.
|
||||
- The only reference for these two buildings is a single street-level oblique with a bus across Lee Drug's shopfront and a car across 6806's; several vertical proportions are assumptions recorded in the part report.
|
||||
|
||||
## [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 +301,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.
|
||||
|
||||
|
||||
@@ -20,12 +20,14 @@ C++ classes in `Game/GigLine/Source/GigLine/`:
|
||||
| --- | --- | --- |
|
||||
| `AGigLineCharacter` | First-person player pawn | 1.75 m capsule, 4.5 m/s walk, Enhanced Input bindings, reset, pause, `-qaselftest` harness |
|
||||
| `AGigLineGameModeBase` | Sets game mode and HUD class | `HUDClass = AGigLineHUD` |
|
||||
| `AGigLineHUD` | HUD text overlay | `DrawText` every frame, bottom-left |
|
||||
| `AGigLineHUD` | HUD text overlay | `DrawText` every frame: ODbL attribution bottom-left, controls bottom-right, and an F3 debug readout top-left (`gigline.Debug`, on for `-perf`/`-qaselftest`) |
|
||||
| `AGigLineNPCPerson` | NPC prefab (future) | Capsule + visible MPFB body, feet on Z 0 |
|
||||
| `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`, `IA_ToggleDebug`, 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
|
||||
|
||||
@@ -47,6 +49,7 @@ Static meshes and materials in `/Game/Hollywood/Site/`:
|
||||
| `SM_HeroFacade` | El Capitan Entertainment Centre detailed façade |
|
||||
| `SM_ElCapitanTheatre`, `SM_Budget_6822`, `SM_Hollywood_6806`, `SM_Hollywood_6902`, `SM_Hollywood_6922`, `SM_LeeDrug_6800` | South block frontage (6 buildings) |
|
||||
| `SM_Madame_Tussauds`, `SM_TCL_Chinese_Theatre` | North-side buildings (within 50 m) |
|
||||
| `SM_Retail_LeeDrug`, `SM_Retail_6806` | Retail trim over 6800 and 6806–6820, modelled from their own photograph (`Tools/retail_frontage_build.py`, staged as `RetailTrim_*`). `6822 Budget` stays a bare volume: no photograph exists. |
|
||||
|
||||
### Art Pipeline
|
||||
|
||||
@@ -56,11 +59,15 @@ 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 |
|
||||
| `export_*`, `hero_facade_build`, `retail_frontage_build`, `theatre_trim_build`, `facade_kit_build` | Blender headless export (FBX); trim builders work in world coordinates |
|
||||
| `ue_import_*`, `ue_hero_facade_import`, `ue_import_retail_detail` | Unreal headless import + placement |
|
||||
| `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 |
|
||||
| `judge_screenshot.py` | Vision-model pre-check (optional) |
|
||||
| `input_emulation.py`, `input_test.py` | OS-level virtual keyboard/mouse injection for input verification |
|
||||
| `Tools/_common.py` | Pure helper shared by Blender builders, importable without Blender (bay layout) |
|
||||
| `Tools/tests/` | Stdlib `unittest` tests for the pure parts of the tools (`requirements-tools.txt`) |
|
||||
| `Geo/tools/mapbox_buildings.py` | Second height source: Mapbox `mapbox-streets-v8` building data vs our OSM heights |
|
||||
|
||||
|
||||
@@ -55,3 +55,19 @@
|
||||
- **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-8: Realistic building façades are a minor milestone, not a patch
|
||||
|
||||
- **Context**: `0.1.4` carried the first façade pass as a _patch_, while the release policy says patches contain fixes and refinements and never new scope. The `0.1.6` retail pass then showed what that costs: a façade built in a patch has no release gate of its own, so a first pass that does not yet read as the building (`Screenshots/README.md`, 0.1.6 gaps) can sit inside a "done" patch series. Realism is also the project's leading quality goal, which a patch cannot carry.
|
||||
|
||||
- **Decision**: Building realism gets its own minor milestone, `0.9.0 — Realistic buildings`, placed before the feature freeze. Its scope is the façade system (orders, profile library, storefront types, signage as decals), per-building photo-matched models, a measured material library with a texel-density rule, and LODs plus collision proxies. Patches keep fixes and refinements only. Renumbered by this decision: the former `0.9.0` freeze/beta/audit milestone becomes `0.10.0`, its patches `0.10.1`–`0.10.3`, and `1.0.0` moves to weeks 39–44.
|
||||
|
||||
- **Consequences**: Façade work now has a release gate — every building measured against its own photograph, every building with a collision proxy and a LOD, no placeholder materials — so "modelled" means verified, not merely present. The price is that realism work stops being sliced into patches, and the schedule shifts by one minor cycle. Blockouts without a photograph stay blockouts and are listed as such, rather than being dressed up to fill the milestone.
|
||||
|
||||
## 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,44 @@
|
||||
|
||||
## 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.
|
||||
|
||||
### Building realism gate (0.9.0)
|
||||
|
||||
| ID | Check | Metric |
|
||||
| --- | --- | --- |
|
||||
| BR-1 | A building with a reference photograph has a review shot compared against it | Silhouette and proportion deltas recorded per building in `Screenshots/README.md`; owner approval per shot |
|
||||
| BR-2 | Every building is either photo-matched or explicitly recorded as an unfounded blockout | List in `TODO.md`; no building in an in-between state |
|
||||
| BR-3 | Every building has a collision proxy and at least one LOD | Count of buildings missing either = 0 |
|
||||
| BR-4 | No surface carries a placeholder material | Material audit in `Tools/ue_audit_scene.py` reports 0 placeholder slots |
|
||||
| BR-5 | The realism pass does not break the floor | QS-9 still holds at 1440p, with the measured cost of the pass recorded |
|
||||
@@ -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.
|
||||
|
||||
@@ -159,12 +164,14 @@ C++ classes in `Game/GigLine/Source/GigLine/`:
|
||||
| --- | --- | --- |
|
||||
| `AGigLineCharacter` | First-person player pawn | 1.75 m capsule, 4.5 m/s walk, Enhanced Input bindings, reset, pause, `-qaselftest` harness |
|
||||
| `AGigLineGameModeBase` | Sets game mode and HUD class | `HUDClass = AGigLineHUD` |
|
||||
| `AGigLineHUD` | HUD text overlay | `DrawText` every frame, bottom-left |
|
||||
| `AGigLineHUD` | HUD text overlay | `DrawText` every frame: ODbL attribution bottom-left, controls bottom-right, and an F3 debug readout top-left (`gigline.Debug`, on for `-perf`/`-qaselftest`) |
|
||||
| `AGigLineNPCPerson` | NPC prefab (future) | Capsule + visible MPFB body, feet on Z 0 |
|
||||
| `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`, `IA_ToggleDebug`, 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
|
||||
|
||||
@@ -186,6 +193,7 @@ Static meshes and materials in `/Game/Hollywood/Site/`:
|
||||
| `SM_HeroFacade` | El Capitan Entertainment Centre detailed façade |
|
||||
| `SM_ElCapitanTheatre`, `SM_Budget_6822`, `SM_Hollywood_6806`, `SM_Hollywood_6902`, `SM_Hollywood_6922`, `SM_LeeDrug_6800` | South block frontage (6 buildings) |
|
||||
| `SM_Madame_Tussauds`, `SM_TCL_Chinese_Theatre` | North-side buildings (within 50 m) |
|
||||
| `SM_Retail_LeeDrug`, `SM_Retail_6806` | Retail trim over 6800 and 6806–6820, modelled from their own photograph (`Tools/retail_frontage_build.py`, staged as `RetailTrim_*`). `6822 Budget` stays a bare volume: no photograph exists. |
|
||||
|
||||
### Art Pipeline
|
||||
|
||||
@@ -195,11 +203,15 @@ 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 |
|
||||
| `export_*`, `hero_facade_build`, `retail_frontage_build`, `theatre_trim_build`, `facade_kit_build` | Blender headless export (FBX); trim builders work in world coordinates |
|
||||
| `ue_import_*`, `ue_hero_facade_import`, `ue_import_retail_detail` | Unreal headless import + placement |
|
||||
| `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 |
|
||||
| `judge_screenshot.py` | Vision-model pre-check (optional) |
|
||||
| `input_emulation.py`, `input_test.py` | OS-level virtual keyboard/mouse injection for input verification |
|
||||
| `Tools/_common.py` | Pure helper shared by Blender builders, importable without Blender (bay layout) |
|
||||
| `Tools/tests/` | Stdlib `unittest` tests for the pure parts of the tools (`requirements-tools.txt`) |
|
||||
| `Geo/tools/mapbox_buildings.py` | Second height source: Mapbox `mapbox-streets-v8` building data vs our OSM heights |
|
||||
|
||||
@@ -446,31 +458,67 @@ Rule: place a building only against a neighbour or street mesh, never in isolati
|
||||
|
||||
- **Consequences**: Scripts are version-controlled and repeatable. No live Blender session needed for CI.
|
||||
|
||||
## ADR-8: Realistic building façades are a minor milestone, not a patch
|
||||
|
||||
- **Context**: `0.1.4` carried the first façade pass as a _patch_, while the release policy says patches contain fixes and refinements and never new scope. The `0.1.6` retail pass then showed what that costs: a façade built in a patch has no release gate of its own, so a first pass that does not yet read as the building (`Screenshots/README.md`, 0.1.6 gaps) can sit inside a "done" patch series. Realism is also the project's leading quality goal, which a patch cannot carry.
|
||||
|
||||
- **Decision**: Building realism gets its own minor milestone, `0.9.0 — Realistic buildings`, placed before the feature freeze. Its scope is the façade system (orders, profile library, storefront types, signage as decals), per-building photo-matched models, a measured material library with a texel-density rule, and LODs plus collision proxies. Patches keep fixes and refinements only. Renumbered by this decision: the former `0.9.0` freeze/beta/audit milestone becomes `0.10.0`, its patches `0.10.1`–`0.10.3`, and `1.0.0` moves to weeks 39–44.
|
||||
|
||||
- **Consequences**: Façade work now has a release gate — every building measured against its own photograph, every building with a collision proxy and a LOD, no placeholder materials — so "modelled" means verified, not merely present. The price is that realism work stops being sliced into patches, and the schedule shifts by one minor cycle. Blockouts without a photograph stay blockouts and are listed as such, rather than being dressed up to fill the milestone.
|
||||
|
||||
## 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.
|
||||
|
||||
# Quality Requirements
|
||||
|
||||
## 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.
|
||||
|
||||
### Building realism gate (0.9.0)
|
||||
|
||||
| ID | Check | Metric |
|
||||
| --- | --- | --- |
|
||||
| BR-1 | A building with a reference photograph has a review shot compared against it | Silhouette and proportion deltas recorded per building in `Screenshots/README.md`; owner approval per shot |
|
||||
| BR-2 | Every building is either photo-matched or explicitly recorded as an unfounded blockout | List in `TODO.md`; no building in an in-between state |
|
||||
| BR-3 | Every building has a collision proxy and at least one LOD | Count of buildings missing either = 0 |
|
||||
| BR-4 | No surface carries a placeholder material | Material audit in `Tools/ue_audit_scene.py` reports 0 placeholder slots |
|
||||
| BR-5 | The realism pass does not break the floor | QS-9 still holds at 1440p, with the measured cost of the pass recorded |
|
||||
|
||||
# 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:
|
||||
|
||||
+28
-1
@@ -1,4 +1,4 @@
|
||||
# Controls (`0.1.0` walkable demo)
|
||||
# Controls
|
||||
|
||||
Keyboard and mouse. The same list is shown in-game, bottom-right of the frame (`AGigLineHUD`), and the packaged Linux build ships with these bindings.
|
||||
|
||||
@@ -12,6 +12,8 @@ Keyboard and mouse. The same list is shown in-game, bottom-right of the frame (`
|
||||
| Jump | `Space` | |
|
||||
| Reset to start | `R` | Returns to the grounded spawn point |
|
||||
| Pause / release mouse | `Escape` | Second press resumes and recaptures the mouse |
|
||||
| Swap first / third person | `V` | First person is the default; the body is hidden in that view |
|
||||
| Show / hide debug overlay | `F3` | FPS, frame time, worst frame, view mode, speed and player position |
|
||||
|
||||
## Notes
|
||||
|
||||
@@ -19,6 +21,31 @@ Keyboard and mouse. The same list is shown in-game, bottom-right of the frame (`
|
||||
- The mouse is captured on launch, so looking works without clicking first. `Escape` toggles the capture as part of pause.
|
||||
- Bindings are Enhanced Input actions in `/Game/Input` built by `Tools/ue_create_input_assets.py`; the mapping context is `IMC_Default`.
|
||||
|
||||
## Changes since `0.1.0`
|
||||
|
||||
- **`V` swaps first and third person** (`0.2.0`, `IA_ToggleCamera`). The shipped default view is still first person, as the owner decided for `0.1.0`; third person adds a collision-tested spring-arm camera and a visible, animated body. `Escape` still pauses and releases the mouse in either view.
|
||||
|
||||
## Debug overlay
|
||||
|
||||
`F3` toggles a top-left readout: smoothed **FPS** and frame time, the **worst** frame in a rolling two-second window, the active view (first or third person), player speed, a frame counter, and the player's position in metres from the project origin. The first two seconds of a session are a warm-up window and are excluded from the worst-frame figure, because shader and streaming hitches there made a healthy run read as a 400 ms stall.
|
||||
|
||||
It is **off in the built demo** — a player should not see an FPS counter over Hollywood Boulevard — and **on automatically** when the process is started with `-perf` or `-qaselftest`, so a performance capture never depends on someone pressing a key first. In a running build it can also be forced with the console variable `gigline.Debug 1`.
|
||||
|
||||
Cost: about **5 %** of frame time at 1440p in the demo (2.64 → 2.77 ms average), measured in the same self-test route. The performance release check is recorded without the overlay for that reason; the overlay run is kept beside it as `Docs/perf/0.2.0-walk-frametimes-overlay.csv`.
|
||||
|
||||
## Review and harness switches
|
||||
|
||||
These are command-line switches for the screenshot pipeline and QA runs, not player controls. They exist so a capture is reproducible rather than dependent on a human pressing keys at the right moment.
|
||||
|
||||
| Switch | Effect |
|
||||
| --- | --- |
|
||||
| `-thirdperson` | Starts the pawn in the third-person rig, so a capture shows the body without anyone pressing `V` |
|
||||
| `-walk` | Holds forward input from the first frame — what makes the stride shot possible |
|
||||
| `-qaselftest` | Runs the scripted walk / reset / pause / transect checks and quits; also enables the debug overlay |
|
||||
| `-perf` | Enables the debug overlay for a manual frame-time check |
|
||||
|
||||
The walking shot is driven per shot rather than by the switch: the shot actor enables the walk only while a tracking (`ShotFollow`) camera is being captured, and returns the pawn to spawn afterwards, so the fixed-camera shots still find it in place.
|
||||
|
||||
## Testing input without a physical keyboard
|
||||
|
||||
`Tools/input_test.py` injects OS-level keyboard/mouse events through a virtual `/dev/uinput` device (see `Tools/input_emulation.py`), e.g.:
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
|
@@ -0,0 +1,723 @@
|
||||
frame,ms
|
||||
0,2.607
|
||||
1,2.532
|
||||
2,2.324
|
||||
3,3.432
|
||||
4,2.808
|
||||
5,2.870
|
||||
6,2.367
|
||||
7,2.311
|
||||
8,2.756
|
||||
9,3.278
|
||||
10,2.418
|
||||
11,2.665
|
||||
12,2.438
|
||||
13,2.646
|
||||
14,2.461
|
||||
15,2.941
|
||||
16,2.975
|
||||
17,3.164
|
||||
18,2.766
|
||||
19,2.341
|
||||
20,2.543
|
||||
21,3.395
|
||||
22,2.772
|
||||
23,2.575
|
||||
24,2.737
|
||||
25,2.427
|
||||
26,2.538
|
||||
27,2.708
|
||||
28,3.183
|
||||
29,2.734
|
||||
30,2.425
|
||||
31,2.546
|
||||
32,2.600
|
||||
33,2.545
|
||||
34,3.705
|
||||
35,2.563
|
||||
36,2.907
|
||||
37,2.368
|
||||
38,2.470
|
||||
39,2.411
|
||||
40,3.778
|
||||
41,3.735
|
||||
42,2.292
|
||||
43,2.458
|
||||
44,2.566
|
||||
45,3.044
|
||||
46,3.449
|
||||
47,2.914
|
||||
48,3.243
|
||||
49,2.084
|
||||
50,3.872
|
||||
51,1.633
|
||||
52,3.389
|
||||
53,2.448
|
||||
54,2.474
|
||||
55,2.509
|
||||
56,2.346
|
||||
57,3.192
|
||||
58,2.762
|
||||
59,2.788
|
||||
60,2.538
|
||||
61,2.520
|
||||
62,2.517
|
||||
63,3.036
|
||||
64,3.205
|
||||
65,3.080
|
||||
66,2.514
|
||||
67,2.429
|
||||
68,2.515
|
||||
69,2.963
|
||||
70,3.555
|
||||
71,2.430
|
||||
72,2.742
|
||||
73,2.435
|
||||
74,2.526
|
||||
75,3.304
|
||||
76,3.511
|
||||
77,2.477
|
||||
78,2.472
|
||||
79,2.452
|
||||
80,2.421
|
||||
81,3.334
|
||||
82,3.286
|
||||
83,2.725
|
||||
84,2.411
|
||||
85,2.597
|
||||
86,2.440
|
||||
87,3.213
|
||||
88,3.411
|
||||
89,3.047
|
||||
90,2.365
|
||||
91,2.530
|
||||
92,2.877
|
||||
93,3.249
|
||||
94,3.186
|
||||
95,2.684
|
||||
96,2.304
|
||||
97,2.406
|
||||
98,2.948
|
||||
99,3.652
|
||||
100,2.822
|
||||
101,2.552
|
||||
102,2.575
|
||||
103,2.354
|
||||
104,2.676
|
||||
105,2.775
|
||||
106,3.865
|
||||
107,2.480
|
||||
108,2.501
|
||||
109,2.675
|
||||
110,2.724
|
||||
111,3.671
|
||||
112,2.828
|
||||
113,3.064
|
||||
114,2.153
|
||||
115,2.752
|
||||
116,2.936
|
||||
117,3.008
|
||||
118,2.932
|
||||
119,2.648
|
||||
120,2.441
|
||||
121,3.471
|
||||
122,1.885
|
||||
123,4.435
|
||||
124,2.504
|
||||
125,3.241
|
||||
126,2.813
|
||||
127,2.445
|
||||
128,3.321
|
||||
129,3.055
|
||||
130,3.222
|
||||
131,2.766
|
||||
132,2.582
|
||||
133,2.597
|
||||
134,3.637
|
||||
135,3.150
|
||||
136,3.163
|
||||
137,2.675
|
||||
138,2.708
|
||||
139,2.581
|
||||
140,3.377
|
||||
141,3.518
|
||||
142,2.565
|
||||
143,2.385
|
||||
144,2.360
|
||||
145,2.768
|
||||
146,3.172
|
||||
147,2.754
|
||||
148,2.758
|
||||
149,2.474
|
||||
150,2.995
|
||||
151,2.903
|
||||
152,3.398
|
||||
153,2.484
|
||||
154,2.894
|
||||
155,3.022
|
||||
156,2.413
|
||||
157,3.517
|
||||
158,3.370
|
||||
159,2.854
|
||||
160,2.485
|
||||
161,3.058
|
||||
162,2.473
|
||||
163,3.390
|
||||
164,3.023
|
||||
165,2.543
|
||||
166,2.629
|
||||
167,2.929
|
||||
168,2.928
|
||||
169,3.268
|
||||
170,2.622
|
||||
171,2.764
|
||||
172,2.920
|
||||
173,2.681
|
||||
174,2.551
|
||||
175,3.492
|
||||
176,2.534
|
||||
177,2.741
|
||||
178,2.949
|
||||
179,2.554
|
||||
180,2.596
|
||||
181,3.522
|
||||
182,3.207
|
||||
183,2.722
|
||||
184,2.945
|
||||
185,2.474
|
||||
186,2.934
|
||||
187,3.447
|
||||
188,2.515
|
||||
189,2.317
|
||||
190,3.059
|
||||
191,2.542
|
||||
192,3.417
|
||||
193,2.737
|
||||
194,2.456
|
||||
195,3.025
|
||||
196,2.640
|
||||
197,2.601
|
||||
198,3.567
|
||||
199,2.350
|
||||
200,2.581
|
||||
201,3.079
|
||||
202,2.514
|
||||
203,2.462
|
||||
204,3.725
|
||||
205,3.730
|
||||
206,2.670
|
||||
207,2.308
|
||||
208,2.689
|
||||
209,2.565
|
||||
210,3.479
|
||||
211,2.604
|
||||
212,3.339
|
||||
213,2.373
|
||||
214,2.678
|
||||
215,3.340
|
||||
216,2.867
|
||||
217,2.751
|
||||
218,2.747
|
||||
219,2.410
|
||||
220,2.485
|
||||
221,2.594
|
||||
222,3.602
|
||||
223,2.689
|
||||
224,2.565
|
||||
225,2.397
|
||||
226,2.562
|
||||
227,2.598
|
||||
228,3.403
|
||||
229,3.303
|
||||
230,2.775
|
||||
231,2.101
|
||||
232,2.499
|
||||
233,2.991
|
||||
234,3.059
|
||||
235,3.210
|
||||
236,2.568
|
||||
237,2.066
|
||||
238,2.498
|
||||
239,2.572
|
||||
240,3.281
|
||||
241,2.676
|
||||
242,2.640
|
||||
243,2.867
|
||||
244,2.418
|
||||
245,2.493
|
||||
246,3.126
|
||||
247,2.845
|
||||
248,2.522
|
||||
249,2.459
|
||||
250,2.524
|
||||
251,2.466
|
||||
252,3.060
|
||||
253,3.848
|
||||
254,2.516
|
||||
255,2.460
|
||||
256,2.499
|
||||
257,2.427
|
||||
258,3.463
|
||||
259,2.761
|
||||
260,2.674
|
||||
261,2.450
|
||||
262,2.491
|
||||
263,2.543
|
||||
264,2.732
|
||||
265,3.679
|
||||
266,2.325
|
||||
267,2.529
|
||||
268,2.344
|
||||
269,2.534
|
||||
270,2.691
|
||||
271,3.176
|
||||
272,2.492
|
||||
273,2.721
|
||||
274,2.587
|
||||
275,2.594
|
||||
276,2.326
|
||||
277,3.434
|
||||
278,3.514
|
||||
279,2.415
|
||||
280,2.481
|
||||
281,2.478
|
||||
282,2.555
|
||||
283,3.303
|
||||
284,3.098
|
||||
285,2.451
|
||||
286,2.552
|
||||
287,2.413
|
||||
288,2.613
|
||||
289,3.194
|
||||
290,3.191
|
||||
291,2.338
|
||||
292,2.561
|
||||
293,2.785
|
||||
294,2.240
|
||||
295,3.324
|
||||
296,2.767
|
||||
297,2.589
|
||||
298,2.608
|
||||
299,2.432
|
||||
300,2.444
|
||||
301,2.968
|
||||
302,3.792
|
||||
303,2.590
|
||||
304,2.335
|
||||
305,2.520
|
||||
306,2.435
|
||||
307,3.613
|
||||
308,2.681
|
||||
309,2.692
|
||||
310,2.329
|
||||
311,2.580
|
||||
312,2.355
|
||||
313,3.084
|
||||
314,3.026
|
||||
315,2.551
|
||||
316,2.490
|
||||
317,2.481
|
||||
318,2.553
|
||||
319,2.619
|
||||
320,3.350
|
||||
321,2.885
|
||||
322,2.576
|
||||
323,2.456
|
||||
324,2.441
|
||||
325,3.223
|
||||
326,3.031
|
||||
327,3.055
|
||||
328,2.592
|
||||
329,2.499
|
||||
330,2.354
|
||||
331,2.573
|
||||
332,3.323
|
||||
333,3.357
|
||||
334,2.241
|
||||
335,2.319
|
||||
336,2.472
|
||||
337,2.609
|
||||
338,3.565
|
||||
339,2.607
|
||||
340,2.539
|
||||
341,2.440
|
||||
342,2.483
|
||||
343,2.626
|
||||
344,3.407
|
||||
345,2.772
|
||||
346,2.491
|
||||
347,2.456
|
||||
348,2.553
|
||||
349,2.773
|
||||
350,3.239
|
||||
351,4.079
|
||||
352,2.346
|
||||
353,2.177
|
||||
354,2.562
|
||||
355,2.350
|
||||
356,3.329
|
||||
357,3.383
|
||||
358,2.441
|
||||
359,2.399
|
||||
360,2.530
|
||||
361,2.455
|
||||
362,3.219
|
||||
363,2.879
|
||||
364,2.726
|
||||
365,2.418
|
||||
366,2.511
|
||||
367,2.544
|
||||
368,3.077
|
||||
369,2.926
|
||||
370,2.571
|
||||
371,2.595
|
||||
372,2.534
|
||||
373,2.367
|
||||
374,2.671
|
||||
375,4.222
|
||||
376,2.411
|
||||
377,2.569
|
||||
378,2.493
|
||||
379,2.464
|
||||
380,2.691
|
||||
381,3.474
|
||||
382,2.697
|
||||
383,2.541
|
||||
384,2.494
|
||||
385,2.476
|
||||
386,2.473
|
||||
387,3.464
|
||||
388,3.084
|
||||
389,2.144
|
||||
390,2.503
|
||||
391,2.501
|
||||
392,2.525
|
||||
393,3.335
|
||||
394,2.919
|
||||
395,2.334
|
||||
396,2.557
|
||||
397,2.579
|
||||
398,2.567
|
||||
399,4.120
|
||||
400,3.297
|
||||
401,2.561
|
||||
402,2.428
|
||||
403,2.495
|
||||
404,2.729
|
||||
405,3.752
|
||||
406,2.783
|
||||
407,2.509
|
||||
408,2.337
|
||||
409,2.589
|
||||
410,2.556
|
||||
411,3.449
|
||||
412,2.702
|
||||
413,2.742
|
||||
414,2.384
|
||||
415,2.759
|
||||
416,2.290
|
||||
417,3.374
|
||||
418,2.934
|
||||
419,2.546
|
||||
420,2.228
|
||||
421,2.672
|
||||
422,2.369
|
||||
423,3.442
|
||||
424,3.484
|
||||
425,2.621
|
||||
426,2.279
|
||||
427,2.529
|
||||
428,2.889
|
||||
429,2.910
|
||||
430,2.981
|
||||
431,2.957
|
||||
432,2.420
|
||||
433,2.398
|
||||
434,2.310
|
||||
435,3.662
|
||||
436,2.422
|
||||
437,2.676
|
||||
438,2.557
|
||||
439,2.527
|
||||
440,2.564
|
||||
441,2.649
|
||||
442,3.168
|
||||
443,2.499
|
||||
444,2.737
|
||||
445,2.637
|
||||
446,2.509
|
||||
447,2.716
|
||||
448,3.845
|
||||
449,2.654
|
||||
450,2.536
|
||||
451,2.366
|
||||
452,2.526
|
||||
453,2.906
|
||||
454,3.101
|
||||
455,2.809
|
||||
456,2.466
|
||||
457,2.502
|
||||
458,2.767
|
||||
459,2.486
|
||||
460,3.137
|
||||
461,3.402
|
||||
462,2.187
|
||||
463,2.398
|
||||
464,2.392
|
||||
465,2.614
|
||||
466,3.371
|
||||
467,2.847
|
||||
468,2.463
|
||||
469,2.639
|
||||
470,2.288
|
||||
471,2.562
|
||||
472,3.381
|
||||
473,3.491
|
||||
474,2.521
|
||||
475,2.397
|
||||
476,2.431
|
||||
477,2.759
|
||||
478,3.222
|
||||
479,2.957
|
||||
480,2.470
|
||||
481,2.403
|
||||
482,2.560
|
||||
483,2.470
|
||||
484,3.451
|
||||
485,2.905
|
||||
486,2.583
|
||||
487,2.329
|
||||
488,2.798
|
||||
489,2.289
|
||||
490,2.882
|
||||
491,2.903
|
||||
492,2.627
|
||||
493,2.897
|
||||
494,2.321
|
||||
495,2.472
|
||||
496,2.882
|
||||
497,3.844
|
||||
498,3.096
|
||||
499,2.525
|
||||
500,2.250
|
||||
501,2.477
|
||||
502,2.728
|
||||
503,3.391
|
||||
504,2.539
|
||||
505,2.635
|
||||
506,2.542
|
||||
507,2.435
|
||||
508,3.011
|
||||
509,3.042
|
||||
510,2.965
|
||||
511,2.273
|
||||
512,2.433
|
||||
513,3.020
|
||||
514,2.776
|
||||
515,3.646
|
||||
516,2.856
|
||||
517,2.886
|
||||
518,2.522
|
||||
519,2.528
|
||||
520,4.396
|
||||
521,3.397
|
||||
522,2.218
|
||||
523,2.433
|
||||
524,2.605
|
||||
525,3.504
|
||||
526,3.087
|
||||
527,3.186
|
||||
528,2.343
|
||||
529,2.433
|
||||
530,2.900
|
||||
531,3.000
|
||||
532,3.511
|
||||
533,2.663
|
||||
534,2.699
|
||||
535,2.423
|
||||
536,3.136
|
||||
537,2.802
|
||||
538,3.151
|
||||
539,2.507
|
||||
540,2.973
|
||||
541,2.325
|
||||
542,3.322
|
||||
543,3.260
|
||||
544,3.525
|
||||
545,2.846
|
||||
546,2.125
|
||||
547,2.697
|
||||
548,2.829
|
||||
549,3.389
|
||||
550,2.758
|
||||
551,2.866
|
||||
552,2.338
|
||||
553,3.115
|
||||
554,2.500
|
||||
555,3.387
|
||||
556,2.851
|
||||
557,2.511
|
||||
558,2.459
|
||||
559,3.107
|
||||
560,2.521
|
||||
561,3.308
|
||||
562,2.782
|
||||
563,2.810
|
||||
564,2.323
|
||||
565,3.259
|
||||
566,2.366
|
||||
567,3.542
|
||||
568,2.929
|
||||
569,2.299
|
||||
570,2.336
|
||||
571,3.027
|
||||
572,2.540
|
||||
573,3.409
|
||||
574,2.784
|
||||
575,2.444
|
||||
576,2.437
|
||||
577,2.734
|
||||
578,2.591
|
||||
579,3.845
|
||||
580,3.212
|
||||
581,2.802
|
||||
582,2.370
|
||||
583,2.704
|
||||
584,2.624
|
||||
585,4.023
|
||||
586,2.360
|
||||
587,2.759
|
||||
588,2.511
|
||||
589,2.608
|
||||
590,2.922
|
||||
591,3.081
|
||||
592,3.271
|
||||
593,2.471
|
||||
594,2.516
|
||||
595,2.474
|
||||
596,2.814
|
||||
597,3.127
|
||||
598,2.537
|
||||
599,3.080
|
||||
600,2.341
|
||||
601,3.027
|
||||
602,2.766
|
||||
603,2.984
|
||||
604,2.450
|
||||
605,2.922
|
||||
606,2.481
|
||||
607,2.616
|
||||
608,2.541
|
||||
609,3.462
|
||||
610,2.971
|
||||
611,2.955
|
||||
612,2.258
|
||||
613,2.682
|
||||
614,2.770
|
||||
615,3.119
|
||||
616,2.792
|
||||
617,2.598
|
||||
618,2.260
|
||||
619,2.441
|
||||
620,2.536
|
||||
621,3.425
|
||||
622,2.958
|
||||
623,2.722
|
||||
624,2.099
|
||||
625,2.400
|
||||
626,2.570
|
||||
627,3.295
|
||||
628,2.919
|
||||
629,2.737
|
||||
630,2.311
|
||||
631,2.408
|
||||
632,2.514
|
||||
633,3.325
|
||||
634,2.767
|
||||
635,2.724
|
||||
636,2.288
|
||||
637,2.680
|
||||
638,2.347
|
||||
639,2.902
|
||||
640,3.058
|
||||
641,2.770
|
||||
642,2.522
|
||||
643,2.406
|
||||
644,2.539
|
||||
645,2.753
|
||||
646,3.215
|
||||
647,2.636
|
||||
648,2.639
|
||||
649,2.666
|
||||
650,2.392
|
||||
651,2.408
|
||||
652,3.515
|
||||
653,2.687
|
||||
654,2.589
|
||||
655,2.428
|
||||
656,2.375
|
||||
657,2.619
|
||||
658,3.308
|
||||
659,2.664
|
||||
660,2.752
|
||||
661,2.456
|
||||
662,2.365
|
||||
663,2.533
|
||||
664,3.354
|
||||
665,2.812
|
||||
666,2.684
|
||||
667,2.349
|
||||
668,2.441
|
||||
669,2.714
|
||||
670,2.769
|
||||
671,3.368
|
||||
672,2.624
|
||||
673,2.308
|
||||
674,2.365
|
||||
675,2.634
|
||||
676,2.617
|
||||
677,3.350
|
||||
678,2.733
|
||||
679,2.623
|
||||
680,2.462
|
||||
681,2.494
|
||||
682,2.460
|
||||
683,3.463
|
||||
684,2.537
|
||||
685,2.628
|
||||
686,2.674
|
||||
687,2.381
|
||||
688,2.516
|
||||
689,3.586
|
||||
690,2.681
|
||||
691,2.676
|
||||
692,2.289
|
||||
693,2.726
|
||||
694,2.303
|
||||
695,2.934
|
||||
696,3.339
|
||||
697,2.606
|
||||
698,2.187
|
||||
699,2.687
|
||||
700,2.328
|
||||
701,3.062
|
||||
702,2.930
|
||||
703,2.871
|
||||
704,2.313
|
||||
705,2.582
|
||||
706,2.464
|
||||
707,2.521
|
||||
708,3.304
|
||||
709,2.492
|
||||
710,2.723
|
||||
711,2.587
|
||||
712,2.608
|
||||
713,2.574
|
||||
714,4.002
|
||||
715,3.011
|
||||
716,2.227
|
||||
717,2.374
|
||||
718,2.343
|
||||
719,2.541
|
||||
720,3.580
|
||||
721,2.854
|
||||
|
@@ -0,0 +1,760 @@
|
||||
frame,ms
|
||||
0,2.428
|
||||
1,2.633
|
||||
2,2.238
|
||||
3,3.606
|
||||
4,2.431
|
||||
5,2.649
|
||||
6,4.050
|
||||
7,2.011
|
||||
8,2.735
|
||||
9,3.390
|
||||
10,2.901
|
||||
11,2.237
|
||||
12,2.686
|
||||
13,2.383
|
||||
14,2.451
|
||||
15,3.412
|
||||
16,2.458
|
||||
17,2.474
|
||||
18,2.588
|
||||
19,2.556
|
||||
20,2.478
|
||||
21,3.150
|
||||
22,2.667
|
||||
23,2.349
|
||||
24,2.568
|
||||
25,2.580
|
||||
26,2.393
|
||||
27,2.575
|
||||
28,3.132
|
||||
29,2.536
|
||||
30,2.508
|
||||
31,2.710
|
||||
32,2.348
|
||||
33,2.537
|
||||
34,2.820
|
||||
35,3.162
|
||||
36,2.163
|
||||
37,2.562
|
||||
38,2.619
|
||||
39,2.323
|
||||
40,2.628
|
||||
41,3.075
|
||||
42,2.512
|
||||
43,2.448
|
||||
44,2.505
|
||||
45,2.454
|
||||
46,2.607
|
||||
47,3.128
|
||||
48,2.476
|
||||
49,2.562
|
||||
50,2.434
|
||||
51,3.385
|
||||
52,2.076
|
||||
53,3.435
|
||||
54,2.672
|
||||
55,2.550
|
||||
56,1.998
|
||||
57,2.583
|
||||
58,2.518
|
||||
59,2.777
|
||||
60,3.920
|
||||
61,2.934
|
||||
62,2.675
|
||||
63,2.320
|
||||
64,2.309
|
||||
65,2.520
|
||||
66,3.947
|
||||
67,2.596
|
||||
68,2.708
|
||||
69,2.291
|
||||
70,2.404
|
||||
71,2.551
|
||||
72,3.715
|
||||
73,2.100
|
||||
74,2.539
|
||||
75,2.682
|
||||
76,2.273
|
||||
77,2.442
|
||||
78,3.952
|
||||
79,2.233
|
||||
80,2.374
|
||||
81,2.686
|
||||
82,2.407
|
||||
83,2.654
|
||||
84,3.613
|
||||
85,2.320
|
||||
86,2.685
|
||||
87,2.744
|
||||
88,2.357
|
||||
89,2.565
|
||||
90,3.756
|
||||
91,2.672
|
||||
92,2.128
|
||||
93,2.583
|
||||
94,2.546
|
||||
95,2.391
|
||||
96,2.935
|
||||
97,3.624
|
||||
98,1.934
|
||||
99,2.509
|
||||
100,2.603
|
||||
101,2.449
|
||||
102,2.802
|
||||
103,3.678
|
||||
104,2.336
|
||||
105,2.509
|
||||
106,2.551
|
||||
107,2.576
|
||||
108,2.997
|
||||
109,3.208
|
||||
110,2.301
|
||||
111,2.359
|
||||
112,2.484
|
||||
113,2.646
|
||||
114,2.801
|
||||
115,3.226
|
||||
116,2.635
|
||||
117,2.325
|
||||
118,2.501
|
||||
119,2.602
|
||||
120,2.564
|
||||
121,3.151
|
||||
122,3.149
|
||||
123,2.235
|
||||
124,2.596
|
||||
125,2.432
|
||||
126,2.665
|
||||
127,2.648
|
||||
128,3.884
|
||||
129,2.087
|
||||
130,2.375
|
||||
131,2.586
|
||||
132,2.734
|
||||
133,2.338
|
||||
134,3.617
|
||||
135,2.418
|
||||
136,2.350
|
||||
137,2.713
|
||||
138,2.758
|
||||
139,2.239
|
||||
140,3.585
|
||||
141,2.565
|
||||
142,2.787
|
||||
143,2.115
|
||||
144,2.435
|
||||
145,2.489
|
||||
146,3.062
|
||||
147,2.958
|
||||
148,2.402
|
||||
149,2.425
|
||||
150,2.516
|
||||
151,2.505
|
||||
152,2.543
|
||||
153,3.690
|
||||
154,2.060
|
||||
155,2.510
|
||||
156,2.428
|
||||
157,2.603
|
||||
158,2.491
|
||||
159,3.178
|
||||
160,2.496
|
||||
161,2.424
|
||||
162,2.495
|
||||
163,2.667
|
||||
164,2.434
|
||||
165,3.040
|
||||
166,2.949
|
||||
167,2.094
|
||||
168,2.510
|
||||
169,2.586
|
||||
170,2.395
|
||||
171,3.004
|
||||
172,3.009
|
||||
173,2.288
|
||||
174,2.548
|
||||
175,2.337
|
||||
176,2.598
|
||||
177,2.498
|
||||
178,3.526
|
||||
179,2.533
|
||||
180,2.134
|
||||
181,2.560
|
||||
182,2.541
|
||||
183,2.436
|
||||
184,2.635
|
||||
185,3.361
|
||||
186,2.261
|
||||
187,2.566
|
||||
188,2.383
|
||||
189,2.529
|
||||
190,2.645
|
||||
191,3.190
|
||||
192,2.523
|
||||
193,2.523
|
||||
194,2.346
|
||||
195,2.865
|
||||
196,2.348
|
||||
197,2.879
|
||||
198,3.657
|
||||
199,1.824
|
||||
200,2.551
|
||||
201,2.449
|
||||
202,2.607
|
||||
203,3.085
|
||||
204,3.163
|
||||
205,2.404
|
||||
206,2.327
|
||||
207,2.553
|
||||
208,2.576
|
||||
209,2.544
|
||||
210,3.193
|
||||
211,2.647
|
||||
212,2.332
|
||||
213,2.544
|
||||
214,2.767
|
||||
215,2.429
|
||||
216,3.103
|
||||
217,2.861
|
||||
218,2.318
|
||||
219,2.552
|
||||
220,2.587
|
||||
221,2.352
|
||||
222,3.128
|
||||
223,2.621
|
||||
224,2.461
|
||||
225,2.422
|
||||
226,2.601
|
||||
227,2.748
|
||||
228,2.545
|
||||
229,3.224
|
||||
230,2.206
|
||||
231,2.459
|
||||
232,2.530
|
||||
233,2.461
|
||||
234,2.570
|
||||
235,3.271
|
||||
236,2.405
|
||||
237,2.424
|
||||
238,2.671
|
||||
239,2.450
|
||||
240,2.644
|
||||
241,2.510
|
||||
242,3.023
|
||||
243,2.461
|
||||
244,2.618
|
||||
245,2.350
|
||||
246,2.551
|
||||
247,2.472
|
||||
248,3.642
|
||||
249,2.576
|
||||
250,2.548
|
||||
251,3.599
|
||||
252,1.992
|
||||
253,2.424
|
||||
254,3.752
|
||||
255,2.241
|
||||
256,2.360
|
||||
257,2.522
|
||||
258,2.584
|
||||
259,2.653
|
||||
260,2.871
|
||||
261,2.827
|
||||
262,2.369
|
||||
263,2.482
|
||||
264,2.599
|
||||
265,2.294
|
||||
266,2.686
|
||||
267,3.428
|
||||
268,2.240
|
||||
269,2.435
|
||||
270,2.468
|
||||
271,2.619
|
||||
272,2.519
|
||||
273,3.247
|
||||
274,2.539
|
||||
275,2.442
|
||||
276,2.518
|
||||
277,2.489
|
||||
278,2.463
|
||||
279,2.839
|
||||
280,2.907
|
||||
281,2.786
|
||||
282,2.211
|
||||
283,2.615
|
||||
284,2.378
|
||||
285,2.671
|
||||
286,3.219
|
||||
287,2.435
|
||||
288,2.509
|
||||
289,2.323
|
||||
290,2.502
|
||||
291,2.526
|
||||
292,3.292
|
||||
293,2.468
|
||||
294,2.526
|
||||
295,2.500
|
||||
296,2.431
|
||||
297,2.481
|
||||
298,2.921
|
||||
299,3.009
|
||||
300,2.415
|
||||
301,2.600
|
||||
302,2.344
|
||||
303,2.533
|
||||
304,2.618
|
||||
305,3.458
|
||||
306,2.455
|
||||
307,2.469
|
||||
308,2.495
|
||||
309,2.568
|
||||
310,2.473
|
||||
311,3.179
|
||||
312,2.694
|
||||
313,2.503
|
||||
314,2.587
|
||||
315,2.317
|
||||
316,2.609
|
||||
317,2.590
|
||||
318,3.211
|
||||
319,2.392
|
||||
320,2.490
|
||||
321,2.515
|
||||
322,2.496
|
||||
323,2.483
|
||||
324,3.157
|
||||
325,2.540
|
||||
326,2.422
|
||||
327,2.574
|
||||
328,2.468
|
||||
329,2.547
|
||||
330,2.788
|
||||
331,3.075
|
||||
332,2.231
|
||||
333,2.537
|
||||
334,2.577
|
||||
335,2.584
|
||||
336,2.473
|
||||
337,3.415
|
||||
338,2.126
|
||||
339,2.506
|
||||
340,2.374
|
||||
341,2.721
|
||||
342,2.418
|
||||
343,3.259
|
||||
344,2.509
|
||||
345,2.519
|
||||
346,2.436
|
||||
347,2.472
|
||||
348,2.504
|
||||
349,2.858
|
||||
350,3.250
|
||||
351,2.161
|
||||
352,2.586
|
||||
353,2.376
|
||||
354,2.651
|
||||
355,2.504
|
||||
356,3.399
|
||||
357,2.353
|
||||
358,2.381
|
||||
359,2.513
|
||||
360,2.633
|
||||
361,2.566
|
||||
362,3.615
|
||||
363,2.593
|
||||
364,2.084
|
||||
365,2.411
|
||||
366,2.653
|
||||
367,2.521
|
||||
368,2.825
|
||||
369,2.891
|
||||
370,2.576
|
||||
371,2.302
|
||||
372,2.582
|
||||
373,2.513
|
||||
374,2.598
|
||||
375,3.790
|
||||
376,2.352
|
||||
377,2.306
|
||||
378,2.434
|
||||
379,2.514
|
||||
380,2.708
|
||||
381,2.978
|
||||
382,2.813
|
||||
383,2.327
|
||||
384,2.502
|
||||
385,2.446
|
||||
386,2.605
|
||||
387,3.033
|
||||
388,3.091
|
||||
389,2.206
|
||||
390,2.648
|
||||
391,2.732
|
||||
392,2.296
|
||||
393,3.406
|
||||
394,2.490
|
||||
395,2.424
|
||||
396,2.443
|
||||
397,2.601
|
||||
398,2.628
|
||||
399,2.477
|
||||
400,3.241
|
||||
401,2.573
|
||||
402,2.422
|
||||
403,2.384
|
||||
404,2.505
|
||||
405,2.575
|
||||
406,2.909
|
||||
407,2.897
|
||||
408,2.486
|
||||
409,2.431
|
||||
410,2.691
|
||||
411,2.342
|
||||
412,2.733
|
||||
413,3.518
|
||||
414,2.391
|
||||
415,2.160
|
||||
416,2.671
|
||||
417,2.505
|
||||
418,2.241
|
||||
419,3.355
|
||||
420,2.775
|
||||
421,2.395
|
||||
422,2.864
|
||||
423,2.290
|
||||
424,2.469
|
||||
425,2.977
|
||||
426,2.759
|
||||
427,2.460
|
||||
428,2.456
|
||||
429,2.497
|
||||
430,2.362
|
||||
431,2.874
|
||||
432,2.812
|
||||
433,2.559
|
||||
434,2.283
|
||||
435,2.670
|
||||
436,2.482
|
||||
437,2.495
|
||||
438,3.118
|
||||
439,3.018
|
||||
440,2.252
|
||||
441,3.222
|
||||
442,2.483
|
||||
443,2.436
|
||||
444,3.209
|
||||
445,3.169
|
||||
446,2.455
|
||||
447,2.432
|
||||
448,2.464
|
||||
449,2.500
|
||||
450,2.967
|
||||
451,2.958
|
||||
452,2.390
|
||||
453,2.494
|
||||
454,2.523
|
||||
455,2.473
|
||||
456,2.516
|
||||
457,3.527
|
||||
458,2.262
|
||||
459,2.377
|
||||
460,2.840
|
||||
461,2.135
|
||||
462,2.520
|
||||
463,3.285
|
||||
464,2.940
|
||||
465,2.256
|
||||
466,2.496
|
||||
467,2.705
|
||||
468,2.395
|
||||
469,3.031
|
||||
470,3.104
|
||||
471,2.411
|
||||
472,2.630
|
||||
473,2.468
|
||||
474,2.416
|
||||
475,2.559
|
||||
476,2.732
|
||||
477,2.441
|
||||
478,2.624
|
||||
479,2.536
|
||||
480,2.458
|
||||
481,2.516
|
||||
482,2.507
|
||||
483,2.606
|
||||
484,2.616
|
||||
485,2.881
|
||||
486,2.358
|
||||
487,2.469
|
||||
488,2.513
|
||||
489,3.250
|
||||
490,2.606
|
||||
491,2.545
|
||||
492,2.407
|
||||
493,2.437
|
||||
494,2.663
|
||||
495,3.346
|
||||
496,2.349
|
||||
497,2.481
|
||||
498,2.613
|
||||
499,2.483
|
||||
500,2.417
|
||||
501,2.923
|
||||
502,3.059
|
||||
503,2.483
|
||||
504,2.373
|
||||
505,2.525
|
||||
506,2.530
|
||||
507,2.431
|
||||
508,2.917
|
||||
509,2.559
|
||||
510,2.623
|
||||
511,3.172
|
||||
512,2.304
|
||||
513,2.331
|
||||
514,3.287
|
||||
515,2.544
|
||||
516,2.429
|
||||
517,2.690
|
||||
518,2.464
|
||||
519,2.406
|
||||
520,2.804
|
||||
521,2.899
|
||||
522,2.461
|
||||
523,2.536
|
||||
524,2.414
|
||||
525,2.380
|
||||
526,2.616
|
||||
527,3.236
|
||||
528,2.499
|
||||
529,2.558
|
||||
530,2.584
|
||||
531,2.309
|
||||
532,2.447
|
||||
533,3.039
|
||||
534,3.133
|
||||
535,2.170
|
||||
536,2.583
|
||||
537,2.505
|
||||
538,2.666
|
||||
539,2.567
|
||||
540,3.307
|
||||
541,2.177
|
||||
542,2.495
|
||||
543,2.441
|
||||
544,2.638
|
||||
545,2.521
|
||||
546,3.527
|
||||
547,2.460
|
||||
548,2.362
|
||||
549,2.520
|
||||
550,2.439
|
||||
551,2.572
|
||||
552,3.087
|
||||
553,2.707
|
||||
554,2.475
|
||||
555,2.283
|
||||
556,2.604
|
||||
557,2.529
|
||||
558,3.094
|
||||
559,3.051
|
||||
560,2.085
|
||||
561,2.365
|
||||
562,2.711
|
||||
563,2.351
|
||||
564,2.572
|
||||
565,2.979
|
||||
566,2.805
|
||||
567,2.373
|
||||
568,2.744
|
||||
569,2.340
|
||||
570,2.285
|
||||
571,2.791
|
||||
572,3.447
|
||||
573,2.208
|
||||
574,2.518
|
||||
575,2.448
|
||||
576,3.917
|
||||
577,1.540
|
||||
578,2.923
|
||||
579,2.632
|
||||
580,2.404
|
||||
581,2.582
|
||||
582,2.508
|
||||
583,2.413
|
||||
584,3.602
|
||||
585,2.342
|
||||
586,2.482
|
||||
587,2.584
|
||||
588,3.062
|
||||
589,2.070
|
||||
590,2.726
|
||||
591,2.937
|
||||
592,2.385
|
||||
593,2.734
|
||||
594,2.188
|
||||
595,2.582
|
||||
596,2.487
|
||||
597,3.339
|
||||
598,2.535
|
||||
599,2.414
|
||||
600,2.459
|
||||
601,2.510
|
||||
602,2.547
|
||||
603,3.414
|
||||
604,2.314
|
||||
605,2.399
|
||||
606,2.550
|
||||
607,2.541
|
||||
608,2.355
|
||||
609,2.709
|
||||
610,3.067
|
||||
611,2.502
|
||||
612,2.467
|
||||
613,2.899
|
||||
614,2.177
|
||||
615,2.719
|
||||
616,3.049
|
||||
617,2.673
|
||||
618,2.259
|
||||
619,2.566
|
||||
620,2.383
|
||||
621,2.420
|
||||
622,3.318
|
||||
623,2.807
|
||||
624,2.348
|
||||
625,2.595
|
||||
626,2.433
|
||||
627,2.569
|
||||
628,2.620
|
||||
629,2.652
|
||||
630,3.140
|
||||
631,2.945
|
||||
632,2.571
|
||||
633,2.546
|
||||
634,2.428
|
||||
635,3.436
|
||||
636,2.393
|
||||
637,2.491
|
||||
638,2.511
|
||||
639,2.485
|
||||
640,2.603
|
||||
641,3.363
|
||||
642,2.442
|
||||
643,2.344
|
||||
644,2.462
|
||||
645,2.508
|
||||
646,2.617
|
||||
647,2.909
|
||||
648,2.824
|
||||
649,2.528
|
||||
650,2.424
|
||||
651,2.511
|
||||
652,2.517
|
||||
653,2.583
|
||||
654,3.331
|
||||
655,2.436
|
||||
656,2.455
|
||||
657,2.380
|
||||
658,2.570
|
||||
659,2.582
|
||||
660,2.789
|
||||
661,2.917
|
||||
662,2.526
|
||||
663,2.538
|
||||
664,2.447
|
||||
665,2.423
|
||||
666,2.695
|
||||
667,3.143
|
||||
668,2.616
|
||||
669,2.126
|
||||
670,2.596
|
||||
671,2.464
|
||||
672,3.092
|
||||
673,2.681
|
||||
674,2.641
|
||||
675,2.311
|
||||
676,2.551
|
||||
677,2.551
|
||||
678,2.837
|
||||
679,2.532
|
||||
680,3.006
|
||||
681,2.289
|
||||
682,2.521
|
||||
683,2.556
|
||||
684,2.593
|
||||
685,2.552
|
||||
686,3.173
|
||||
687,2.415
|
||||
688,2.429
|
||||
689,2.733
|
||||
690,2.286
|
||||
691,2.476
|
||||
692,3.256
|
||||
693,2.897
|
||||
694,2.528
|
||||
695,2.377
|
||||
696,2.518
|
||||
697,2.504
|
||||
698,3.074
|
||||
699,2.814
|
||||
700,2.433
|
||||
701,2.338
|
||||
702,2.476
|
||||
703,2.458
|
||||
704,3.141
|
||||
705,2.551
|
||||
706,2.717
|
||||
707,2.300
|
||||
708,2.527
|
||||
709,2.446
|
||||
710,2.469
|
||||
711,2.866
|
||||
712,2.776
|
||||
713,2.507
|
||||
714,2.457
|
||||
715,2.520
|
||||
716,2.450
|
||||
717,2.568
|
||||
718,3.192
|
||||
719,2.471
|
||||
720,2.790
|
||||
721,2.337
|
||||
722,2.548
|
||||
723,2.435
|
||||
724,3.313
|
||||
725,2.274
|
||||
726,2.461
|
||||
727,2.451
|
||||
728,2.556
|
||||
729,2.546
|
||||
730,2.869
|
||||
731,3.308
|
||||
732,2.293
|
||||
733,2.428
|
||||
734,2.686
|
||||
735,2.562
|
||||
736,2.523
|
||||
737,3.309
|
||||
738,2.311
|
||||
739,2.498
|
||||
740,2.485
|
||||
741,2.568
|
||||
742,2.392
|
||||
743,3.260
|
||||
744,2.534
|
||||
745,2.550
|
||||
746,2.355
|
||||
747,2.768
|
||||
748,2.319
|
||||
749,3.039
|
||||
750,2.721
|
||||
751,2.490
|
||||
752,2.378
|
||||
753,2.539
|
||||
754,2.823
|
||||
755,2.480
|
||||
756,3.533
|
||||
757,2.285
|
||||
758,2.365
|
||||
|
+33
-13
@@ -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 |
|
||||
@@ -40,8 +42,10 @@ Measured at 1440p on the development workstation: Bazzite, Ryzen 7 7700X, 64 GB
|
||||
| `0.1.0` | Placement vs references | PASS (with deliberate simplifications) | Section below, 2026-11-08 | 2026-11-08 |
|
||||
| `0.1.0` | Collision and packaging | PASS — no blocking collision, no invisible walls, no seam gaps (see audit note) | `-qaselftest` on the packaged build, 2026-11-08 | 2026-11-08 |
|
||||
| `0.1.0` | 60 FPS at 1440p | PASS — 2.42 ms avg (414 fps), p99 3.01 ms at 2560×1440, TSR on, VSync off, no FG | `-qaselftest` perf capture, `Docs/perf/0.1.0-walk-frametimes.csv` | 2026-11-08 |
|
||||
| `0.2.0` | 60 FPS at 1440p | PASS — 2.64 ms avg (379 fps), p50 2.54, p95 3.39, p99 3.76, max 4.05 ms at 2560×1440, TSR on, VSync off, no FG | `-qaselftest` perf capture, `Docs/perf/0.2.0-walk-frametimes.csv` | 2026-11-09 |
|
||||
| `0.2.0` | Debug overlay cost | PASS — same route with the F3 overlay drawn: 2.77 ms avg (361 fps), p50 2.64, p95 3.52, p99 3.87, max 4.43 ms; about **5 %** heavier, still 6× the floor. The overlay is on for `-perf`/`-qaselftest`, off in the built demo | `Docs/perf/0.2.0-walk-frametimes-overlay.csv` | 2026-11-09 |
|
||||
| `0.1.0` | Fresh checkout build | PASS — clean clone of `main` (`4bc41e9`), `git lfs pull` + `check-lfs.sh` OK (130 LFS files), both `GigLineEditor` and `GigLine` targets built with the documented engine | `/tmp/fresh-check`, 2026-11-08 | 2026-11-08 |
|
||||
| `0.1.0` | Packaged Linux build | PASS — packaged Development build launches straight into DemoMap, no missing-asset/linker errors; controls shown in-game and in `Docs/controls.md` | `Docs/perf/0.1.0-packaged-hud-controls.png`, 2026-11-08 | 2026-11-08 |
|
||||
| `0.1.0` | Packaged Linux build | PASS — packaged Development build launches straight into DemoMap, no missing-asset/linker errors; controls shown in-game and in `Docs/controls.md` | measurement recorded 2026-11-08; the capture was removed from the repository 2026-11-09 | 2026-11-08 |
|
||||
|
||||
## `0.1.0` — Placement vs references — record (2026-11-08)
|
||||
|
||||
@@ -125,6 +129,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:
|
||||
@@ -157,6 +177,6 @@ Engine/Build/BatchFiles/Linux/Build.sh GigLine Linux Development \
|
||||
|
||||
- `Docs/controls.md` — full binding table (WASD, mouse look, Space, R, Esc) plus how to test input headlessly.
|
||||
- `AGigLineHUD` now draws a control legend bottom-right every frame: "WASD move · Mouse look · Space jump · R reset · Esc pause".
|
||||
- Verified in the packaged capture: the bottom-right text bbox is 426×12 px (a single right-aligned line), attributed text bottom-left. Screenshot: `Docs/perf/0.1.0-packaged-hud-controls.png`.
|
||||
- Measured in the packaged build on 2026-11-08: the bottom-right text bbox was 426×12 px (a single right-aligned line), attributed text bottom-left. The capture has since been removed from the repository, so the numbers stand as the record.
|
||||
|
||||
**Verdict: PASS.**
|
||||
@@ -1,80 +0,0 @@
|
||||
# `0.1.4` façade review — signoff
|
||||
|
||||
Four demo-map captures from the `facades-0.1.4` branch, after the module kit, the textured materials and the theatre marquee went in. **All seven items signed off 2026-11-09** (owner review of the local renders).
|
||||
|
||||
Reference photographs to compare against: `Art/reference/hollywood/` and `Art/reference/el-capitan-*` (local, not committed). Regenerate the set with `Tools/render_screenshots.sh --verify`; measure it with `python3 Tools/visual_loop.py --compare --iteration 5`.
|
||||
|
||||
## Signoff
|
||||
|
||||
- [x] **1. Block from the north sidewalk** — hero between its neighbours, sidewalk and road read correctly
|
||||
- [x] **2. Street level** — colonnade, shopfronts and window rhythm hold up at eye height
|
||||
- [x] **3. Crossing** — zebra bars, kerb line and the frontage behind
|
||||
- [x] **4. North buildings** — TCL and Tussaud's framed across the boulevard
|
||||
- [x] **5. Materials** — stone reads as coursed stone, glazing as glass, not flat colour
|
||||
- [x] **6. Theatre trim** — marquee and blade sign proportions believable at street level
|
||||
- [x] **7. Approve** `v0.1.4` (kit + materials + theatre trim); remaining geometry moves to the next version
|
||||
|
||||
## 1. Block from the north sidewalk
|
||||
|
||||

|
||||
|
||||
Expected: the neoclassical hero façade between its neighbours, both sidewalks and the road in the foreground. _Metric: 0.68 similarity, edges 0.28× reference._
|
||||
|
||||
## 2. Street level
|
||||
|
||||

|
||||
|
||||
Expected: the hero colonnade with adjacent frontage buildings either side, shopfronts at pavement level, 4.96 m bay rhythm. _Metric: 0.33, edges 0.27×._
|
||||
|
||||
## 3. The designated crossing
|
||||
|
||||

|
||||
|
||||
Expected: continental zebra bars on the asphalt at s=130 m, south frontage and colonnade behind. _Metric: 0.42, edges 0.34×._
|
||||
|
||||
## 4. North-side buildings
|
||||
|
||||

|
||||
|
||||
Expected: TCL Chinese Theatre and Madame Tussaud's across Hollywood Blvd, full height in frame. _Metric: 0.34, edges 0.14×._
|
||||
|
||||
---
|
||||
|
||||
## Appendix — what was checked, and what was broken
|
||||
|
||||
### Verified
|
||||
|
||||
- **Behaviour**, packaged build, `-qaselftest`: walk 761 cm in 2 s (Z steady 456), reset 0 cm, pause/unpause, seam transect 1456 samples / **0 holes**, wall probe **0/35**, façade occlusion probe **0/9** — all PASS. Run twice: the harness is time-based and one loaded run reported a false failure (`walk 246 cm`, `pause FAIL`), an immediate re-run passed — re-run before believing a single FAIL.
|
||||
- **Placement**, probed from the live map against `Geo/verification/2026-11-06-el-capitan-block.md`: 6922 −24276…−17700, 6902 −17749…−14591, **hero −14644…−12028 between them**, theatre −12082…−8210, 6822 / 6806 / 6800 −7854…−1437, TCL −22889…−18442, Tussaud's −27071…−22799. Blockout affine residual **0.0 cm**.
|
||||
- **Budgets**: hero 148 parts / 3 044 tris · kit 16 meshes × 12 tris · 625 kit actors ≈ 7 500 tris · 8 × 512² textures (196 KB) · 67 tool tests pass.
|
||||
- **Topology**: every kit piece manifold, no loose geometry.
|
||||
|
||||
### Defects found this cycle
|
||||
|
||||
Every one of these rendered convincingly; the placement ones were caught by coordinate probes, not by looking.
|
||||
|
||||
| Sev | Defect | Root cause |
|
||||
| --- | --- | --- |
|
||||
| Blocker | Frontage shifted ≈119 m X / 2.4 m Y, hero outside its gap | Blockout regenerated without `--origin` |
|
||||
| Blocker | Kit placed ~12 km from its buildings | Used mesh-local bbox; actors carry the offset |
|
||||
| Blocker | Marquee on the far side of the boulevard | Blender +Y is Unreal −Y |
|
||||
| Major | Marquee canopy 5 cm above the pavement | Absolute Z; pavement is 4.56 m above datum |
|
||||
| Major | Window bays invisible, inside the solid wall | Piece modelled entirely at +Y |
|
||||
| Major | Glazing wasn't glass | Grey `M_Kit_*` from the FBX import, never overridden |
|
||||
| Major | Hero windows floated 1.44 m in front of the wall | Wrong Y in the builder |
|
||||
| Major | Parapet medallions were discs on the coping | Built with the Z-axis cylinder helper |
|
||||
| Major | Hero had no usable collision | Complex-only; traces use simple |
|
||||
| Minor | "No smoothing group information" on every import | `mesh_smooth_type="OFF"` in six exporters |
|
||||
| Minor | `save_map()` returned False silently | LFS `.umap` read-only, importer never chmod'd |
|
||||
| Minor | North-buildings import silently no-op | Missing `import sys` |
|
||||
| Minor | Kit folder held 24 assets (8 orphan materials) | No stale-asset sweep |
|
||||
| Minor | Edge-density metric counted PIL's border ring | `FIND_EDGES` leaves a 128 ring |
|
||||
| Minor | QA wall probe grazed the theatre wall | Scan row 1 cm from the building line |
|
||||
|
||||
### Limitations worth knowing
|
||||
|
||||
- Kit pieces are boxes; dentil spacing, abacus and plinth dimensions are recorded assumptions, not measurements.
|
||||
- Textures are generated, not authored — no decals, signage text or weathering.
|
||||
- The vision pre-check stays advisory: it has rejected shots the owner approved.
|
||||
- The low edge ratios in the metrics are partly structural — a wide block view against a tight façade crop always scores low. Weigh the images, not the scores.
|
||||
- Performance has not been re-measured since `0.1.0`; the 625 kit actors are unmeasured (tracked in `TODO.md` under `0.1.5`).
|
||||
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.
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,10 @@
|
||||
#include "GigLineCharacter.h"
|
||||
|
||||
#include "Animation/AnimSequence.h"
|
||||
#include "Animation/AnimSingleNodeInstance.h"
|
||||
#include "Camera/CameraComponent.h"
|
||||
#include "Components/SkeletalMeshComponent.h"
|
||||
#include "GameFramework/SpringArmComponent.h"
|
||||
#include "Components/CapsuleComponent.h"
|
||||
#include "EnhancedInputComponent.h"
|
||||
#include "EnhancedInputSubsystems.h"
|
||||
@@ -11,6 +15,7 @@
|
||||
#include "Engine/World.h"
|
||||
#include "TimerManager.h"
|
||||
#include "Containers/Ticker.h"
|
||||
#include "HAL/IConsoleManager.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "Misc/CommandLine.h"
|
||||
#include "Misc/Parse.h"
|
||||
@@ -28,7 +33,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 +56,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/CharacterV3.CharacterV3"));
|
||||
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/CharacterV3Human_rig_GigLine_Idle.CharacterV3Human_rig_GigLine_Idle"));
|
||||
if (IdleFinder.Succeeded())
|
||||
{
|
||||
IdleAnimation = IdleFinder.Object;
|
||||
}
|
||||
static ConstructorHelpers::FObjectFinder<UAnimSequence> WalkFinder(
|
||||
TEXT("/Game/Test/Character/CharacterV3Human_rig_GigLine_Walk.CharacterV3Human_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 +133,10 @@ 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> DebugF(
|
||||
TEXT("/Game/Input/IA_ToggleDebug.IA_ToggleDebug"));
|
||||
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 +148,8 @@ AGigLineCharacter::AGigLineCharacter()
|
||||
LookAction = LookF.Object;
|
||||
JumpAction = JumpF.Object;
|
||||
ResetAction = ResetF.Object;
|
||||
ToggleCameraAction = CamF.Object;
|
||||
ToggleDebugAction = DebugF.Object;
|
||||
PauseAction = PauseF.Object;
|
||||
|
||||
if (!MappingContext || !MoveAction || !MoveBackAction || !StrafeAction || !StrafeLeftAction
|
||||
@@ -138,6 +199,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.
|
||||
@@ -178,6 +256,51 @@ void AGigLineCharacter::BeginPlay()
|
||||
{
|
||||
StartQASelfTest();
|
||||
}
|
||||
|
||||
// `-walk` holds forward input from the first frame. The screenshot run uses it
|
||||
// with `-thirdperson` so a capture can show the walk clip mid-stride: an idle
|
||||
// shot cannot prove the animation set, and a still cannot show a stride.
|
||||
bWalkForever = FParse::Param(FCommandLine::Get(), TEXT("walk"));
|
||||
|
||||
// `-perf` starts the overlay on: the performance capture and any manual frame
|
||||
// check should not depend on someone pressing F3 first. The built demo has
|
||||
// neither switch, so the counter stays hidden for players.
|
||||
if (FParse::Param(FCommandLine::Get(), TEXT("perf")) || FParse::Param(FCommandLine::Get(), TEXT("qaselftest")))
|
||||
{
|
||||
if (IConsoleVariable* Var = IConsoleManager::Get().FindConsoleVariable(TEXT("gigline.Debug")))
|
||||
{
|
||||
Var->Set(1, ECVF_SetByConsole);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AGigLineCharacter::ToggleDebugOverlay()
|
||||
{
|
||||
static IConsoleVariable* Var = IConsoleManager::Get().FindConsoleVariable(TEXT("gigline.Debug"));
|
||||
if (Var)
|
||||
{
|
||||
const bool bNowOn = Var->GetInt() == 0;
|
||||
Var->Set(bNowOn ? 1 : 0, ECVF_SetByConsole);
|
||||
UE_LOG(LogTemp, Log, TEXT("GigLine: debug overlay %s"), bNowOn ? TEXT("on") : TEXT("off"));
|
||||
}
|
||||
}
|
||||
|
||||
void AGigLineCharacter::SetReviewWalk(bool bEnable)
|
||||
{
|
||||
bWalkForever = bEnable;
|
||||
}
|
||||
|
||||
void AGigLineCharacter::ReturnToStartForReview()
|
||||
{
|
||||
ResetToStart();
|
||||
}
|
||||
|
||||
void AGigLineCharacter::WalkForever()
|
||||
{
|
||||
if (bWalkForever && GetCharacterMovement() && GetCharacterMovement()->IsMovingOnGround())
|
||||
{
|
||||
AddMovementInput(GetActorForwardVector(), 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void AGigLineCharacter::StartQASelfTest()
|
||||
@@ -557,6 +680,14 @@ void AGigLineCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputCo
|
||||
{
|
||||
Input->BindAction(PauseAction, ETriggerEvent::Started, this, &AGigLineCharacter::TogglePause);
|
||||
}
|
||||
if (ToggleCameraAction)
|
||||
{
|
||||
Input->BindAction(ToggleCameraAction, ETriggerEvent::Started, this, &AGigLineCharacter::ToggleCamera);
|
||||
}
|
||||
if (ToggleDebugAction)
|
||||
{
|
||||
Input->BindAction(ToggleDebugAction, ETriggerEvent::Started, this, &AGigLineCharacter::ToggleDebugOverlay);
|
||||
}
|
||||
if (JumpAction)
|
||||
{
|
||||
Input->BindAction(JumpAction, ETriggerEvent::Started, this, &AGigLineCharacter::StartJump);
|
||||
@@ -644,6 +775,87 @@ 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.
|
||||
WalkForever();
|
||||
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)
|
||||
{
|
||||
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 || GetMesh()->GetSingleNodeInstance() == nullptr)
|
||||
{
|
||||
// Only the mode setter is idempotent; calling SetAnimation unconditionally
|
||||
// restarts the clip every frame, which freezes the body on frame 0 - that is
|
||||
// why the walking capture showed a still figure with both legs together.
|
||||
if (GetMesh()->GetAnimationMode() != EAnimationMode::AnimationSingleNode)
|
||||
{
|
||||
GetMesh()->SetAnimationMode(EAnimationMode::AnimationSingleNode);
|
||||
}
|
||||
if (Wanted != PlayingAnimation)
|
||||
{
|
||||
GetMesh()->SetAnimation(Wanted);
|
||||
PlayingAnimation = Wanted;
|
||||
GetMesh()->Play(true);
|
||||
if (CVarGigLineSetupLogs.GetValueOnGameThread())
|
||||
{
|
||||
UE_LOG(LogTemp, Log, TEXT("GigLine: body clip %s, mode=%d"),
|
||||
*Wanted->GetName(), static_cast<int32>(GetMesh()->GetAnimationMode()));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Same clip but the instance was recreated after a mode change: resume it
|
||||
// once rather than re-setting the animation.
|
||||
GetMesh()->Play(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,34 @@ protected:
|
||||
void ResetToStart();
|
||||
void TogglePause();
|
||||
|
||||
public:
|
||||
/** True while the third-person rig is the active view (for the debug overlay). */
|
||||
bool IsThirdPerson() const { return bThirdPerson; }
|
||||
|
||||
/** Review harness: hold or release forward input from the screenshot actor. */
|
||||
void SetReviewWalk(bool bEnable);
|
||||
|
||||
/** Review harness: put the pawn back where it spawned, after a walking capture. */
|
||||
void ReturnToStartForReview();
|
||||
|
||||
protected:
|
||||
/** 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 +140,52 @@ 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;
|
||||
|
||||
/** True while `-walk` holds forward input for the screenshot run. */
|
||||
bool bWalkForever = false;
|
||||
|
||||
/** Applies the `-walk` forward input when the flag is set and the pawn is grounded. */
|
||||
void WalkForever();
|
||||
|
||||
/** Debug-overlay action (F3). */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
|
||||
TObjectPtr<UInputAction> ToggleDebugAction;
|
||||
|
||||
/** Flips the gigline.Debug console variable that the HUD reads. */
|
||||
void ToggleDebugOverlay();
|
||||
|
||||
/** 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;
|
||||
|
||||
@@ -2,11 +2,61 @@
|
||||
|
||||
#include "Engine/Canvas.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "GigLineCharacter.h"
|
||||
#include "HAL/IConsoleManager.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
|
||||
// Console variable so a packaged build can be checked without a rebuild:
|
||||
// gigline.Debug 1
|
||||
// The HUD reads it every frame; the F3 binding toggles it.
|
||||
static TAutoConsoleVariable<int32> CVarGigLineDebug(
|
||||
TEXT("gigline.Debug"),
|
||||
0,
|
||||
TEXT("Draw the performance and state overlay (0=off, 1=on). F3 toggles it."),
|
||||
ECVF_Cheat);
|
||||
|
||||
void AGigLineHUD::UpdateFrameStats(float DeltaSeconds)
|
||||
{
|
||||
if (DeltaSeconds <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
++FramesDrawn;
|
||||
const float Ms = DeltaSeconds * 1000.0f;
|
||||
if (WarmupSeconds > 0.0f)
|
||||
{
|
||||
WarmupSeconds -= DeltaSeconds;
|
||||
SmoothedFrameMs = FMath::Lerp(SmoothedFrameMs, Ms, 0.1f);
|
||||
WorstFrameMs = Ms;
|
||||
WorstWindowSeconds = 0.0f;
|
||||
return;
|
||||
}
|
||||
// A 0.9/0.1 EMA settles in a few frames and still reacts to a stall.
|
||||
SmoothedFrameMs = FMath::Lerp(SmoothedFrameMs, Ms, 0.1f);
|
||||
WorstFrameMs = FMath::Max(WorstFrameMs, Ms);
|
||||
// Rolling worst-frame window: a spike stays visible for two seconds, so a
|
||||
// hitch is not missed between glances (and not averaged away).
|
||||
WorstWindowSeconds += DeltaSeconds;
|
||||
if (WorstWindowSeconds >= 2.0f)
|
||||
{
|
||||
WorstWindowSeconds = 0.0f;
|
||||
WorstFrameMs = Ms;
|
||||
}
|
||||
}
|
||||
|
||||
void AGigLineHUD::DrawHUD()
|
||||
{
|
||||
Super::DrawHUD();
|
||||
|
||||
// Console variable is authoritative; the UPROPERTY is the per-instance default
|
||||
// for tests that set it before BeginPlay.
|
||||
if (IConsoleVariable* Var = IConsoleManager::Get().FindConsoleVariable(TEXT("gigline.Debug")))
|
||||
{
|
||||
bShowDebug = Var->GetInt() != 0;
|
||||
}
|
||||
UpdateFrameStats(GetWorld() ? GetWorld()->GetDeltaSeconds() : 0.0f);
|
||||
|
||||
if (!Canvas || AttributionLine.IsEmpty())
|
||||
{
|
||||
return;
|
||||
@@ -37,4 +87,35 @@ void AGigLineHUD::DrawHUD()
|
||||
DrawText(ControlsLine, Shadow, CX + 1.0f, CY + 1.0f, nullptr, TextScale * 0.82f);
|
||||
DrawText(ControlsLine, Ink, CX, CY, nullptr, TextScale * 0.82f);
|
||||
}
|
||||
|
||||
if (!bShowDebug)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Performance and state readout, top-left. Everything here is cheap enough to
|
||||
// leave on during a capture: four formatted lines and no additional passes.
|
||||
const APawn* Pawn = UGameplayStatics::GetPlayerPawn(this, 0);
|
||||
const AGigLineCharacter* Character = Cast<AGigLineCharacter>(Pawn);
|
||||
const float Fps = SmoothedFrameMs > 0.0f ? 1000.0f / SmoothedFrameMs : 0.0f;
|
||||
const FVector Where = Pawn ? Pawn->GetActorLocation() / 100.0f : FVector::ZeroVector;
|
||||
const float Speed = Pawn ? Pawn->GetVelocity().Size2D() / 100.0f : 0.0f;
|
||||
|
||||
const FString Header = FString::Printf(TEXT("%.0f FPS %.2f ms worst %.2f ms"), Fps, SmoothedFrameMs, WorstFrameMs);
|
||||
const FString State = FString::Printf(TEXT("%s %.2f m/s frames %lld"),
|
||||
Character && Character->IsThirdPerson() ? TEXT("third person") : TEXT("first person"),
|
||||
Speed, FramesDrawn);
|
||||
const FString Place = FString::Printf(TEXT("x %.1f y %.1f z %.1f m"), Where.X, Where.Y, Where.Z);
|
||||
const FString Hint = TEXT("F3 hide");
|
||||
|
||||
const FLinearColor DebugInk(0.85f, 1.0f, 0.55f, 0.95f);
|
||||
const FLinearColor DebugShadow(0.0f, 0.0f, 0.0f, 0.6f);
|
||||
const float DX = MarginPx;
|
||||
float DY = MarginPx;
|
||||
for (const FString& Line : { Header, State, Place, Hint })
|
||||
{
|
||||
DrawText(Line, DebugShadow, DX + 1.0f, DY + 1.0f, nullptr, TextScale * 0.9f);
|
||||
DrawText(Line, DebugInk, DX, DY, nullptr, TextScale * 0.9f);
|
||||
DY += 22.0f * TextScale * 0.9f;
|
||||
}
|
||||
}
|
||||
@@ -34,12 +34,25 @@ public:
|
||||
* in-game"). Keep in sync with Docs/controls.md.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Controls")
|
||||
FString ControlsLine = TEXT("WASD move \u00B7 Mouse look \u00B7 Space jump \u00B7 R reset \u00B7 Esc pause");
|
||||
FString ControlsLine = TEXT("WASD move \u00B7 Mouse look \u00B7 Space jump \u00B7 V view \u00B7 R reset \u00B7 Esc pause");
|
||||
|
||||
/** Licence line, including where the licence can be read. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attribution")
|
||||
FString LicenceLine = TEXT("Data \u00A9 OpenStreetMap contributors, ODbL 1.0 \u2014 openstreetmap.org/copyright");
|
||||
|
||||
/**
|
||||
* Performance and state readout, top-left, off by default.
|
||||
*
|
||||
* Toggle in game with **F3**, or start with it on by passing `-perf` (the
|
||||
* performance capture does). The built demo ships with it hidden so a player
|
||||
* does not see an FPS counter over Hollywood Boulevard.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Debug")
|
||||
bool bShowDebug = false;
|
||||
|
||||
/** Smoothed FPS and a rolling frame-time window, refreshed every frame. */
|
||||
void UpdateFrameStats(float DeltaSeconds);
|
||||
|
||||
/** Screen-space margin in pixels. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attribution")
|
||||
float MarginPx = 18.0f;
|
||||
@@ -47,4 +60,25 @@ public:
|
||||
/** Text scale relative to the default canvas font. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attribution")
|
||||
float TextScale = 1.45f;
|
||||
|
||||
private:
|
||||
/** Exponential moving average of frame time, in milliseconds. */
|
||||
float SmoothedFrameMs = 16.7f;
|
||||
|
||||
/** Worst frame in the rolling window, so a hitch is visible without a log. */
|
||||
float WorstFrameMs = 0.0f;
|
||||
|
||||
/** Seconds until the rolling worst-frame window resets. */
|
||||
float WorstWindowSeconds = 0.0f;
|
||||
|
||||
/** Frames drawn, so the overlay can show that the counter is live. */
|
||||
int64 FramesDrawn = 0;
|
||||
|
||||
/**
|
||||
* Seconds left in the warm-up window. The first frames of a session include
|
||||
* shader and streaming hitches of several hundred milliseconds; without this
|
||||
* the "worst" figure read 400 ms for the whole first two seconds and made a
|
||||
* healthy run look broken.
|
||||
*/
|
||||
float WarmupSeconds = 2.0f;
|
||||
};
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "GigLineShotActor.h"
|
||||
|
||||
#include "GigLineCharacter.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
|
||||
#include "Camera/CameraActor.h"
|
||||
#include "Camera/CameraComponent.h"
|
||||
#include "Engine/World.h"
|
||||
@@ -32,6 +35,12 @@ void AGigLineShotActor::Tick(float DeltaSeconds)
|
||||
|
||||
if (bWaitingForSettle)
|
||||
{
|
||||
// A follow shot tracks the pawn while it settles, otherwise the subject
|
||||
// walks out of the frame between placement and capture.
|
||||
if (CurrentShot < ShotFollow.Num() && ShotFollow[CurrentShot])
|
||||
{
|
||||
UpdateFollowCamera();
|
||||
}
|
||||
SettleCountdown -= DeltaSeconds;
|
||||
if (SettleCountdown <= 0.f)
|
||||
{
|
||||
@@ -53,11 +62,58 @@ void AGigLineShotActor::Tick(float DeltaSeconds)
|
||||
{
|
||||
bWaitingForScreenshot = false;
|
||||
UE_LOG(LogTemp, Log, TEXT("GigLineShotActor: captured %s"), *FScreenshotRequest::GetFilename());
|
||||
// Walking shots move the pawn; put it back before the fixed-camera shots run.
|
||||
if (ShotFollow.IsValidIndex(CurrentShot) && ShotFollow[CurrentShot])
|
||||
{
|
||||
if (AGigLineCharacter* Character = Cast<AGigLineCharacter>(UGameplayStatics::GetPlayerPawn(this, 0)))
|
||||
{
|
||||
Character->ReturnToStartForReview();
|
||||
}
|
||||
}
|
||||
++CurrentShot;
|
||||
NextShot();
|
||||
}
|
||||
}
|
||||
|
||||
void AGigLineShotActor::UpdateFollowCamera()
|
||||
{
|
||||
if (!CurrentCamera)
|
||||
{
|
||||
return;
|
||||
}
|
||||
APawn* Pawn = UGameplayStatics::GetPlayerPawn(this, 0);
|
||||
if (!Pawn)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Enable the walk here rather than only in NextShot: the sequencer's BeginPlay
|
||||
// runs before the player pawn exists, so a one-shot cast there silently found
|
||||
// nothing and the pawn never walked during its follow shot.
|
||||
if (AGigLineCharacter* Character = Cast<AGigLineCharacter>(Pawn))
|
||||
{
|
||||
Character->SetReviewWalk(true);
|
||||
// Park the walk clip at a quarter cycle the first frame of the shot, so the
|
||||
// capture lands mid-stride. Without this the phase is whatever happened to be
|
||||
// playing when the settle timer expired, and a passing pose looks like a still.
|
||||
if (!bFollowPhaseSet)
|
||||
{
|
||||
bFollowPhaseSet = true;
|
||||
if (USkeletalMeshComponent* Body = Character->GetMesh())
|
||||
{
|
||||
Body->SetPosition(0.25f, /*bFireNotifies=*/false);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Offset in the pawn's frame: to its right, slightly above, looking at the
|
||||
// pelvis. Right-of-pawn gives a profile view, which is where a stride reads.
|
||||
const FVector Right = Pawn->GetActorRightVector();
|
||||
const FVector Origin = Pawn->GetActorLocation();
|
||||
const FVector CameraLocation = Origin + Right * 420.f + FVector(0.f, 0.f, 90.f);
|
||||
const FVector Target = Origin + FVector(0.f, 0.f, 40.f);
|
||||
CurrentCamera->SetActorLocation(CameraLocation);
|
||||
CurrentCamera->SetActorRotation((Target - CameraLocation).Rotation());
|
||||
}
|
||||
|
||||
void AGigLineShotActor::NextShot()
|
||||
{
|
||||
if (!PlayerController)
|
||||
@@ -93,6 +149,21 @@ void AGigLineShotActor::NextShot()
|
||||
Cam->SetFieldOfView(ShotFieldOfView[CurrentShot]);
|
||||
}
|
||||
}
|
||||
// Follow shots ignore their recorded transform after this point: the camera is
|
||||
// rebuilt from the pawn every frame until the capture. They also own the walking:
|
||||
// the pawn only holds forward input while a follow shot is being taken, so the
|
||||
// fixed-camera shots later in the list still find it at the spawn point.
|
||||
const bool bFollowThisShot = ShotFollow.IsValidIndex(CurrentShot) && ShotFollow[CurrentShot];
|
||||
bFollowPhaseSet = false;
|
||||
if (AGigLineCharacter* Character = Cast<AGigLineCharacter>(UGameplayStatics::GetPlayerPawn(this, 0)))
|
||||
{
|
||||
Character->SetReviewWalk(bFollowThisShot);
|
||||
}
|
||||
if (bFollowThisShot)
|
||||
{
|
||||
UpdateFollowCamera();
|
||||
}
|
||||
|
||||
PlayerController->SetViewTarget(CurrentCamera);
|
||||
|
||||
SettleCountdown = SettleSeconds;
|
||||
|
||||
@@ -50,6 +50,18 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
float SettleSeconds = 0.35f;
|
||||
|
||||
/**
|
||||
* Optional per-shot "follow the player" flag, same index as ShotLocations.
|
||||
*
|
||||
* A fixed camera cannot photograph a walking pawn: at 4.5 m/s it covers the
|
||||
* distance between two captures, so three attempts framed it at a different
|
||||
* distance each run and two missed it entirely. With this flag the camera is
|
||||
* re-placed every frame at a fixed offset in the pawn's frame, which is what
|
||||
* makes a stride shot deterministic.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
TArray<bool> ShotFollow;
|
||||
|
||||
/** Quit the game after the last shot (disable to keep the game open). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
bool bQuitWhenDone = true;
|
||||
@@ -57,6 +69,12 @@ public:
|
||||
private:
|
||||
void NextShot();
|
||||
|
||||
/** Re-places the active camera at the follow offset behind the player pawn. */
|
||||
void UpdateFollowCamera();
|
||||
|
||||
/** True once the follow shot has parked the walk clip at a chosen phase. */
|
||||
bool bFollowPhaseSet = false;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<class APlayerController> PlayerController;
|
||||
|
||||
|
||||
@@ -417,6 +417,72 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO.
|
||||
|
||||
## 2026-11-09 — 0.1.4 signed off; 0.1.5 created for the carried geometry
|
||||
|
||||
- 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`.
|
||||
- Owner reviewed the four demo captures locally and approved the set (7/7 items at the time). The signoff lived in `Docs/reviews/0.1.4-review.md`, which was removed on 2026-11-09; the per-shot `Approved` column of `Screenshots/README.md` is now the only record and is authoritative.
|
||||
- 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.
|
||||
|
||||
## 2026-11-09 — Retail frontage and two pipeline traps
|
||||
|
||||
- **`Tools/render_screenshots.sh` called `./Engine/...` for the capture step** while every commandlet above went through `run_cmdlet`'s `cd "$ENGINE"`. With `set -e` the script died at exit 127 _after_ rebuilding the maps, so the PNGs silently stayed a day old and `--verify` never ran. Fixed to `"$ENGINE/Engine/Binaries/..."`. Symptom to remember: maps rebuild, screenshots do not move.
|
||||
- **The generic façade kit is applied to every actor labelled `Frontage_*` or `North_*`**, so staging a hand-modelled façade under those labels gets the kit pasted over it — two contradicting façade languages on one wall. `ue_apply_facade_kit.py` now skips `MODELLED` labels, and retail trim stages as `RetailTrim_*`.
|
||||
- **A Blender builder cannot be imported by a unit test** (`import bpy` at module scope). Pure layout arithmetic lives in `Tools/_common.py`; `Tools/tests/test_retail_frontage.py` tests it there, and the builder delegates. Same trick `fbx_export_preset.py` uses for its preset dicts.
|
||||
- **Joining 85 parts into one mesh per building** before export cut the Unreal import from 85 assets to 2, and the staging step to two spawns. Record per-part dimensions _before_ the join: after it only the joined bounds exist.
|
||||
|
||||
## 2026-11-09 — Third-person body renders in bind pose (open defect)
|
||||
|
||||
- **Symptom**: the pawn's body is visible and skinned, the run logs `GigLine: playing body clip Character_AnimHuman_rig_GigLine_Idle`, and the figure still stands in the A-pose bind. Two captures were taken before the call was proven to succeed — the camera, then the material, were blamed first and neither was the cause.
|
||||
- **The FBX is not the problem.** Re-importing `/tmp/gigline/character-anim.fbx` into Blender gives two actions, `GigLine_Idle` (frames 1–60) and `GigLine_Walk` (1–30), each with **1379 f-curves over 137 bones**. Animation is baked and exported correctly, so the defect is on the Unreal side.
|
||||
- **The material was a real but separate defect**: `M_Character_Export` had no `SkeletalMesh` usage flag, so Unreal substituted the default grey with only a `LogMaterial` warning. Fixed in place (`used_with_skeletal_mesh = True`).
|
||||
- **`PlayAnimation` alone is not enough.** `USkeletalMeshComponent::PlayAnimation` needs the component already in `AnimationSingleNode` mode; the body is hidden at construction (`SetHiddenInGame(true)`) and only unhidden by the camera toggle, so the first call can land on a component that is not yet in the right mode and never retries — the guard `Wanted == PlayingAnimation` then returns forever and the pose stays at bind.
|
||||
- **What to log next time**: animation mode and `GetSingleNodeInstance()` together with the clip name. A clip name alone shows the intent, not the effect.
|
||||
- **Fixed 2026-11-09, two independent causes.** (1) `PlayAnimation` needs `AnimationSingleNode` plus a live single-node instance; the body is hidden at construction and only unhidden by the camera toggle, so the first call could land before the component was ready and the `Wanted == PlayingAnimation` guard then returned forever. `UpdateBodyAnimation` now sets the mode, calls `SetAnimation` + `Play`, and re-asserts while `GetSingleNodeInstance()` is null. (2) The arms-down offsets lived only in the clip keys — the exporter writes clip data **relative to the rest pose**, so they arrived in Unreal as a ±20° swing around an A-posed bind. `apply_base_pose` now bakes them into the rest pose before any clip is authored, which is what `mpfb_character_export.py --pose idle` already did.
|
||||
- **The base pose needs inverted signs when baked to rest.** Applied as written, the FBX axis mapping produced arms raised in a V; negating the table gives arms down. Both states were verified on the imported asset, not assumed.
|
||||
- **Unreal bone names use underscores**: `upperarm01_L`, not Blender's `upperarm01.L`. `GetBoneIndex(TEXT("upperarm01.L"))` returns INDEX_NONE, `GetBoneTransform` then returns identity, and a probe reading it reports a constant 0.0000 — which looks like "animation not applied" and is actually "bone not found". Check `AnimPose.get_bone_names()` before trusting any bone probe.
|
||||
- **`Log` verbosity does not reach the capture log, `Warning` does.** Two diagnostic runs produced zero lines because the messages were `UE_LOG(LogTemp, Log, ...)`; the same lines at `Warning` appeared. Instrument with Warning when the evidence has to survive into `/tmp/gigline/render-*.log`.
|
||||
|
||||
## 2026-11-09 — Unreal silently refuses to replace an imported asset
|
||||
|
||||
- **The real reason the character pose never changed across a dozen rebuilds:** the FBX importer reported success every time, but `Character_Anim` and its two AnimSequences kept their original data. Proof: after changing the arm base from 1.55 rad to 0.55 rad and rebuilding, `AnimPose.get_bone_pose("upperarm01_L")` returned the **identical** quaternion `(+0.006, +0.002, -0.179, -0.984)` as before. An asset that does not change when its source does is not being written.
|
||||
- `AssetImportTask.replace_existing = True` was set, and `EditorAssetLibrary.delete_asset` returned **False** for both clips, even after `chmod u+w` (the `.uasset` files are LFS-lockable and check out read-only). Deleting is blocked because the pawn's C++ constructor hard-references the clips.
|
||||
- **Workaround that works: import under a new name.** `Tools/ue_character_anim_import.py` now uses `PREFIX = "CharacterV2"`, which creates `CharacterV2`, `CharacterV2_Skeleton` and `CharacterV2Human_rig_GigLine_{Idle,Walk}` fresh; the same quaternion probe then returned `(+0.439, +0.898)`, i.e. the 0.55 rad arm rotation had finally landed.
|
||||
- **Check the asset, not the importer.** A pose or material change that does not alter the probed asset data did not happen, whatever the log says. Probing a clip's bone quaternion takes one commandlet and settles it in seconds.
|
||||
- **The arms-down rotation belongs in the clips, not in the bind.** Every bind experiment (bake pose to rest, clear shape keys, apply the armature modifier, re-bind) was either impossible on this shape-keyed MPFB mesh or left Unreal showing the old bind. `Tools/character_anim_build.py` now carries a measured base offset in the clip keys (`BASE`: 0.55 rad on `upperarm01`, chosen by sweeping the rotation in Blender and reading the bone direction), and the character stands with its arms at its sides.
|
||||
@@ -108,8 +108,9 @@ Solo-development estimate: **approximately 30–40 weeks**, subject to asset qua
|
||||
| `0.6.0` | 22–24 | Visual/audio pass and evening lighting preset | `0.6.1` lighting fixes; `0.6.2` material corrections; `0.6.3` audio balancing |
|
||||
| `0.7.0` | 25–27 | Settings, control rebinding, save/load, accessibility basics | `0.7.1` save fixes; `0.7.2` UI fixes; `0.7.3` input persistence fixes |
|
||||
| `0.8.0` | 28–30 | Performance budgets and broader Linux hardware testing | `0.8.1` VRAM reductions; `0.8.2` frame-time fixes; `0.8.3` compatibility fixes |
|
||||
| `0.9.0` | 31–34 | Feature freeze, public beta, license and build audit | `0.9.1` blocker fixes; `0.9.2` regression fixes; `0.9.3` documentation fixes |
|
||||
| `1.0.0` | 35–40 | Stable neighborhood-scale walking/driving release | `1.0.x` maintenance and compatibility fixes |
|
||||
| `0.9.0` | 31–34 | Realistic buildings: façade system, per-building models, measured material library, LODs and collision proxies | `0.9.1` façade fixes |
|
||||
| `0.10.0` | 35–38 | Feature freeze, public beta, license and build audit | `0.10.1` blocker fixes; `0.10.2` regression fixes; `0.10.3` documentation fixes |
|
||||
| `1.0.0` | 39–44 | Stable neighborhood-scale walking/driving release | `1.0.x` maintenance and compatibility fixes |
|
||||
|
||||
Finish each milestone's release checks before expanding scope. Cut map size and simulation complexity before compromising the core walking/driving experience.
|
||||
|
||||
|
||||
+27
-7
@@ -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), **`v0.1.6` released 2026-11-09**: retail frontage for 6800 Lee Drug and 6806–6820 modelled from their own photograph; 6822 Budget deferred (no reference) and the lettering/parapet detail moved to `0.9.0`. **`v0.2.0` released 2026-11-09**: visible animated body, third-person camera with a V toggle, idle and walk clips, and a deterministic stride capture. Two cosmetic character defects (splayed fingers, a swept-not-derived base rotation) are deferred to `0.2.2`. **Next:** `0.3.0` (Chaos vehicle, enter/exit, closed driving route).
|
||||
|
||||
| Area | State |
|
||||
| --- | --- |
|
||||
@@ -164,22 +168,38 @@ Workstation, geo pipeline, and engine validation are done. The art pipeline has
|
||||
|
||||
**Goal:** fix compatibility issues from the hardware testing matrix.
|
||||
|
||||
## 0.9.0 — Feature freeze, public beta, license and build audit (weeks 31–34)
|
||||
## 0.9.0 — Realistic buildings (weeks 31–34)
|
||||
|
||||
**Goal:** every building in the demo area reads as the building it is, at walking distance, judged against its own photograph — and where no photograph exists it stays an honest blockout rather than an invented façade. This is the milestone that turns the façade system from "a kit applied to volumes" into architecture: a shared bay-and-profile vocabulary, a material library with measured albedo and roughness, modelled storefronts and signage, and per-building reference matching with a measured tolerance.
|
||||
|
||||
**Scope:**
|
||||
|
||||
- A façade system that carries more than one order: Art Deco, neoclassical and streamline-moderne variants on the same 4.96 m bay module, with a profile library for cornices, copings, string courses and storefront surrounds.
|
||||
- Per-building façades for the six south-side buildings and the two north-side buildings inside the 50 m buffer, each from its own reference set; the hero keeps its existing model and gains the detail levels this pass defines.
|
||||
- Storefronts and signage as first-class geometry plus decals: mullions, bulkheads, recessed entries, awning and canopy brackets, painted wall lettering, blade and projecting signs. Wall lettering is a texture/decal problem, not a block of geometry — the `0.1.6` attempt proved plates do not read.
|
||||
- A material library with measured values per surface type (stucco, painted render, ashlar, brick, terrazzo, glazing, bronze and steel trim) and a texel-density rule, replacing per-building ad-hoc colours.
|
||||
- LOD chain and collision proxies per building, so realism does not cost the 60 FPS floor.
|
||||
|
||||
**Out of scope:** interiors, night lighting and audio (the `0.6.0` evening preset owns those), vehicles, pedestrians, and any change to footprints or massing — OSM stays the source for those.
|
||||
|
||||
**Release gate:** each building has a review shot compared against its own photograph with the silhouette and proportion deltas recorded in `Screenshots/README.md`; no building carries an unverified façade; every building has a collision proxy and at least one LOD; 60 FPS at 1440p holds; no surface is left with a placeholder material; `THIRD_PARTY_NOTICES.md` covers every photographic reference used.
|
||||
|
||||
## 0.10.0 — Feature freeze, public beta, license and build audit (weeks 35–38)
|
||||
|
||||
**Goal:** freeze features, produce a public beta for testers, and complete the audits: licences for `THIRD_PARTY_NOTICES.md` and all MPFB, texture, animation, vehicle, and audio assets; Epic distribution requirements; and a fresh checkout that builds and packages reproducibly.
|
||||
|
||||
## 0.9.1 — Blocker fixes (patch)
|
||||
## 0.10.1 — Blocker fixes (patch)
|
||||
|
||||
**Goal:** fix blockers reported during the public beta.
|
||||
|
||||
## 0.9.2 — Regression fixes (patch)
|
||||
## 0.10.2 — Regression fixes (patch)
|
||||
|
||||
**Goal:** fix regressions introduced by `0.9.x` fixes.
|
||||
**Goal:** fix regressions introduced by `0.10.x` fixes.
|
||||
|
||||
## 0.9.3 — Documentation fixes (patch)
|
||||
## 0.10.3 — Documentation fixes (patch)
|
||||
|
||||
**Goal:** update all `Docs/` for accuracy before `1.0.0`.
|
||||
|
||||
## 1.0.0 — Stable neighborhood-scale walking/driving release (weeks 35–40)
|
||||
## 1.0.0 — Stable neighborhood-scale walking/driving release (weeks 39–44)
|
||||
|
||||
**Goal:** a final acceptance sweep across all `0.x` features, confirmation that the agreed scope (neighbourhood-scale walking and driving) is met, and final build, documentation, and licensing verification. `1.0.0` marks the stable scope, not completion of a whole city.
|
||||
+139
-14
@@ -12,18 +12,125 @@ 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 | [Retail frontage](#5-retail-frontage) | ☑ 2026-11-09 | ☑ 2026-11-09 |
|
||||
| 6 | [Third-person body](#6-third-person-body) | ☑ 2026-11-09 | ☑ 2026-11-09 |
|
||||
|
||||
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. Retail frontage
|
||||
|
||||

|
||||
|
||||
- **File:** `demo/review-demo-retail-frontage.png`
|
||||
- **View:** north sidewalk, level horizon, 68° lens
|
||||
- **Expected content:** the retail run beside the hero — 6800 Lee Drug's bronze cornice, mural row and parapet next to 6806–6820's burgundy fascia, pilasters and window storey
|
||||
- **Verified:** ☑ 2026-11-09 · **Approved:** ☑ 2026-11-09
|
||||
|
||||
Read this one against the reference photograph it was modelled from: `Art/reference/retail-neighbours/01-lee-drug-and-neighbor.jpg` (Gb321, CC BY-SA 4.0).
|
||||
|
||||
Known gaps in this first pass, recorded rather than hidden:
|
||||
|
||||
- **The parapet still reads as a blank band.** It is now two tiers with three deeper reveals, and the step is visible, but there is no profile or shadow line to break the wall — a moulded coping and a modelled reveal need the `0.9.0` façade system.
|
||||
- **The mural row now reads** (framed plates standing 0.12–0.20 m off the wall), but **the lettering band does not**: it is a plate at 0.10 m and it disappears at 40 m. Wall lettering is a decal/texture problem, scoped in `0.9.0`; no amount of extra plate depth fixed it in the second pass.
|
||||
- **The corner tower is a plain cylinder with two cap rings** — the stepped Deco profile in the photograph is richer than that.
|
||||
- **The camera crops the east end** of Lee Drug at 68° from the north sidewalk.
|
||||
- **6822 Budget is untouched** and still a bare blockout volume: no photograph of it exists.
|
||||
|
||||
---
|
||||
|
||||
## 6. 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:** ☑ 2026-11-09
|
||||
|
||||
What this shot is evidence for:
|
||||
|
||||
- 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`.
|
||||
- **The idle clip drives the body and the arms hang at the sides**, with the hands at the hips and the feet flat. Earlier captures of this shot showed a bind A-pose.
|
||||
|
||||
Still open, tracked in `TODO.md`:
|
||||
|
||||
- The fingers splay slightly, which is the bind's finger spread carried into the pose. That is the `0.2.2` pose-and-skin tuning pass, not this rig.
|
||||
- **Camera collision is implemented but unproven:** the spawn has open space behind it, so no shot exercises the arm shortening.
|
||||
|
||||
---
|
||||
|
||||
## 7. Third-person front
|
||||
|
||||

|
||||
|
||||
- **File:** `demo/review-demo-third-person-front.png`
|
||||
- **View:** from the road, ~6 m in front of the pawn, looking back north. The pawn spawns facing +Y, so this is its front
|
||||
- **Expected content:** both arms hanging at the sides with the hands by the hips, face and chest visible
|
||||
- **Verified:** ☑ 2026-11-09 · **Approved:** ☑ 2026-11-09
|
||||
|
||||
**Why the shot exists:** the third-person shot only ever shows the pawn's back, so a pose defect on the front — arms out, hands splayed, a face pointing the wrong way — was invisible in review. This angle is the check for it.
|
||||
|
||||
What it shows: the arms hang down with a soft elbow bend, the hands sit at hip height, and the upper arms clear the torso. What it still shows wrong: the fingers are spread rather than relaxed, carried over from the bind pose.
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## The iteration loop (uncommitted)
|
||||
|
||||
@@ -40,12 +147,30 @@ 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 (columns, stairs, set-back entrance, door frames, signage) are not positioned as the photographs show | `0.9.0` → per-building models, hero façade | Open |
|
||||
| Nothing may stand in front of the hero façade that blocks it — no awning, no temporary structure | `0.9.0` → per-building models, hero façade | Open |
|
||||
| Retail parapet has no moulded profile, and wall lettering built as a plate does not read at 40 m | `0.9.0` → 6800 Lee Drug and 6806–6820, second pass | Open (moved out of `0.1.6`) |
|
||||
| 6822 Budget is a bare blockout volume — no photograph exists to model it from | `0.9.0` → photography gap | Deferred (moved out of `0.1.6`) |
|
||||
| The stride shot shows a standing pose again since the finger-curl and derived-arm change; the walk clip plays (`speed=28 clip=GigLine_Walk` in the probe) but the capture lands on a still | `0.2.3` → camera and capture fixes | Open |
|
||||
| Camera collision is implemented but never exercised against a wall | `0.2.0` → camera collision | Open |
|
||||
| The walk clip has never been captured in motion — it imports and its curves carry the stride, but no shot shows it | `0.2.0` → verify animation plays correctly | Open |
|
||||
| Between kerb and building line the walkable surface is the terrain mesh, not a modelled pavement | `0.6.0` → model the pavement between kerb and building line | 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 from this list on 2026-11-09:
|
||||
|
||||
- "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: every surface carries a look-dev material built in `Tools/ue_build_demo_map.py`.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -9,51 +9,6 @@ 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)
|
||||
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
## 0.2.0 — MPFB player body, third-person camera, basic animation (weeks 7–9)
|
||||
|
||||
### Character setup
|
||||
|
||||
- [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.
|
||||
|
||||
### Camera and controls
|
||||
|
||||
- [ ] Add third-person camera following the player body.
|
||||
- [ ] Add first/third-person toggle.
|
||||
- [ ] Add camera collision to prevent clipping through geometry.
|
||||
- [ ] Document any input changes.
|
||||
|
||||
### Release checks
|
||||
|
||||
- [ ] Third-person traversal works end-to-end in the demo area.
|
||||
- [ ] No blocking animation or rig defects.
|
||||
- [ ] Performance target still met at 1440p.
|
||||
- [ ] Tag `v0.2.0` and write `CHANGELOG.md` entry; then remove this section.
|
||||
|
||||
## 0.2.1 — Rig fixes (patch)
|
||||
|
||||
### Patch tasks
|
||||
|
||||
- [ ] Fix skeletal/rigging defects reported after `0.2.0`.
|
||||
- [ ] Tag `v0.2.1` and write `CHANGELOG.md` entry; then remove this section.
|
||||
|
||||
## 0.2.2 — Skin and hair tuning (patch)
|
||||
|
||||
### Patch tasks
|
||||
@@ -186,6 +141,7 @@ The `0.1.4` kit, materials and theatre trim are signed off; these are the geomet
|
||||
|
||||
- [ ] Create an evening lighting preset.
|
||||
- [ ] Complete a visual polish pass across the neighborhood.
|
||||
- [ ] Model the pavement between kerb and building line; today that strip is the terrain mesh, which reads as bare ground under the shopfronts (`Screenshots/README.md`). Carried over 2026-11-09.
|
||||
- [ ] Add ambient audio (city ambience, traffic, basic SFX).
|
||||
- [ ] Verify licenses for all added audio files.
|
||||
|
||||
@@ -289,7 +245,49 @@ The `0.1.4` kit, materials and theatre trim are signed off; these are the geomet
|
||||
- [ ] Fix compatibility issues from the hardware testing matrix.
|
||||
- [ ] Tag `v0.8.3` and write `CHANGELOG.md` entry; then remove this section.
|
||||
|
||||
## 0.9.0 — Feature freeze, public beta, license and build audit (weeks 31–34)
|
||||
## 0.9.0 — Realistic buildings (weeks 31–34)
|
||||
|
||||
Suggested blender skills: **archviz** (real-world scale, façade composition, elevation read), **references** (reference-image-match and visual-match checklists — every building is judged against its own photograph), **procedural-modeling** + **geometry-nodes** (the façades system: bay module, cornice and coping profiles, storefront types as reusable node groups), **materials** (measured albedo and roughness per surface type), **texture-workflow** (baked AO/curvature/normal, decals for painted lettering and signs), **uv-workflow** (texel density and modular UVs), **lod-pipeline** (LOD chain per building), **collision-proxy** (UCX proxies so detail geometry is not the collider), **lookdev** (screenshot comparison loop), **camera-cinematography** (reproducing each building's reference angle), **asset-optimization** + **unreal-export** (pre-export validation, `ue_metric_v1`), **qa-review** (the gate before every review shot).
|
||||
|
||||
### Façade system
|
||||
|
||||
- [ ] **Extend the module kit beyond one order.** Art Deco, neoclassical and streamline-moderne variants on the shared 4.96 m bay: alternate window proportions, spandrel and transom bands, and the way each order terminates at the roof.
|
||||
- [ ] **Build a profile library** for cornices, copings, string courses, sills and storefront surrounds, so a building is assembled from named profiles rather than ad-hoc boxes.
|
||||
- [ ] **Storefront types**: recessed entry, bulkhead and kickplate, mullion spacing, transom, awning/canopy brackets, and grille/shutter variants.
|
||||
- [ ] **Signage and lettering as decals.** Painted wall lettering, blade signs and fascia panels become texture and decal work with a documented projection; `0.1.6` proved that lettering built as geometry plates cannot resolve at review distance.
|
||||
- [ ] Document the system in `Art/blender/README.md` and the arc42 building block view.
|
||||
|
||||
### Per-building models
|
||||
|
||||
Each building needs its own reference set; where none exists the blockout stays and the item says so.
|
||||
|
||||
- [ ] Hero façade (6840): raise to this pass's detail level — capitals and fluting, window rhythm behind the colonnade, parapet motto band and medallions.
|
||||
- [ ] Hero façade (6840), entrance: **doors at street level left and right of the central bay**, as the photographs show, and correct positioning of the central-bay elements (column bases, entrance stairs, set-back entrance, door frames, banners and signage). Nothing may stand in front of the façade that blocks it — no awning, no temporary structure. Carried over from `Screenshots/README.md`, 2026-11-09.
|
||||
- [ ] El Capitan Theatre (6838): marquee, blade sign, entrance and the theatre's own elevation from its 18-frame set.
|
||||
- [ ] 6800 Lee Drug and 6806–6820: second pass on the `0.1.6` trim. Moved here from that patch because geometry cannot close them: **wall lettering as a decal** (a plate at 0.10 m still vanishes at 40 m), a **moulded parapet profile** instead of a two-tier box, richer **corner-tower stepping**, and the portrait panels reworked as textures rather than framed plates.
|
||||
- [ ] **Photography gap — 6822 Budget** (deferred out of `0.1.6`, blocked): no photograph exists; the single-storey 6.0 × 5.5 m volume cannot be read from the neighbour shot, so it stays a bare blockout until references are acquired. Record the sources checked in `Geo/`. Also re-shoot 6806–6820 and 6800 from the south sidewalk: the only reference is one oblique with a bus across Lee Drug's shopfront and a car across 6806's.
|
||||
- [ ] 6902, 6922 and the remaining south frontage.
|
||||
- [ ] North side: TCL Chinese Theatre and Madame Tussaud's façades from their reference sets.
|
||||
- [ ] Reconcile every modelled height against `Geo/tools/block_height_report.py` and record the source per building.
|
||||
|
||||
### Materials, UVs and performance
|
||||
|
||||
- [ ] **Measured material library**, including the hero's stone, which currently renders pale because it is a procedural graph and a generated texture set rather than a measured surface: stucco, painted render, ashlar, brick, terrazzo, glazing, bronze and steel trim, with albedo and roughness taken from the photographs rather than guessed; one Unreal material instance per surface type.
|
||||
- [ ] **Texel-density rule** per building class, applied through modular UVs; lightmap UVs where required.
|
||||
- [ ] **LOD chain and collision proxies** per building: UCX proxies for blocking volume, detail geometry non-colliding.
|
||||
- [ ] Re-validate the 60 FPS floor at 1440p once the detail lands, and record the cost of the pass.
|
||||
|
||||
### Release checks
|
||||
|
||||
- [ ] Every building with a photograph has a review shot measured against it, with silhouette and proportion deltas recorded in `Screenshots/README.md`.
|
||||
- [ ] No building carries an unverified façade; blockouts without references are listed as such in `TODO.md`.
|
||||
- [ ] Every building has a collision proxy and at least one LOD.
|
||||
- [ ] No surface left on a placeholder material.
|
||||
- [ ] `THIRD_PARTY_NOTICES.md` covers every photographic reference used in this pass.
|
||||
- [ ] Performance target still met at 1440p.
|
||||
- [ ] Tag `v0.9.0` and write `CHANGELOG.md` entry; then remove this section.
|
||||
|
||||
## 0.10.0 — Feature freeze, public beta, license and build audit (weeks 35–38)
|
||||
|
||||
### Freeze and audit
|
||||
|
||||
@@ -301,29 +299,29 @@ The `0.1.4` kit, materials and theatre trim are signed off; these are the geomet
|
||||
|
||||
### Release checks
|
||||
|
||||
- [ ] Beta feedback triaged; blockers logged for `0.9.x`.
|
||||
- [ ] Tag `v0.9.0` and write `CHANGELOG.md` entry; then remove this section.
|
||||
- [ ] Beta feedback triaged; blockers logged for `0.10.x`.
|
||||
- [ ] Tag `v0.10.0` and write `CHANGELOG.md` entry; then remove this section.
|
||||
|
||||
## 0.9.1 — Blocker fixes (patch)
|
||||
## 0.10.1 — Blocker fixes (patch)
|
||||
|
||||
### Patch tasks
|
||||
|
||||
- [ ] Fix blockers reported during the public beta.
|
||||
- [ ] Tag `v0.9.1` and write `CHANGELOG.md` entry; then remove this section.
|
||||
- [ ] Tag `v0.10.1` and write `CHANGELOG.md` entry; then remove this section.
|
||||
|
||||
## 0.9.2 — Regression fixes (patch)
|
||||
## 0.10.2 — Regression fixes (patch)
|
||||
|
||||
### Patch tasks
|
||||
|
||||
- [ ] Fix regressions introduced by `0.9.x` fixes.
|
||||
- [ ] Tag `v0.9.2` and write `CHANGELOG.md` entry; then remove this section.
|
||||
- [ ] Tag `v0.10.2` and write `CHANGELOG.md` entry; then remove this section.
|
||||
|
||||
## 0.9.3 — Documentation fixes (patch)
|
||||
## 0.10.3 — Documentation fixes (patch)
|
||||
|
||||
### Patch tasks
|
||||
|
||||
- [ ] Update all `Docs/` for accuracy before `1.0.0`.
|
||||
- [ ] Tag `v0.9.3` and write `CHANGELOG.md` entry; then remove this section.
|
||||
- [ ] Tag `v0.10.3` and write `CHANGELOG.md` entry; then remove this section.
|
||||
|
||||
## 1.0.0 — Stable neighborhood-scale walking/driving release (weeks 35–40)
|
||||
|
||||
|
||||
@@ -17,11 +17,16 @@ Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side b
|
||||
| --- | --- |
|
||||
| `check-lfs.sh` | Fails if the `lfs` filter is missing or an LFS-tracked file was committed as a plain blob |
|
||||
| `facade_kit_build.py` | Builds the reusable façade **module kit** at the measured 4.96 m bay: pilaster, sill, lintel, window bay (surround + glazing), shopfront bay (piers, glass, fascia), door bay (jambs, door, threshold), cornice and parapet + coping. Exports each piece with the shared preset plus a JSON part report (dims + evidence per piece). |
|
||||
| `ue_import_retail_detail.py` | Imports the two retail trim meshes into `/Game/Hollywood/Frontage` with complex-as-simple collision — the trim decorates a solid the player already collides with, so it must not add snag points. Staged by the demo builder as `RetailTrim_*`. |
|
||||
| `ue_import_facade_kit.py` | Imports the kit into `/Game/Hollywood/Kit/*` as static meshes with complex-as-simple collision; writes `/tmp/gigline/kit-import.txt`. |
|
||||
| `retail_frontage_build.py` | Retail frontage trim for **6800 Lee Drug** and **6806–6820** (0.1.6): bronze cornice, mural row, lettering band, stepped Deco parapet and rounded corner tower on Lee Drug; burgundy fascia, pilasters, white-framed windows and banded parapet on 6806–6820. Built in world coordinates from the blockout bounds, one joined mesh per building, JSON part report with the evidence behind every piece. `6822 Budget` is **not** built — no photograph exists. |
|
||||
| `retail_frontage_build.py` | Retail frontage trim for **6800 Lee Drug** and **6806–6820** (0.1.6): bronze cornice, mural row, lettering band, stepped Deco parapet and rounded corner tower on Lee Drug; burgundy fascia, pilasters, white-framed windows and banded parapet on 6806–6820. Built in world coordinates from the blockout bounds, one joined mesh per building, JSON part report with the evidence behind every piece. `6822 Budget` is **not** built — no photograph exists. |
|
||||
| `theatre_trim_build.py` | El Capitan Theatre trim: projecting marquee canopy, illuminated fascia, recessed soffit, vertical blade sign with frame and bracket. Built **in world coordinates** (span and pavement height baked in) with a JSON part report, so staging is a single spawn at the origin. |
|
||||
| `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 +45,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 +57,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,27 @@
|
||||
"""Shared helpers that must stay importable outside Blender and Unreal.
|
||||
|
||||
Blender-side builders import `bpy` at module scope, so nothing in them can be unit
|
||||
tested. Pure layout arithmetic lives here instead: the builders import it, and
|
||||
`Tools/tests/` imports it without a 3D application.
|
||||
|
||||
Standard library only.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def bay_centres(x0, x1, margin=0.9, bay=2.2):
|
||||
"""Centres of equal bays across a frontage, inset by `margin` at each end.
|
||||
|
||||
Used by the façade kit and the retail frontage so both use one rhythm. The count
|
||||
is the span divided by the bay module and rounded, so a 33 m frontage at a 2.2 m
|
||||
module gets 14 bays rather than one bay per 2.2 m of wall with a stub at the end.
|
||||
|
||||
Returns [] when the margins consume the whole frontage.
|
||||
"""
|
||||
span = (x1 - x0) - 2.0 * margin
|
||||
if span <= 0:
|
||||
return []
|
||||
count = max(1, int(round(span / bay)))
|
||||
step = span / count
|
||||
return [x0 + margin + step * (i + 0.5) for i in range(count)]
|
||||
@@ -0,0 +1,366 @@
|
||||
#!/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.
|
||||
# Finger curl, in radians per joint. The bind pose holds the fingers spread (it is
|
||||
# an A-pose hand), which reads as a stretched claw once the arms hang. Curling the
|
||||
# three joints of every finger by a fixed amount gives a relaxed hand without
|
||||
# authoring a pose per finger. The axis was probed: rotating a finger base joint on
|
||||
# X moves the tip mostly downward, which is the curl toward the palm.
|
||||
FINGER_CURL = (0.28, 0.34, 0.22)
|
||||
|
||||
PARTS: list[dict] = []
|
||||
|
||||
|
||||
def derive_arm_rotation(arm, target_dir=(0.12, 0.0, -1.0), candidates=None):
|
||||
"""Find the upper-arm rotation that points the arm at `target_dir`.
|
||||
|
||||
The 0.2.0 value (0.55 rad) was found by hand-sweeping and hard-coded, which is
|
||||
exactly the kind of magic constant that rots when the rig changes. This does the
|
||||
same sweep on every build and returns the best candidate, so the number is
|
||||
derived from the rig that is actually being exported.
|
||||
"""
|
||||
from mathutils import Euler, Vector
|
||||
|
||||
candidates = candidates or [0.35 + 0.05 * i for i in range(11)]
|
||||
target = Vector(target_dir).normalized()
|
||||
bpy.context.view_layer.objects.active = arm
|
||||
bpy.ops.object.mode_set(mode="POSE")
|
||||
upper = arm.pose.bones["upperarm01.L"]
|
||||
upper.rotation_mode = "XYZ"
|
||||
best, best_dot = 0.55, -2.0
|
||||
for z in candidates:
|
||||
upper.rotation_euler = Euler((0.0, 0.0, -z))
|
||||
bpy.context.view_layer.update()
|
||||
direction = (upper.matrix.to_3x3() @ Vector((0.0, 1.0, 0.0))).normalized()
|
||||
dot = direction.dot(target)
|
||||
if dot > best_dot:
|
||||
best, best_dot = z, dot
|
||||
upper.rotation_euler = Euler((0.0, 0.0, 0.0))
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
log(f"arm rotation derived: {best:.2f} rad (direction dot {best_dot:.3f})")
|
||||
return best
|
||||
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.
|
||||
# Values are **measured, not inherited**. The rig's A bind already points the upper
|
||||
# arm (0.65, 0.01, -0.76), i.e. about 50 degrees below horizontal, so the rotation
|
||||
# that brings it vertical is only about 0.55 rad - not the 1.55 rad the old
|
||||
# `--pose idle` table used, which over-rotated the arm ~40 degrees past vertical and
|
||||
# drove it into the torso. Sweeping the rotation in Blender gave:
|
||||
# z=-0.55 -> arm (0.16, -0.02, -0.99) down, slightly clear of the body
|
||||
# z=-0.65 -> arm (0.06, -0.02, -1.00) straight down, touching the hip
|
||||
# z=-0.75 -> arm (-0.04, -0.03, -1.00) past vertical, into the body
|
||||
# 0.55 is the value used.
|
||||
BASE = {
|
||||
"upperarm01.L": (0.0, 0.0, -0.55),
|
||||
"upperarm01.R": (0.0, 0.0, 0.55),
|
||||
"lowerarm01.L": (0.0, 0.0, -0.12),
|
||||
"lowerarm01.R": (0.0, 0.0, 0.12),
|
||||
}
|
||||
|
||||
|
||||
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 apply_base_pose(arm):
|
||||
"""Bake the arms-down pose into the REST pose before authoring any clip.
|
||||
|
||||
The first two attempts added the arms-down as an offset inside every key. That
|
||||
is not what the export sees: the exporter writes clip data relative to the rest
|
||||
pose, so an offset that lives only in the keys arrives in Unreal as a small
|
||||
delta around an A-posed bind - the body stood in an A with a ±20 degree arm
|
||||
swing, which is exactly what the Unreal pose probe measured. Baking the pose
|
||||
into the rest pose first is the same trick `mpfb_character_export.py --pose
|
||||
idle` uses, and it makes bind and clips agree.
|
||||
"""
|
||||
bpy.context.view_layer.objects.active = arm
|
||||
bpy.ops.object.mode_set(mode="POSE")
|
||||
for name, eul in BASE.items():
|
||||
bone = arm.pose.bones.get(name)
|
||||
if bone is None:
|
||||
continue
|
||||
bone.rotation_mode = "XYZ"
|
||||
bone.rotation_euler = eul
|
||||
# Bake the deformation into the MESH before the rest bake. Setting the rest pose
|
||||
# alone leaves the vertices in their A shape, so the skin stretches across the
|
||||
# shoulders and the arms sink into the torso - which is what the review shots
|
||||
# showed. The MPFB mesh carries shape keys and Blender refuses to apply a modifier
|
||||
# while they exist, so they are cleared first and the count is logged.
|
||||
mesh = next((o for o in bpy.data.objects
|
||||
if o.type == "MESH" and any(m.type == "ARMATURE" and m.object == arm
|
||||
for m in o.modifiers)), None)
|
||||
keys = 0
|
||||
if mesh is not None:
|
||||
if mesh.data.shape_keys:
|
||||
keys = len(mesh.data.shape_keys.key_blocks) - 1
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
bpy.context.view_layer.objects.active = mesh
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
mesh.select_set(True)
|
||||
if mesh.data.shape_keys:
|
||||
bpy.ops.object.shape_key_remove(all=True)
|
||||
for mod in list(mesh.modifiers):
|
||||
if mod.type == "ARMATURE" and mod.object == arm:
|
||||
bpy.ops.object.modifier_apply(modifier=mod.name)
|
||||
log(f"mesh deformation baked into vertices; {keys} shape key(s) cleared")
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
bpy.context.view_layer.objects.active = arm
|
||||
bpy.ops.object.mode_set(mode="POSE")
|
||||
|
||||
bpy.ops.pose.armature_apply()
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
if mesh is not None:
|
||||
added = mesh.modifiers.new(name="Armature", type="ARMATURE")
|
||||
added.object = arm
|
||||
log("mesh re-bound to the new rest pose")
|
||||
|
||||
log("base pose applied to rest: arms down before any clip is authored")
|
||||
|
||||
|
||||
BASE = {}
|
||||
|
||||
|
||||
def fill_base(arm):
|
||||
"""Populate the standing base: derived arm angle plus the finger curl."""
|
||||
arm_z = derive_arm_rotation(arm)
|
||||
BASE.update({
|
||||
"upperarm01.L": (0.0, 0.0, -arm_z),
|
||||
"upperarm01.R": (0.0, 0.0, arm_z),
|
||||
"lowerarm01.L": (0.0, 0.0, -0.12),
|
||||
"lowerarm01.R": (0.0, 0.0, 0.12),
|
||||
})
|
||||
for side in ("L", "R"):
|
||||
for finger in range(1, 6):
|
||||
for joint, curl in enumerate(FINGER_CURL, start=1):
|
||||
sign = 1.0 if side == "L" else -1.0
|
||||
BASE[f"finger{finger}-{joint}.{side}"] = (sign * curl, 0.0, 0.0)
|
||||
log(f"standing base: {len(BASE)} bones (arm {arm_z:.2f} rad, "
|
||||
f"finger curl {FINGER_CURL})")
|
||||
|
||||
|
||||
def key_euler(pb, frame, x=0.0, y=0.0, z=0.0):
|
||||
"""Key a pose-bone rotation, offset by the standing base.
|
||||
|
||||
The bind is left alone: the MPFB mesh carries shape keys, and every attempt to
|
||||
re-bind it (clear keys, bake the armature modifier, set rest) either failed or
|
||||
left Unreal showing the old bind. Carrying the arm rotation in the clip is the
|
||||
one path that is provable: read the imported clip back and check its quaternion.
|
||||
"""
|
||||
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)
|
||||
|
||||
# Key the standing base on every bone it touches, at both ends of the clip, so
|
||||
# the arms and fingers hold their offset instead of snapping back to the bind.
|
||||
for name in BASE:
|
||||
bone = bones.get(name)
|
||||
if bone is None:
|
||||
continue
|
||||
key_euler(bone, 1)
|
||||
key_euler(bone, frames)
|
||||
|
||||
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)
|
||||
|
||||
for name in BASE:
|
||||
bone = bones.get(name)
|
||||
if bone is None:
|
||||
continue
|
||||
key_euler(bone, 1)
|
||||
key_euler(bone, frames)
|
||||
|
||||
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)
|
||||
fill_base(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,40 @@ 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 =="
|
||||
# Clear the engine's screenshot folder first. The collect step below copies every
|
||||
# review-*.png it finds in Saved/, so without this a shot retired from the camera list
|
||||
# keeps coming back as a stale file - that is how a removed walk shot reappeared with
|
||||
# byte-identical content after a run that never captured it.
|
||||
rm -rf "$REPO"/Game/GigLine/Saved/Screenshots/* "$HOME"/.config/Epic/GigLine/Saved/Screenshots/* 2>/dev/null || true
|
||||
# Recreate the folder: the engine writes into it and does not recreate a deleted
|
||||
# directory, which silently produced zero captures in the run that removed it.
|
||||
mkdir -p "$REPO/Game/GigLine/Saved/Screenshots/LinuxEditor"
|
||||
# The engine path is absolute: 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.
|
||||
"$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 +69,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"
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Retail frontage trim for 6800 Lee Drug and 6806–6820 (0.1.6).
|
||||
|
||||
Both buildings are bare OSM extrusions with a flat stucco material; what makes them
|
||||
read as *the* retail run beside the hero is the façade layer: the bronze cornice and
|
||||
stepped Deco parapet on Lee Drug, and the pilaster-and-fascia rhythm on 6806–6820.
|
||||
|
||||
The trim is built **in world coordinates** in the shared frame (metres, X east,
|
||||
Y north, Z from the project datum), so the export carries its position and the
|
||||
Unreal staging step is a single spawn at the origin — the same convention as
|
||||
`Tools/theatre_trim_build.py` and `Tools/export_block_frontage.py`.
|
||||
|
||||
blender -b --factory-startup --python Tools/retail_frontage_build.py -- \
|
||||
--out /tmp/gigline/retail --report /tmp/gigline/retail-report.json
|
||||
|
||||
Bounds are **measured**, not assumed: they are the blockout objects' world bounds
|
||||
(`Lee Drug Building`, `element.019`), which sit at Z 0 with the volume top at 12.10 m.
|
||||
Vertical proportions come from the single reference photograph and are recorded with
|
||||
the assumption in `Art/blender/retail/ref-analysis.md`.
|
||||
|
||||
6822 Budget is deliberately **not** built: there is no photograph of it, and the TODO
|
||||
item says so. Guessing its façade would be the misattribution the item warns against.
|
||||
"""
|
||||
|
||||
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 _common # noqa: E402
|
||||
import fbx_export_preset as fbx_preset # noqa: E402
|
||||
|
||||
# --- measured bounds, from the blockout (world metres) ------------------------
|
||||
# Lee Drug 6800, OSM way 424024149. Front face is the north edge (max Y).
|
||||
LD_X0, LD_X1 = -33.20, -14.37
|
||||
LD_FACE_Y = -14.63
|
||||
LD_H = 12.10
|
||||
|
||||
# 6806-6820, OSM way 424024150.
|
||||
B6_X0, B6_X1 = -66.08, -33.07
|
||||
B6_FACE_Y = -12.52
|
||||
B6_H = 12.10
|
||||
|
||||
# --- proportions, read off the reference photograph ---------------------------
|
||||
# Fractions of the 12.10 m height; see Art/blender/retail/ref-analysis.md for how
|
||||
# they were read and why they are assumptions at ±0.03 H.
|
||||
LD_SHOP_TOP = 0.36 * LD_H # 4.36 m: shopfront glazing / cornice soffit
|
||||
LD_CORNICE_TOP = 0.43 * LD_H # 5.20 m: top of the bronze band
|
||||
LD_MURAL_Z = (0.49 * LD_H, 0.61 * LD_H)
|
||||
LD_LETTER_Z = (0.65 * LD_H, 0.76 * LD_H)
|
||||
LD_PARAPET_Z = 0.82 * LD_H # 9.92 m: bottom of the stepped parapet
|
||||
|
||||
B6_SHOP_TOP = 0.32 * B6_H # 3.87 m
|
||||
B6_FASCIA_TOP = 0.38 * B6_H # 4.60 m
|
||||
B6_WINDOW_Z = (0.47 * B6_H, 0.64 * B6_H)
|
||||
B6_PARAPET_Z = 0.74 * B6_H # 8.95 m (first pass used 0.62 and left a 4.6 m blank band)
|
||||
|
||||
BAY = 2.20 # mural / bay rhythm on Lee Drug, assumed
|
||||
PROJECT = 0.55 # bronze cornice projection, assumed
|
||||
TOWER_R = 2.10 # Lee Drug corner tower radius, assumed
|
||||
|
||||
PARTS: list[dict] = []
|
||||
|
||||
|
||||
def make_material(name, rgb, rough=0.6, metallic=0.0):
|
||||
"""Principled material with an explicit colour; the export preset carries it as a slot."""
|
||||
mat = bpy.data.materials.new(name)
|
||||
mat.use_nodes = True
|
||||
bsdf = mat.node_tree.nodes.get("Principled BSDF")
|
||||
bsdf.inputs["Base Color"].default_value = (rgb[0], rgb[1], rgb[2], 1.0)
|
||||
bsdf.inputs["Roughness"].default_value = rough
|
||||
bsdf.inputs["Metallic"].default_value = metallic
|
||||
return mat
|
||||
|
||||
|
||||
def clear_scene():
|
||||
bpy.ops.object.select_all(action="SELECT")
|
||||
bpy.ops.object.delete(use_global=False)
|
||||
for mesh in list(bpy.data.meshes):
|
||||
if mesh.users == 0:
|
||||
bpy.data.meshes.remove(mesh)
|
||||
|
||||
|
||||
def cube(name, x, y, z, dx, dy, dz, material):
|
||||
bpy.ops.mesh.primitive_cube_add(size=1.0, location=(x, y, z))
|
||||
obj = bpy.context.active_object
|
||||
obj.name = name
|
||||
obj.scale = (dx, dy, dz)
|
||||
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
|
||||
obj.data.materials.append(material)
|
||||
return obj
|
||||
|
||||
|
||||
def cylinder(name, x, y, z, radius, height, material, vertices=28):
|
||||
bpy.ops.mesh.primitive_cylinder_add(vertices=vertices, radius=radius, depth=height,
|
||||
location=(x, y, z))
|
||||
obj = bpy.context.active_object
|
||||
obj.name = name
|
||||
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
|
||||
obj.data.materials.append(material)
|
||||
return obj
|
||||
|
||||
|
||||
def bay_centres(x0, x1, margin=0.9, bay=BAY):
|
||||
"""Bay centres; the arithmetic lives in `_common` so it is testable without Blender."""
|
||||
return _common.bay_centres(x0, x1, margin=margin, bay=bay)
|
||||
|
||||
|
||||
def note(obj, evidence):
|
||||
PARTS.append({
|
||||
"object": obj.name,
|
||||
"asset": "SM_" + obj.name,
|
||||
"dims_m": [round(d, 3) for d in obj.dimensions],
|
||||
"location_m": [round(c, 3) for c in obj.location],
|
||||
"evidence": evidence,
|
||||
})
|
||||
print(f"[retail] {obj.name:26} {obj.dimensions.x:6.2f} x {obj.dimensions.y:5.2f} "
|
||||
f"x {obj.dimensions.z:5.2f} m ({evidence})")
|
||||
|
||||
|
||||
def build_lee_drug(mats):
|
||||
"""6800 Lee Drug: bronze cornice, mural row, lettering band, stepped Deco parapet,
|
||||
rounded corner tower."""
|
||||
stucco, bronze, mural, glass, white = (mats["stucco"], mats["bronze"], mats["mural"],
|
||||
mats["glass"], mats["white"])
|
||||
lettering, reveal = mats["lettering"], mats["reveal"]
|
||||
parts = []
|
||||
cx = (LD_X0 + LD_X1) / 2.0
|
||||
width = LD_X1 - LD_X0
|
||||
|
||||
# Bronze cornice: the strongest horizontal line on the building.
|
||||
cornice = cube("LD_Cornice", cx, LD_FACE_Y + PROJECT / 2.0,
|
||||
(LD_SHOP_TOP + LD_CORNICE_TOP) / 2.0,
|
||||
width + 0.4, PROJECT, LD_CORNICE_TOP - LD_SHOP_TOP, bronze)
|
||||
parts.append((cornice, "photographs: projecting bronze cornice over the shopfront; "
|
||||
f"{PROJECT:.2f} m projection assumed"))
|
||||
|
||||
# Shopfront glazing, recessed behind the face, one panel per bay.
|
||||
for i, bx in enumerate(bay_centres(LD_X0, LD_X1, margin=1.2), start=1):
|
||||
panel = cube(f"LD_ShopGlass_{i}", bx, LD_FACE_Y + 0.12, LD_SHOP_TOP / 2.0,
|
||||
BAY - 0.30, 0.10, LD_SHOP_TOP - 0.55, glass)
|
||||
parts.append((panel, "photographs: full-height shopfront glazing; bay width and "
|
||||
"the 0.55 m kickplate are assumed (a bus hides this zone)"))
|
||||
mullion = cube(f"LD_Mullion_{i}", bx - (BAY - 0.30) / 2.0, LD_FACE_Y - 0.02,
|
||||
LD_SHOP_TOP / 2.0, 0.10, 0.16, LD_SHOP_TOP - 0.55, bronze)
|
||||
parts.append((mullion, "assumption: slim mullion at each bay boundary"))
|
||||
|
||||
# Celebrity-portrait panels, one per bay, in the band above the cornice.
|
||||
#
|
||||
# First pass modelled these as a shallow plate on the wall plane and they vanished
|
||||
# at review distance: no shadow, no edge. Now each panel is a near-black plate set
|
||||
# 0.14 m back inside a stucco frame that stands 0.10 m proud, so the sun casts a
|
||||
# shadow line down one side and the row reads as a row of pictures.
|
||||
z0, z1 = LD_MURAL_Z
|
||||
for i, bx in enumerate(bay_centres(LD_X0, LD_X1, margin=1.2), start=1):
|
||||
frame = cube(f"LD_MuralFrame_{i}", bx, LD_FACE_Y + 0.20, (z0 + z1) / 2.0,
|
||||
BAY - 0.55, 0.12, (z1 - z0) + 0.30, white)
|
||||
parts.append((frame, "photographs: stucco frame around each portrait panel; "
|
||||
"0.30 m border assumed"))
|
||||
panel = cube(f"LD_Mural_{i}", bx, LD_FACE_Y + 0.12, (z0 + z1) / 2.0,
|
||||
BAY - 0.85, 0.06, (z1 - z0), mural)
|
||||
parts.append((panel, "photographs: row of black-framed portrait panels; count and "
|
||||
"size assumed from the visible row"))
|
||||
|
||||
# Lettering band: the photographs show dark letters painted straight on the stucco,
|
||||
# so this is a slightly proud plate in a mid-dark tone rather than a black recess.
|
||||
# The words themselves remain a decal job; the plate only reserves the band.
|
||||
lz0, lz1 = LD_LETTER_Z
|
||||
letter = cube("LD_LetterBand", cx, LD_FACE_Y + 0.10, (lz0 + lz1) / 2.0,
|
||||
width - 3.6, 0.10, lz1 - lz0, lettering)
|
||||
parts.append((letter, "photographs: large dark lettering across the upper wall; "
|
||||
"modelled as a painted band, the text itself is a decal job"))
|
||||
|
||||
# Stepped Deco parapet. A single 2.18 m wall with 0.10 m reveals read as one blank
|
||||
# white band, so it is now two tiers with a 0.10 m step between them, and the
|
||||
# reveals are 0.22 m tall and cut 0.16 m deep - deep enough to hold a shadow.
|
||||
tier_h = (LD_H - LD_PARAPET_Z) / 2.0
|
||||
lower = cube("LD_ParapetLower", cx, LD_FACE_Y + 0.14, LD_PARAPET_Z + tier_h / 2.0,
|
||||
width + 0.2, 0.34, tier_h, stucco)
|
||||
parts.append((lower, "photographs: parapet lower tier above the frontage wall; "
|
||||
"thickness assumed"))
|
||||
upper = cube("LD_ParapetUpper", cx, LD_FACE_Y + 0.24, LD_PARAPET_Z + tier_h * 1.5,
|
||||
width - 3.2, 0.30, tier_h, stucco)
|
||||
parts.append((upper, "photographs: parapet upper tier, stepped back from the lower "
|
||||
"one; setback and length assumed"))
|
||||
for i, (z, span) in enumerate(((LD_PARAPET_Z + tier_h * 0.45, width + 0.2),
|
||||
(LD_PARAPET_Z + tier_h * 1.30, width + 0.2),
|
||||
(LD_H - 0.14, width - 3.2)), start=1):
|
||||
groove = cube(f"LD_ParapetReveal_{i}", cx, LD_FACE_Y - 0.02, z,
|
||||
span, 0.16, 0.22, reveal)
|
||||
parts.append((groove, "photographs: horizontal reveal lines in the parapet; "
|
||||
"count read off the frame"))
|
||||
|
||||
# Rounded corner tower at the east end, stepped twice at the top. The centre
|
||||
# sits INSIDE the footprint so the tower projects 0.35 m from the face instead of
|
||||
# bulging 4 m into the pavement, which the first run did.
|
||||
tower_y = LD_FACE_Y - TOWER_R + 0.35
|
||||
tower = cylinder("LD_CornerTower", LD_X1 - TOWER_R, tower_y,
|
||||
(LD_H - 0.9) / 2.0, TOWER_R, LD_H - 0.9, stucco)
|
||||
parts.append((tower, "photographs: rounded Deco corner tower rising above the "
|
||||
f"frontage; {TOWER_R:.2f} m radius assumed"))
|
||||
# Vertical fins around the curve: the photograph's tower is fluted, not a smooth
|
||||
# drum. Twelve ribs at this radius are legible from the street.
|
||||
for i in range(12):
|
||||
ang = math.pi * (i + 0.5) / 12.0
|
||||
fx = (LD_X1 - TOWER_R) + math.cos(ang) * (TOWER_R + 0.06)
|
||||
fy = tower_y + math.sin(ang) * (TOWER_R + 0.06)
|
||||
fin = cube(f"LD_TowerFin_{i + 1}", fx, fy, (LD_H - 0.9) / 2.0,
|
||||
0.16, 0.16, LD_H - 0.9, white)
|
||||
parts.append((fin, "photographs: fluted fins on the corner tower; count assumed"))
|
||||
|
||||
# Cap rings stack exactly to the measured 12.10 m top: 11.20 -> 11.75 -> 12.10.
|
||||
cap_z = LD_H - 0.9
|
||||
for i, (dh, radius) in enumerate(((0.55, TOWER_R - 0.35), (0.35, TOWER_R - 0.70)), start=1):
|
||||
cap = cylinder(f"LD_TowerCap_{i}", LD_X1 - TOWER_R, tower_y,
|
||||
cap_z + dh / 2.0, radius, dh, white)
|
||||
parts.append((cap, "photographs: two stepped cap rings on the tower"))
|
||||
cap_z += dh
|
||||
return parts
|
||||
|
||||
|
||||
def build_6806(mats):
|
||||
"""6806–6820: burgundy fascia, brown pilasters, white-framed windows, banded parapet."""
|
||||
stucco, bronze, glass, white, burgundy = (mats["stucco"], mats["bronze"], mats["glass"],
|
||||
mats["white"], mats["burgundy"])
|
||||
reveal = mats["reveal"]
|
||||
parts = []
|
||||
cx = (B6_X0 + B6_X1) / 2.0
|
||||
width = B6_X1 - B6_X0
|
||||
|
||||
fascia = cube("B6_Fascia", cx, B6_FACE_Y - 0.02,
|
||||
(B6_SHOP_TOP + B6_FASCIA_TOP) / 2.0, width, 0.22,
|
||||
B6_FASCIA_TOP - B6_SHOP_TOP, burgundy)
|
||||
parts.append((fascia, "photographs: dark fascia band over the shopfront; colour "
|
||||
"sampled, height assumed"))
|
||||
|
||||
centres = bay_centres(B6_X0, B6_X1, margin=1.0)
|
||||
for i, bx in enumerate(centres, start=1):
|
||||
panel = cube(f"B6_ShopGlass_{i}", bx, B6_FACE_Y + 0.12, B6_SHOP_TOP / 2.0,
|
||||
BAY - 0.25, 0.10, B6_SHOP_TOP - 0.45, glass)
|
||||
parts.append((panel, "photographs: framed shopfront glazing behind the fascia"))
|
||||
# Brown pilaster on every second bay boundary, as the photo shows.
|
||||
if i % 2 == 1:
|
||||
pil = cube(f"B6_Pilaster_{i}", bx, B6_FACE_Y - 0.10,
|
||||
(B6_FASCIA_TOP + B6_PARAPET_Z) / 2.0, 0.40, 0.22,
|
||||
B6_PARAPET_Z - B6_FASCIA_TOP, bronze)
|
||||
parts.append((pil, "photographs: brown pilaster strips dividing the frontage"))
|
||||
|
||||
wz0, wz1 = B6_WINDOW_Z
|
||||
for i, bx in enumerate(centres, start=1):
|
||||
frame = cube(f"B6_WindowFrame_{i}", bx, B6_FACE_Y - 0.08, (wz0 + wz1) / 2.0,
|
||||
1.70, 0.18, wz1 - wz0, white)
|
||||
parts.append((frame, "photographs: white-framed upper windows with security bars; "
|
||||
"count and size assumed"))
|
||||
pane = cube(f"B6_WindowGlass_{i}", bx, B6_FACE_Y + 0.04, (wz0 + wz1) / 2.0,
|
||||
1.46, 0.08, wz1 - wz0 - 0.24, glass)
|
||||
parts.append((pane, "photographs: dark glazing inside each frame"))
|
||||
# Security bars: three thin verticals per window, the detail that stops the
|
||||
# upper storey reading as a row of black rectangles.
|
||||
for b in (-1, 0, 1):
|
||||
bar = cube(f"B6_WindowBar_{i}_{b + 2}", bx + b * 0.40, B6_FACE_Y - 0.15,
|
||||
(wz0 + wz1) / 2.0, 0.06, 0.06, wz1 - wz0 - 0.30, white)
|
||||
parts.append((bar, "photographs: vertical security bars on the upper windows"))
|
||||
|
||||
# Two-tier parapet with deep reveals, as on Lee Drug: one flat band with hairlines
|
||||
# disappeared at review distance.
|
||||
tier_h = (B6_H - B6_PARAPET_Z) / 2.0
|
||||
para = cube("B6_Parapet", cx, B6_FACE_Y + 0.12, B6_PARAPET_Z + tier_h / 2.0,
|
||||
width + 0.2, 0.30, tier_h, stucco)
|
||||
parts.append((para, "photographs: parapet lower tier above the window storey; "
|
||||
"thickness assumed"))
|
||||
upper = cube("B6_ParapetUpper", cx, B6_FACE_Y + 0.20, B6_PARAPET_Z + tier_h * 1.5,
|
||||
width - 2.4, 0.26, tier_h, stucco)
|
||||
parts.append((upper, "photographs: parapet upper tier stepped back; length assumed"))
|
||||
for i, (z, span) in enumerate(((B6_PARAPET_Z + tier_h * 0.45, width + 0.2),
|
||||
(B6_PARAPET_Z + tier_h * 1.30, width + 0.2),
|
||||
(B6_H - 0.12, width - 2.4)), start=1):
|
||||
groove = cube(f"B6_ParapetReveal_{i}", cx, B6_FACE_Y - 0.02, z,
|
||||
span, 0.14, 0.18, reveal)
|
||||
parts.append((groove, "photographs: reveal lines in the parapet; count assumed"))
|
||||
return parts
|
||||
|
||||
|
||||
def join_parts(parts, name):
|
||||
"""Join one building's parts into a single mesh so Unreal imports one asset.
|
||||
|
||||
The per-part dimensions and evidence are recorded before this runs, so joining
|
||||
loses nothing from the report. Materials survive as separate slots.
|
||||
"""
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
for obj, _text in parts:
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = parts[0][0]
|
||||
bpy.ops.object.join()
|
||||
joined = bpy.context.active_object
|
||||
joined.name = name
|
||||
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
|
||||
return joined
|
||||
|
||||
|
||||
def export_objects(objects, out_dir):
|
||||
count = 0
|
||||
for obj in objects:
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
fbx_preset.export_static(os.path.join(out_dir, "SM_" + obj.name + ".fbx"),
|
||||
selection=True)
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
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/retail")
|
||||
ap.add_argument("--report", default=None)
|
||||
args = ap.parse_args(argv)
|
||||
os.makedirs(args.out, exist_ok=True)
|
||||
|
||||
clear_scene()
|
||||
mats = {
|
||||
"stucco": make_material("M_Retail_Stucco", (0.86, 0.83, 0.76), rough=0.72),
|
||||
"bronze": make_material("M_Retail_Bronze", (0.20, 0.11, 0.06), rough=0.45, metallic=0.35),
|
||||
"mural": make_material("M_Retail_Mural", (0.035, 0.032, 0.030), rough=0.55),
|
||||
"glass": make_material("M_Retail_Glass", (0.035, 0.045, 0.055), rough=0.15),
|
||||
"white": make_material("M_Retail_White", (0.90, 0.88, 0.84), rough=0.60),
|
||||
"burgundy": make_material("M_Retail_Burgundy", (0.20, 0.09, 0.09), rough=0.55),
|
||||
# Painted lettering and shadow reveals: same value family as the wall so they
|
||||
# read as paint and shadow, not as black holes.
|
||||
"lettering": make_material("M_Retail_Lettering", (0.30, 0.27, 0.24), rough=0.65),
|
||||
"reveal": make_material("M_Retail_Reveal", (0.42, 0.38, 0.34), rough=0.70),
|
||||
}
|
||||
|
||||
ld_parts = build_lee_drug(mats)
|
||||
b6_parts = build_6806(mats)
|
||||
for obj, text in ld_parts + b6_parts:
|
||||
note(obj, text)
|
||||
|
||||
# One mesh per building: 85 separate actors over the frontage would cost more
|
||||
# draw calls than the trim is worth, and the staging step stays two spawns.
|
||||
joined = [join_parts(ld_parts, "Retail_LeeDrug"), join_parts(b6_parts, "Retail_6806")]
|
||||
for obj in joined:
|
||||
print(f"[retail] joined {obj.name}: {len(obj.data.vertices)} verts, "
|
||||
f"{len(obj.data.materials)} material slot(s), "
|
||||
f"X {min(v.co.x for v in obj.data.vertices):.2f}.."
|
||||
f"{max(v.co.x for v in obj.data.vertices):.2f}")
|
||||
exported = export_objects(joined, args.out)
|
||||
print(f"[retail] exported {exported} object(s) to {args.out}")
|
||||
|
||||
if args.report:
|
||||
with open(args.report, "w", encoding="utf-8") as handle:
|
||||
json.dump({
|
||||
"scope": ["6800 Lee Drug (way 424024149)", "6806-6820 (way 424024150)"],
|
||||
"deferred": ["6822 Budget (way 424024144): no photograph, not modelled"],
|
||||
"bounds_m": {
|
||||
"lee_drug": [LD_X0, LD_X1, LD_FACE_Y, LD_H],
|
||||
"b6806": [B6_X0, B6_X1, B6_FACE_Y, B6_H],
|
||||
},
|
||||
"reference": "Art/reference/retail-neighbours/01-lee-drug-and-neighbor.jpg "
|
||||
"(Gb321, CC BY-SA 4.0)",
|
||||
"analysis": "Art/blender/retail/ref-analysis.md",
|
||||
"parts": PARTS,
|
||||
}, handle, indent=2)
|
||||
print(f"[retail] report -> {args.report}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -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,32 @@
|
||||
"""Tests for the pure layout helper in Tools/retail_frontage_build.py.
|
||||
|
||||
The builder itself needs Blender; `bay_centres` does not, so the bay rhythm the
|
||||
façade depends on is checkable without starting Blender or Unreal.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
from Tools._common import bay_centres
|
||||
|
||||
|
||||
class BayCentreTests(unittest.TestCase):
|
||||
def test_centres_stay_inside_the_margins(self):
|
||||
centres = bay_centres(-33.20, -14.37, margin=1.2)
|
||||
self.assertGreaterEqual(min(centres), -33.20 + 1.2 - 1e-9)
|
||||
self.assertLessEqual(max(centres), -14.37 - 1.2 + 1e-9)
|
||||
|
||||
def test_bays_are_evenly_spaced(self):
|
||||
centres = bay_centres(-66.08, -33.07, margin=1.0)
|
||||
steps = {round(b - a, 6) for a, b in zip(centres, centres[1:])}
|
||||
self.assertEqual(len(steps), 1, f"uneven bay spacing: {steps}")
|
||||
|
||||
def test_bay_count_is_the_round_of_span_over_bay(self):
|
||||
# 33.01 m frontage, 1.0 m margin each end -> 31.01 m -> 14 bays at 2.2 m.
|
||||
self.assertEqual(len(bay_centres(-66.08, -33.07, margin=1.0)), 14)
|
||||
|
||||
def test_degenerate_frontage_returns_nothing(self):
|
||||
self.assertEqual(bay_centres(0.0, 1.0, margin=1.0), [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -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
|
||||
@@ -217,9 +235,17 @@ def main() -> None:
|
||||
removed += 1
|
||||
log(f"cleared {removed} previous kit actors")
|
||||
|
||||
# The kit is the fallback for buildings that have nothing but a blockout. These
|
||||
# two are modelled from their own photograph in 0.1.6
|
||||
# (Tools/retail_frontage_build.py); pasting the generic kit over them put a
|
||||
# second, contradicting facade language on the same wall.
|
||||
MODELLED = ("Frontage_SM_LeeDrug_6800", "Frontage_SM_Hollywood_6806")
|
||||
targets = [a for a in unreal.EditorLevelLibrary.get_all_level_actors()
|
||||
if a.get_actor_label().startswith(("Frontage_", "North_"))]
|
||||
log(f"frontage actors: {len(targets)}")
|
||||
if a.get_actor_label().startswith(("Frontage_", "North_"))
|
||||
and a.get_actor_label() not in MODELLED]
|
||||
skipped = [a.get_actor_label() for a in unreal.EditorLevelLibrary.get_all_level_actors()
|
||||
if a.get_actor_label() in MODELLED]
|
||||
log(f"frontage actors: {len(targets)} (kit skipped on modelled facades: {skipped})")
|
||||
total = 0
|
||||
for actor in targets:
|
||||
total += apply_to_actor(actor, kit, materials)
|
||||
|
||||
Loaded 100 of 109 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user