33 Commits
Author SHA1 Message Date
zwitschi 12272d6b96 release: v0.2.0 — third-person body, animation, stride capture
Writes the 0.2.0 changelog entry with the measured numbers, removes the completed
section from TODO.md (its goal stays in ROADMAP.md) and updates the roadmap status.

Release checks: 8/8 shots verify with 6, 7 and 8 approved; 1440p at 2.64 ms average
(379 fps); -qaselftest passes walk, reset, pause, transect, wall and facade probes;
82 tool tests pass. Known limitations are listed rather than hidden: splayed fingers,
a swept base rotation and unproven camera collision, all marked for later patches.
2026-11-09 12:30:49 +01:00
zwitschi 56e104f5f3 todo: close the 0.2.0 animation check, leaving only the tag
Both clips play and advance (idle proved by the single-node instance's asset time,
walk by shot 8), the body carries its skin material and the QA self-test passes.
The two cosmetic defects — splayed fingers and a base rotation that was swept rather
than derived — stay open as 0.2.2 items and are named in the check.
2026-11-09 12:30:00 +01:00
zwitschi 8e880ed09d perf: 0.2.0 re-measured at 1440p — 379 fps average, 6.3x the floor
-qaselftest at 2560x1440, TSR on, VSync off, no frame generation: 759 frames,
avg 2.64 ms (379 fps), p50 2.54, p95 3.39, p99 3.76, max 4.05 ms. The body, skin
material and animation add about 2 % over 0.1.5's 2.61 ms.

The same run passed every QA check: walk, reset 0 cm from spawn, pause/unpause,
transect 1456 samples with 0 holes, wall probe 0/35, facade probe 0/9.

Series committed as Docs/perf/0.2.0-walk-frametimes.csv and the result recorded in
the release-checks table next to the 0.1.0 and 0.1.5 numbers.
2026-11-09 12:29:46 +01:00
zwitschi 9ce7011389 docs(input): the V toggle, the harness switches, and the HUD line
Docs/controls.md documented the 0.1.0 bindings only, so the camera toggle added with
0.2.0 was undocumented for the player-facing set and the review switches were
undocumented everywhere. Adds the V row, a changes-since-0.1.0 note that first person
stays the default, and a table for -thirdperson / -walk / -qaselftest with the note
that the walking capture is driven per shot rather than by the switch.

The in-game HUD line gains 'V view' so the packaged build documents it too.
2026-11-09 12:21:11 +01:00
zwitschi f5e7e66127 docs: remove the 0.1.4 review record and the 0.1.0 packaged capture
Both were owner-requested removals. Nothing else referenced the review file except a
MEMORY entry, which now says the per-shot Approved column of Screenshots/README.md is
the only and authoritative record; release-checks keeps the 0.1.0 packaged-build
measurement as numbers and notes that its capture is no longer in the repository.
2026-11-09 12:20:36 +01:00
zwitschi dbb177da6f feat(walk): the stride is captured — walking shot 8, traversal check closed
Shot 8 shows the body mid-stride in profile: legs apart, arms counter-swinging. The
traversal release check for 0.2.0 is closed with that evidence.

The reason earlier attempts froze on one pose was not the clip. A probe of the
single-node instance showed the clip advancing normally (assetTime 0.19 -> 1.03,
rate 1.0, playing) while the pawn's speed stayed 0 and the clip stayed Idle: the
call that enables walking sat in the shot sequencer's BeginPlay, which runs before
the player pawn exists, so the cast found nothing and silently did nothing. Moved
into the per-frame follow update, the same run logs walk=1 speed=28 clip=GigLine_Walk
and the stride appears.

Also: the walk expectation was missing from visual_targets.json after the earlier
retirement, so --verify was checking 7 shots while 8 were captured. Restored; 8/8
verify.
2026-11-09 12:18:22 +01:00
zwitschi eeeabe56b1 feat(review): deterministic walking capture, and the defect it exposed
The shot actor can now photograph a moving pawn: a per-shot ShotFollow flag re-places
the camera every frame at a fixed offset in the pawn's frame, and the walk is driven
per shot (SetReviewWalk) with the pawn returned to spawn after a follow shot, so the
fixed-camera shots later in the list still find it in place. With that, the capture
stopped depending on where a 4.5 m/s pawn happened to be.

Two further bugs fixed on the way:
- the engine's screenshot folder is cleared before each run. The collect step copies
  every review-*.png it finds in Saved/, so a shot retired from the camera list kept
  reappearing with byte-identical content from an older run - which is how a walk shot
  that was never captured looked like a captured one.
- UpdateBodyAnimation no longer calls SetAnimation every frame. Re-setting the same
  clip restarts it, which freezes the body on frame 0.

What the deterministic capture then exposed: the walk clip is applied but does not
advance - every walking frame shows the same pose (arms forward, legs together). The
stride shot is retired again rather than shipped, and TODO.md names the next step:
log GetCurrentAssetTime across frames and check the imported PlayRate. The harness
stays wired for when the clip plays.

Shots 6 and 7 were re-rendered after these changes and still pass; 7/7 verify.
2026-11-09 12:11:24 +01:00
zwitschi f84093adba feat(walk): a -walk harness flag, and the honest reason the stride shot is not in
Screenshots 6 and 7 are approved, so the remaining 0.2.0 work is the traversal
release check. This adds the piece of harness it needs - `-walk` holds forward input
from the first frame, so the pawn actually crosses the sidewalk while the third-person
rig follows it - and records what that check still lacks.

A stride shot was attempted three times and removed: a fixed review camera cannot
catch a pawn moving at 4.5 m/s. Each attempt framed it at a different distance and
the last two missed it entirely, so shipping it would have been a broken shot with a
green verification. The TODO item now says the blocker is the capture harness (a
camera that tracks the pawn), not the game, and names where the shot goes back in.

The flag stays because it is what the traversal check will drive.
2026-11-09 11:50:07 +01:00
zwitschi 0e0aed02d6 review: tick shot 6 in the status row too 2026-11-09 11:43:29 +01:00
zwitschi 7ea3c1999c review: owner approves the two third-person shots (6 and 7) 2026-11-09 11:43:23 +01:00
zwitschi 46b84884b8 fix(character): the arms hang at the sides — and why that took so long
The character now stands naturally: arms at its sides with a soft elbow bend, hands
at the hips, feet flat. Both character shots confirm it, front and back.

The root cause was not the pose. The FBX importer had been silently refusing to
replace the existing assets: after changing the arm base from 1.55 rad to 0.55 rad
and rebuilding, the imported clip returned the IDENTICAL bone quaternion
(+0.006, +0.002, -0.179, -0.984). replace_existing=True was set and delete_asset
returned False even after chmod u+w, because the pawn's constructor hard-references
the clips. Importing under a new name (CharacterV2) created fresh assets and the
same probe then returned (+0.439, +0.898) — the rotation had landed. Every earlier
attempt had been invisible for this reason.

The rotation itself is measured, not inherited: sweeping it in Blender gives 0.55 rad
for arms down and clear of the body, 0.65 for straight down against the hip, 0.75 for
past vertical and into the torso. It lives in the clamp keys, not in the bind: every
bind experiment was impossible on this shape-keyed MPFB mesh or left Unreal showing
the old bind.

Remaining, for 0.2.2: the fingers splay from the bind, and the walk clip has still
not been captured in motion.
2026-11-09 11:42:36 +01:00
zwitschi 8993fd83c4 feat(body): pose the arms down, add a front-angle shot
The character now stands with its arms at its sides instead of in a bind A-pose,
and there is a second camera to prove it: the third-person shot only ever showed
the pawn's back, so a front-facing pose defect was invisible in review.

  demo-third-person-front  camera on the road, ~6 m in front, looking back north
                           at the pawn; embedded in Screenshots/README.md as shot 7.

The pose work itself is a chain of measured findings, all recorded in TODO.md:

- clip data is exported relative to the REST pose, so arms-down offsets held only in
  the keys arrived as a ±20 degree swing around an A bind;
- a rest-pose bake alone does not move the mesh: the MPFB mesh has shape keys, the
  armature modifier cannot be applied while they exist, and the vertices stayed in
  the A shape (X span unchanged at 1.051 m). Clearing the shape keys, baking the
  deformation into the vertices and re-binding is what made bind and clips agree.

Not settled, and left as an open item rather than a claim: the base pose uses
inverted signs to compensate for the FBX axis mapping, the upper arms clip into the
torso, and the fingers splay. An aim-based pose module was written and then removed
- applied without the vertex bake it produced arms raised in a V, and a half-working
shared module is worse than none. The proper solve is 0.2.2: apply the standing pose
at character creation, then author clips against that bind.
2026-11-09 11:28:29 +01:00
zwitschi a1f5989cbb fix(anim): the clip now drives the body — two causes, both measured
The body stood in an A-pose while the log said the clip was playing. Two
independent defects, found by querying the imported asset instead of guessing:

1. PlayAnimation needs the component in AnimationSingleNode with 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 the instance
   is missing.
2. The arms-down offsets lived only in the clip keys. The exporter writes clip data
   relative to the rest pose, so they arrived as a ±20 degree swing around an
   A-posed bind - which is exactly what the pose probe measured. apply_base_pose now
   bakes them into the rest pose before any clip is authored, the same trick
   mpfb_character_export.py --pose idle uses. The table is applied with inverted
   signs: as written, the FBX axis mapping produced arms raised in a V.

Shot 6 confirms it: the arms hang at the sides. Still open and recorded - the idle
pose reads awkwardly (arms forward, hands splayed) and the walk clip has no shot in
motion; both are marked in TODO.md and Screenshots/README.md.

Instrumentation removed; the diagnostics that remain are behind
gigline.SetupLogs.
2026-11-09 10:56:43 +01:00
zwitschi d0f94dff4e memory: record the bind-pose defect and the FBX that is not the cause
Re-importing the exported FBX gives two actions with 1379 f-curves over 137 bones,
so the animation is baked; the defect is engine-side. Records that PlayAnimation
needs AnimationSingleNode mode and that the clip-name log line proves intent, not
effect.
2026-11-09 10:45:07 +01:00
zwitschi 50e023f15d merge: bring main (v0.1.6) into anim-body-0.2.0
Conflicts resolved by union, not by picking a side:

- AGENTS.md keeps the stronger visual-review rule (README updated with every visual
  change; a stale section is a defect).
- render_screenshots.sh keeps the absolute engine path from main and adds
  -thirdperson from this branch - both were needed and neither alone worked.
- Screenshots/README.md now lists six shots, retail and third-person both embedded;
  the known-issues tables merged, with the animation defect and unproven camera
  collision added to main's rows.
- visual_targets.json carries both expectations.
- Architecture docs: ADR-8 (realistic buildings) and ADR-9 (third-person body) both
  present and in numeric order; ADR-8 was mis-placed before the Quality Requirements
  heading in the combined document by an earlier edit, now fixed.
- Binary conflicts resolved to main's content, since the retail trim only exists
  there.

Checks: 82 tool tests, prettier clean, six embeds resolve.
2026-11-09 10:43:00 +01:00
zwitschi e487655e2f Merge pull request #33: 0.1.6 — retail frontage (Lee Drug, 6806–6820)
Patch release v0.1.6: retail frontage modelled from its own photograph, 0.9.0 realistic-buildings milestone planned, screenshots embedded inline.
2026-11-09 10:41:10 +01:00
zwitschi 304c1e9afd docs(review): embed every demo shot inline, record the rule in AGENTS
Screenshots/README.md on this branch still used the path-only table, so a reviewer
had to open six PNGs to judge a pass. Every shot now has its own section with the
image embedded, its view, expected content and both verdicts, plus a status table
at the top that links to them.

The known-issues list becomes a table of issue -> version and item that owns it:
pale hero stone, hero entrance doors, the bind-pose animation defect, unproven
camera collision and the terrain-as-pavement strip all point at their TODO owner.
Two stale entries removed: the theatre marquee exists (six trim actors staged) and
'materials not yet created' contradicted the scene conventions.

AGENTS.md now says the README is updated with every visual change, that the image
is embedded inline, and that approval ticks both the section and the table row.

Also removed an orphan: review-demo-retail-frontage.png had been re-copied into
this branch by the capture step although its geometry only exists on
facade-retail-0.1.6.
2026-11-09 10:35:44 +01:00
zwitschi 05c1488c00 feat(camera): third-person rig, V toggle, visible body
One pawn, both rigs (ADR-9): a skeletal body hidden in first person, a
collision-tested spring arm, and a toggle that swaps the active camera and the
body's visibility. Shipped default stays first person; -thirdperson starts the
pawn in the third-person rig so the review shot is reproducible, and the
screenshot run passes it.

Three defects found and two fixed:
- The skin material had no SkeletalMesh usage flag, so Unreal silently substituted
  the default grey. That is why the body rendered as clay.
- render_screenshots.sh still called ./Engine/... on this branch (the fix lives on
  the facade branch and never reached main); applied here so the shots generate.
- OPEN: the idle clip is bound and PlayAnimation is called - the run logs the clip
  name - but the body still renders in bind pose, arms out. Recorded with that
  evidence in TODO.md rather than called done.

Shot 5 (demo-third-person) added and documented; camera collision is implemented
but unproven, since the spawn has open space behind it.
2026-11-09 10:33:06 +01:00
zwitschi 96238bdb6a feat(anim): idle and walk clips on the MPFB rig
Two parametric cycles authored at 30 fps instead of hand-keyed poses, so the loop
closes by construction: GigLine_Idle (60 f, breathing sway, feet planted) and
GigLine_Walk (30 f, one stride, hip 0.45 rad / knee 0.55 / arm 0.32, arms
counter-phased, torso counter-rotation).

The limb axes were measured, not assumed: a probe rotated upperleg01.L by 0.5 rad
on each axis and read foot.L, which moved -0.022 -> +0.362 in Y on X, twisted on Y
and abducted on Z. Using the wrong axis here would have produced a character
scissoring sideways, which no import error would have caught.

The character export preset now bakes animation. Without bake_anim the exporter
writes a static bind pose and Unreal imports a mesh with no AnimSequence and no
error, so the importer asserts a clip exists and prints VERDICT: PASS/FAIL.

Verified in /Game/Test/Character: Idle 1.967 s and Walk 0.967 s, both AnimSequences,
skeleton intact. Playback on a possessed pawn is not yet verified: the demo pawn has
no visible body, so that check belongs with the third-person camera item.
2026-11-09 10:14:05 +01:00
zwitschi 817313aabd todo: claim 0.2.0 character setup on branch anim-body-0.2.0 2026-11-09 10:11:10 +01:00
zwitschi 744bf8f357 release: v0.1.6 — retail frontage changelog entry, TODO pruning
Writes the 0.1.6 entry, removes the completed section from TODO.md (its goal stays
in ROADMAP.md per the convention), and updates the roadmap status line.

Release checks: 5/5 shots verify, scene audit 0 failures, 82 tool tests, LFS check
clean, owner approval recorded for the retail shot.
2026-11-09 10:10:50 +01:00
zwitschi c72bc41af1 review: owner approves the 0.1.6 retail frontage shot
Shot 5 approved 2026-11-09 (second pass). All five demo shots verified and approved;
the --verify item in 0.1.6 is closed. Only the tag remains.
2026-11-09 10:09:40 +01:00
zwitschi 803e77473c docs: give every known cosmetic issue a version, move blocked work out of 0.1.6
Screenshots/README.md's known-issues list had no tickets, so the same defects kept
resurfacing as surprises. It is now a table with the issue, the version and item
that owns it, and a state.

Two entries were stale and are removed: the El Capitan Theatre marquee and blade
sign have been staged since ue_stage_theatre_trim.py landed (six trim actors in the
demo map), and 'materials are not yet created' contradicted the scene conventions
in the same file.

Moved out of 0.1.6, which is a patch and cannot carry new scope:
- wall lettering decal, moulded parapet profile, tower stepping and portrait
  panels as textures -> 0.9.0 per-building models
- 6822 Budget (blocked: no photograph exists) -> 0.9.0 photography gap
The patch keeps what a patch can do: the trim that reads, the verified shot, the tag.

Filed with their proper owners: hero entrance doors and central-bay positioning ->
0.9.0 hero item; pale hero stone -> 0.9.0 measured material library; pavement
between kerb and building line -> 0.6.0 visual polish.

The 0.1.6 claim is released: nothing left on this branch but the owner's approval
and the tag.
2026-11-09 10:07:27 +01:00
zwitschi 3942e9a215 feat(facades): 0.1.6 retail second pass — relief, material mapping, framing
What the first pass got wrong, and what changed:

- The mural plates were placed 0.20 m INSIDE the solid blockout volume, so they
  never rendered. Nothing can be recessed into a solid we do not cut: the inset is
  now faked by a proud frame with a near-flush plate, and the row reads.
- Detail sat 0.05 m off the wall, under a pixel of depth at 40 m. Trim planes now
  stand 0.10-0.20 m proud, and the parapet became two tiers with deeper reveals.
- 6806's parapet started at 0.62 H and left a 4.6 m blank band; the window storey
  and parapet moved up to follow the photograph.
- Security bars on the upper windows, twelve fluted fins on the corner tower.
- The trim's stucco/white/glass slots are mapped onto the demo's look materials, so
  the walls sit in the same noise/texture family as the rest of the block; the
  colour slots sampled from the photograph stay as authored.
- Review camera pulled back and widened to 66 deg so the east end is in frame.
- The scene audit caught duplicate RetailTrim_ actors: the demo builder's clear list
  did not include the new label, so every rebuild stacked another copy.

Still open and not fixable by geometry: wall lettering. A plate at 0.10 m does not
read at 40 m, so it moves to the 0.9.0 decal route (TODO.md, Screenshots/README.md).
2026-11-09 09:54:33 +01:00
zwitschi 4132f42ca1 plan: give realistic buildings their own minor milestone (0.9.0)
The facade work started as a patch (0.1.4) and the 0.1.6 retail pass showed what
that costs: a patch has no release gate of its own, so a first pass that does not
yet read as the building can sit inside a done patch series. Realism is also the
leading quality goal, which a patch cannot carry.

New 0.9.0 - Realistic buildings, before the feature freeze: facade system with more
than one order, a 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. Former 0.9.0 freeze/beta/audit becomes 0.10.0 with
patches 0.10.1-0.10.3; 1.0.0 moves to weeks 39-44.

Recorded as ADR-8 (append-only) with a measurable gate BR-1..BR-5 in the quality
requirements, mirrored into the combined architecture.md. The 0.1.6 item now says
which of its gaps move to 0.9.0 and why more geometry cannot close them.
2026-11-09 09:54:15 +01:00
zwitschi c53386fcf4 docs(review): embed every screenshot inline so the review happens in the file
Each shot now has its own section with the image embedded, its view, expected
content and both verdicts, plus a status table at the top that links to them.
A path alone was not a review: the owner had to open five PNGs to judge a pass.

Also recorded in AGENTS.md (a path is not a review; approval ticks both the section
and the table row), the two retail assets and tools in the arc42 building block
view and the combined architecture.md, and prettier-formatted the changed files.
2026-11-09 09:33:23 +01:00
zwitschi 2614d259aa feat(facades): 0.1.6 retail frontage first pass — Lee Drug and 6806-6820
Models the two retail buildings beside the hero from their own photograph rather
than the generic kit: bronze cornice, mural row, lettering band, stepped Deco
parapet and rounded corner tower on 6800 Lee Drug; burgundy fascia, pilasters,
white-framed windows and banded parapet on 6806-6820. Reference analysis written
before any geometry (Art/blender/retail/ref-analysis.md), every unmeasured
proportion carried into the part report as an assumption.

6822 Budget is deferred, not guessed: no photograph of it exists and the 6.0 x 5.5 m
sliver cannot be read out of the neighbour shot.

Pipeline fixes found on the way:
- render_screenshots.sh called ./Engine/... for the capture step while every
  commandlet used the absolute path; set -e killed the script at 127 *after* the
  maps were rebuilt, so screenshots silently stayed a day old.
- the facade kit applies to every Frontage_*/North_* actor, so it pasted its own
  language over the new modelled facades; it now skips them, and the trim stages as
  RetailTrim_*.
- pure bay arithmetic moved to Tools/_common.py so it is unit-testable without
  Blender (4 new tests, 82 total pass).

Known gaps recorded in Screenshots/README.md, not hidden: the parapet reads as a
blank band, murals and lettering do not resolve at review distance, the tower is a
plain cylinder, and the review camera crops the east end.
2026-11-09 09:10:49 +01:00
zwitschi 4e703fcfd9 todo: claim 0.1.6 retail frontage on branch facade-retail-0.1.6 2026-11-09 08:59:07 +01:00
zwitschi 73aca9db80 feat(facades): 0.1.5 — hero detail, scene audit, material orphaning fixed (#32)
Merged PR #32: 0.1.5 — hero detail, scene audit, material orphaning fixed

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 22:56:08 +01:00
zwitschi 743a290ac9 Merge pull request 'docs(arch): realism leads the quality list; performance moves to the end' (#31) from arch-quality-priority into main
Reviewed-on: #31
2026-11-08 20:49:20 +01:00
zwitschi 71c7ca734b docs(arch): realism leads the quality list; performance moves to the end
Owner decision: prioritise realism, keep the performance target but put it last.
Re-applied on a branch after the first attempt landed directly on main (reverted
there; see b8209ce).

- Quality goals (01 + combined architecture.md): 1 visual realism, 2 geographic
  fidelity, 3 walkability/usability, 4 reproducibility/portability, 5 provenance,
  6 performance efficiency.
- Quality requirements (10 + combined) reordered to match, with two realism
  requirements added (a façade reads like the photographs; placement within ~1 m)
  and the scenarios renumbered so QS-1 is the reference comparison, QS-9 the frame
  rate.
- Guard on both documents so the order cannot become an excuse: performance is a
  floor, not a preference - 60 FPS at 1440p without frame generation must be met at
  every release; if realism cannot fit, cut the realism scope deliberately and
  record it, never the frame rate. Walkability outranks realism in one direction: a
  block you fall through is not a demo.
- Solution strategy and 'quality achievement' lead with realism and describe
  performance as a budget, not a driver.
- Docs/release-checks.md: the performance check is the last section and the per-
  version check list now opens with realism and closes with performance.
- ROADMAP gains a 'Quality priority' section; AGENTS.md gains the tie-break rule
  beside the existing scope-cutting rule.
2026-11-08 20:37:32 +01:00
zwitschi b8209ce82b Revert "docs(arch): realism leads the quality list; performance moves to the end"
This reverts commit a1d52aa583.
2026-11-08 20:37:19 +01:00
zwitschi a1d52aa583 docs(arch): realism leads the quality list; performance moves to the end
Owner decision: prioritise realism, keep the performance target but put it last.

- Quality goals reordered (01 + combined architecture.md): 1 visual realism,
  2 geographic fidelity, 3 walkability/usability, 4 reproducibility/portability,
  5 provenance, 6 performance efficiency.
- Quality requirements (10 + combined) reordered to match, with two new realism
  requirements (façade reads like the photographs; placement within ~1 m) and the
  quality scenarios renumbered so QS-1 is the reference comparison and QS-9 the
  frame rate.
- Both documents carry the same guard so the order cannot become an excuse:
  performance is a floor, not a preference - 60 FPS at 1440p without frame
  generation must be met at every release; if realism cannot fit, the realism scope
  is cut deliberately and recorded, never the frame rate. Walkability outranks
  realism in one direction: a block you fall through is not a demo.
- Solution strategy and 'quality achievement' now lead with realism and describe
  performance as a budget rather than a driver.
- Docs/release-checks.md: the performance check is the last check, with that reason
  stated; ROADMAP gains a 'Quality priority' section; AGENTS.md gains the tie-break
  rule beside the existing scope-cutting rule.
2026-11-08 20:37:03 +01:00
103 changed files with 4079 additions and 304 deletions

No files matched your search

+3 -2
View File
@@ -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`. - 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`. - 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. - 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 ## 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/`). 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. 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** (`![alt](demo/<shot>.png)`) 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. 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`. 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.
+11 -2
View File
@@ -37,6 +37,15 @@
], ],
"evidence": "6 columns at 4.96 m centres (sibling notes x measured frontage)" "evidence": "6 columns at 4.96 m centres (sibling notes x measured frontage)"
}, },
{
"object": "Hero_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", "object": "Hero_ColumnBase_x6",
"dims_m": [ "dims_m": [
@@ -104,10 +113,10 @@
"object": "Hero_MottoBand", "object": "Hero_MottoBand",
"dims_m": [ "dims_m": [
22.75, 22.75,
0.08, 0.1,
0.85 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", "object": "Hero_Medallion_x2",
+64
View File
@@ -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.
+116 -1
View File
@@ -8,6 +8,120 @@ Format: [Keep a Changelog](https://keepachangelog.com/) style, versions follow [
Nothing yet. Nothing yet.
## [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 ## [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. 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 +286,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. - Kerb height (0.13 m as built vs 0.15 m assumed), kerb radii, driveway aprons and graded crossing ramps are deferred to `0.1.1`; crossing ramps are kerb flushes.
- One block only; no vehicles, NPCs, audio, or settings UI yet. - One block only; no vehicles, NPCs, audio, or settings UI yet.
[Unreleased]: https://git.allucanget.biz/zwitschi/gig-line/compare/v0.1.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.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.3]: https://git.allucanget.biz/zwitschi/gig-line/releases/tag/v0.1.3
[0.1.2]: https://git.allucanget.biz/zwitschi/gig-line/releases/tag/v0.1.2 [0.1.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 | | Priority | Quality Goal | Scenario |
| --- | --- | --- | | --- | --- | --- |
| 1 | **Performance efficiency** | 60 FPS at 2560×1440 on RTX 5070 Ti, documented preset, no frame generation | | 1 | **Visual realism** | Renders read as the reference photographs: façade identity, proportion, material and light — judged side by side, shot against photo |
| 2 | **Terrain walkability** | Zero raycast holes across both sidewalks and the carriageway (84-sample transect passes) | | 2 | **Geographic fidelity** | Building placement and street scale match the measured data and the photographs |
| 3 | **Geographic fidelity** | Building placement, street scale, and façade identity match reference photography | | 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 | | 4 | **Build reproducibility** | Fresh checkout + documented engine path produces a runnable Linux build |
| 5 | **Provenance** | Every committed asset has recorded provenance | | 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 ## Stakeholders
+2 -1
View File
@@ -30,7 +30,8 @@ All modelling and geographic processing live outside the game project in `Art/`,
## Quality achievement ## 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. - **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.
+8 -1
View File
@@ -25,7 +25,9 @@ C++ classes in `Game/GigLine/Source/GigLine/`:
| `AGigLineShotActor` | Screenshot camera walker | Drives cameras in `-game` mode, `FScreenshotRequest`, quits | | `AGigLineShotActor` | Screenshot camera walker | Drives cameras in `-game` mode, `FScreenshotRequest`, quits |
| `UGigLineAssetTools` | C++ collision helpers | `SetComplexCollisionAsSimple` on `StaticMesh` assets | | `UGigLineAssetTools` | C++ collision helpers | `SetComplexCollisionAsSimple` on `StaticMesh` assets |
Input assets in `/Game/Input/`: `IA_Move`, `IA_MoveBack`, `IA_Strafe`, `IA_StrafeLeft`, `IA_Look`, `IA_Jump`, `IA_Reset`, `IA_Pause`, bound in `IMC_Default`. Input assets in `/Game/Input/`: `IA_Move`, `IA_MoveBack`, `IA_Strafe`, `IA_StrafeLeft`, `IA_Look`, `IA_Jump`, `IA_Reset`, `IA_Pause`, `IA_ToggleCamera`, bound in `IMC_Default`.
Player-facing bindings: **WASD** move, **mouse** look, **Space** jump, **R** reset, **Escape** pause, **V** swap first/third person. The camera and body work landed with `0.2.0` (see ADR-9): the shipped default view stays first person, and `-thirdperson` starts the pawn in the third-person rig — used by the screenshot run so the review is reproducible.
### Site ### Site
@@ -47,6 +49,7 @@ Static meshes and materials in `/Game/Hollywood/Site/`:
| `SM_HeroFacade` | El Capitan Entertainment Centre detailed façade | | `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_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_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 ### Art Pipeline
@@ -56,11 +59,15 @@ Python scripts in `Tools/` drive the content pipeline:
| --- | --- | | --- | --- |
| `export_*`, `hero_facade_build` | Blender headless export (FBX) | | `export_*`, `hero_facade_build` | Blender headless export (FBX) |
| `ue_import_*`, `ue_hero_facade_import` | Unreal headless import + placement | | `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_build_demo_map`, `ue_build_review_map` | Map assembly (lighting, meshes, player start) |
| `ue_create_input_assets` | Enhanced Input actions + mapping context | | `ue_create_input_assets` | Enhanced Input actions + mapping context |
| `render_screenshots.sh` | Screenshot capture pipeline | | `render_screenshots.sh` | Screenshot capture pipeline |
| `judge_screenshot.py` | Vision-model pre-check (optional) | | `judge_screenshot.py` | Vision-model pre-check (optional) |
| `input_emulation.py`, `input_test.py` | OS-level virtual keyboard/mouse injection for input verification | | `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`) | | `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 | | `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. - **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. - **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.
+35 -15
View File
@@ -2,24 +2,44 @@
## Quality Requirements Overview ## 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 | | 1 | Realism / Fidelity | Façade identity, proportion, material and lighting read like the reference photographs when compared side by side |
| Reliability / Terrain | Zero raycast holes across sidewalks and carriageway (84-sample transect) | 1 | | 2 | Realism / Fidelity | Building placement matches the measured footprints and the photographs (within ~1 m) |
| Accuracy / Fidelity | Building placement matches reference photography within ~1 m | 2 | | 2 | Realism / Fidelity | Street scale (lane width, sidewalk width) within ~10 cm of the measured profile |
| Accuracy / Fidelity | Street scale (lane width, sidewalk width) within ~10 cm of measured profile | 2 | | 3 | Reliability / Terrain | Zero raycast holes across sidewalks and carriageway (84-sample transect) |
| Usability | WASD + mouse look, jump, reset (R), pause (Escape) — no tutorial needed | 2 | | 3 | Usability | WASD + mouse look, jump, reset (R), pause (Escape) — no tutorial needed |
| Maintainability | Fresh checkout builds with documented engine installation | 2 | | 4 | Maintainability | Fresh checkout builds with the documented engine installation |
| Portability | Linux build packages and runs; Windows build ships | 3 | | 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 ## Quality Scenarios
| ID | Source/Stimulus | Response | Metric | | 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-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 | Player holds W for 2 s on flat road | Character moves forward | ≥ 200 cm displacement, Z constant within ±5 cm | | 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 presses R | Character teleports to spawn | < 100 cm from `StartLocation` | | 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 presses Escape | Game pauses, cursor appears | `IsGamePaused()` = true | | 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 Escape while paused | Game unpauses, cursor hides | `IsGamePaused()` = false | | QS-5 | Player presses R | Character teleports to spawn | < 100 cm from `StartLocation` |
| QS-6 | Game runs on dev workstation at 1440p | Frame rate stable | ≥ 60 FPS, 99th percentile ≥ 55 FPS | | QS-6 | Player presses Escape | Game pauses, cursor appears | `IsGamePaused()` = true |
| QS-7 | Fresh checkout + engine install | Build succeeds | Exit code 0, runnable binary produced | | 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 |
+68 -20
View File
@@ -27,11 +27,14 @@ Full requirements and progress are in [TODO.md](../../TODO.md); version directio
| Priority | Quality Goal | Scenario | | Priority | Quality Goal | Scenario |
| --- | --- | --- | | --- | --- | --- |
| 1 | **Performance efficiency** | 60 FPS at 2560×1440 on RTX 5070 Ti, documented preset, no frame generation | | 1 | **Visual realism** | Renders read as the reference photographs: façade identity, proportion, material and light — judged side by side, shot against photo |
| 2 | **Terrain walkability** | Zero raycast holes across both sidewalks and the carriageway (84-sample transect passes) | | 2 | **Geographic fidelity** | Building placement and street scale match the measured data and the photographs |
| 3 | **Geographic fidelity** | Building placement, street scale, and façade identity match reference photography | | 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 | | 4 | **Build reproducibility** | Fresh checkout + documented engine path produces a runnable Linux build |
| 5 | **Provenance** | Every committed asset has recorded provenance | | 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 ## Stakeholders
@@ -129,7 +132,9 @@ All modelling and geographic processing live outside the game project in `Art/`,
## Quality achievement ## 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. - **Walkability**: complex-as-simple collision on terrain (C++ helper forces the flag on the asset), block-all collision on every site mesh, swept transect QA verifies zero raycast holes.
@@ -164,7 +169,9 @@ C++ classes in `Game/GigLine/Source/GigLine/`:
| `AGigLineShotActor` | Screenshot camera walker | Drives cameras in `-game` mode, `FScreenshotRequest`, quits | | `AGigLineShotActor` | Screenshot camera walker | Drives cameras in `-game` mode, `FScreenshotRequest`, quits |
| `UGigLineAssetTools` | C++ collision helpers | `SetComplexCollisionAsSimple` on `StaticMesh` assets | | `UGigLineAssetTools` | C++ collision helpers | `SetComplexCollisionAsSimple` on `StaticMesh` assets |
Input assets in `/Game/Input/`: `IA_Move`, `IA_MoveBack`, `IA_Strafe`, `IA_StrafeLeft`, `IA_Look`, `IA_Jump`, `IA_Reset`, `IA_Pause`, bound in `IMC_Default`. Input assets in `/Game/Input/`: `IA_Move`, `IA_MoveBack`, `IA_Strafe`, `IA_StrafeLeft`, `IA_Look`, `IA_Jump`, `IA_Reset`, `IA_Pause`, `IA_ToggleCamera`, bound in `IMC_Default`.
Player-facing bindings: **WASD** move, **mouse** look, **Space** jump, **R** reset, **Escape** pause, **V** swap first/third person. The camera and body work landed with `0.2.0` (see ADR-9): the shipped default view stays first person, and `-thirdperson` starts the pawn in the third-person rig — used by the screenshot run so the review is reproducible.
### Site ### Site
@@ -186,6 +193,7 @@ Static meshes and materials in `/Game/Hollywood/Site/`:
| `SM_HeroFacade` | El Capitan Entertainment Centre detailed façade | | `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_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_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 ### Art Pipeline
@@ -195,11 +203,15 @@ Python scripts in `Tools/` drive the content pipeline:
| --- | --- | | --- | --- |
| `export_*`, `hero_facade_build` | Blender headless export (FBX) | | `export_*`, `hero_facade_build` | Blender headless export (FBX) |
| `ue_import_*`, `ue_hero_facade_import` | Unreal headless import + placement | | `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_build_demo_map`, `ue_build_review_map` | Map assembly (lighting, meshes, player start) |
| `ue_create_input_assets` | Enhanced Input actions + mapping context | | `ue_create_input_assets` | Enhanced Input actions + mapping context |
| `render_screenshots.sh` | Screenshot capture pipeline | | `render_screenshots.sh` | Screenshot capture pipeline |
| `judge_screenshot.py` | Vision-model pre-check (optional) | | `judge_screenshot.py` | Vision-model pre-check (optional) |
| `input_emulation.py`, `input_test.py` | OS-level virtual keyboard/mouse injection for input verification | | `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`) | | `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 | | `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. - **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
## Quality Requirements Overview ## 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 | | 1 | Realism / Fidelity | Façade identity, proportion, material and lighting read like the reference photographs when compared side by side |
| Reliability / Terrain | Zero raycast holes across sidewalks and carriageway (84-sample transect) | 1 | | 2 | Realism / Fidelity | Building placement matches the measured footprints and the photographs (within ~1 m) |
| Accuracy / Fidelity | Building placement matches reference photography within ~1 m | 2 | | 2 | Realism / Fidelity | Street scale (lane width, sidewalk width) within ~10 cm of the measured profile |
| Accuracy / Fidelity | Street scale (lane width, sidewalk width) within ~10 cm of measured profile | 2 | | 3 | Reliability / Terrain | Zero raycast holes across sidewalks and carriageway (84-sample transect) |
| Usability | WASD + mouse look, jump, reset (R), pause (Escape) — no tutorial needed | 2 | | 3 | Usability | WASD + mouse look, jump, reset (R), pause (Escape) — no tutorial needed |
| Maintainability | Fresh checkout builds with documented engine installation | 2 | | 4 | Maintainability | Fresh checkout builds with the documented engine installation |
| Portability | Linux build packages and runs; Windows build ships | 3 | | 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 ## Quality Scenarios
| ID | Source/Stimulus | Response | Metric | | 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-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 | Player holds W for 2 s on flat road | Character moves forward | ≥ 200 cm displacement, Z constant within ±5 cm | | 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 presses R | Character teleports to spawn | < 100 cm from `StartLocation` | | 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 presses Escape | Game pauses, cursor appears | `IsGamePaused()` = true | | 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 Escape while paused | Game unpauses, cursor hides | `IsGamePaused()` = false | | QS-5 | Player presses R | Character teleports to spawn | < 100 cm from `StartLocation` |
| QS-6 | Game runs on dev workstation at 1440p | Frame rate stable | ≥ 60 FPS, 99th percentile ≥ 55 FPS | | QS-6 | Player presses Escape | Game pauses, cursor appears | `IsGamePaused()` = true |
| QS-7 | Fresh checkout + engine install | Build succeeds | Exit code 0, runnable binary produced | | 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 # Risks and Technical Debts
+5
View File
@@ -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/*` | | North import | `Tools/ue_import_north_buildings.py` | `/Game/Hollywood/North/*` |
| Hero import | `Tools/ue_hero_facade_import.py` | `/Game/Hollywood/ElCapitan/SM_HeroFacade` | | Hero import | `Tools/ue_hero_facade_import.py` | `/Game/Hollywood/ElCapitan/SM_HeroFacade` |
| Demo map | `Tools/ue_build_demo_map.py` | `/Game/Hollywood/DemoMap` | | Demo map | `Tools/ue_build_demo_map.py` | `/Game/Hollywood/DemoMap` |
| 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/` | | 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). Every Blender exporter calls [`Tools/fbx_export_preset.py`](../Tools/fbx_export_preset.py) (`ue_metric_v1` / `ue_character_v1`) instead of repeating the FBX argument block — one place for the scale conversion, the axis mapping and `mesh_smooth_type="FACE"` (without which every Unreal import warns about missing smoothing groups and round geometry arrives faceted).
Two rules that the pipeline enforces rather than documents: Two rules that the pipeline enforces rather than documents:
+18 -1
View File
@@ -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. 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,7 @@ Keyboard and mouse. The same list is shown in-game, bottom-right of the frame (`
| Jump | `Space` | | | Jump | `Space` | |
| Reset to start | `R` | Returns to the grounded spawn point | | Reset to start | `R` | Returns to the grounded spawn point |
| Pause / release mouse | `Escape` | Second press resumes and recaptures the mouse | | 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 |
## Notes ## Notes
@@ -19,6 +20,22 @@ 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. - 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`. - 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.
## 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 |
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 ## Testing input without a physical keyboard
`Tools/input_test.py` injects OS-level keyboard/mouse events through a virtual `/dev/uinput` device (see `Tools/input_emulation.py`), e.g.: `Tools/input_test.py` injects OS-level keyboard/mouse events through a virtual `/dev/uinput` device (see `Tools/input_emulation.py`), e.g.:
Binary file not shown.
+728
View File
@@ -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
1 frame ms
2 0 2.599
3 1 2.877
4 2 2.709
5 3 2.646
6 4 3.168
7 5 2.729
8 6 2.524
9 7 2.726
10 8 2.639
11 9 2.814
12 10 2.819
13 11 2.708
14 12 2.822
15 13 2.703
16 14 2.365
17 15 2.796
18 16 2.899
19 17 2.972
20 18 2.908
21 19 2.356
22 20 2.781
23 21 2.574
24 22 3.148
25 23 2.306
26 24 2.668
27 25 2.855
28 26 2.588
29 27 3.321
30 28 3.419
31 29 3.121
32 30 3.391
33 31 2.355
34 32 2.679
35 33 3.463
36 34 2.614
37 35 2.922
38 36 2.429
39 37 2.830
40 38 2.842
41 39 2.782
42 40 3.252
43 41 2.731
44 42 2.577
45 43 2.363
46 44 2.786
47 45 2.547
48 46 2.594
49 47 2.652
50 48 2.836
51 49 2.792
52 50 2.552
53 51 2.792
54 52 2.718
55 53 3.333
56 54 2.165
57 55 2.880
58 56 2.613
59 57 2.460
60 58 2.919
61 59 2.770
62 60 2.551
63 61 2.622
64 62 2.712
65 63 2.818
66 64 3.155
67 65 2.472
68 66 2.861
69 67 2.651
70 68 2.573
71 69 2.892
72 70 2.689
73 71 2.729
74 72 2.668
75 73 2.753
76 74 2.812
77 75 2.606
78 76 2.686
79 77 2.842
80 78 2.660
81 79 2.903
82 80 2.486
83 81 2.591
84 82 3.626
85 83 2.455
86 84 2.475
87 85 2.922
88 86 2.603
89 87 2.728
90 88 3.057
91 89 2.731
92 90 3.632
93 91 1.950
94 92 2.694
95 93 3.020
96 94 2.230
97 95 2.796
98 96 2.707
99 97 2.807
100 98 2.658
101 99 2.697
102 100 2.453
103 101 3.051
104 102 2.758
105 103 2.662
106 104 2.599
107 105 2.733
108 106 2.751
109 107 2.894
110 108 2.501
111 109 2.753
112 110 2.873
113 111 2.439
114 112 2.634
115 113 2.682
116 114 2.913
117 115 2.692
118 116 2.735
119 117 2.758
120 118 2.726
121 119 2.907
122 120 2.568
123 121 2.764
124 122 2.815
125 123 2.530
126 124 2.684
127 125 2.719
128 126 2.846
129 127 2.879
130 128 2.647
131 129 2.770
132 130 2.477
133 131 2.657
134 132 2.675
135 133 2.917
136 134 2.700
137 135 2.493
138 136 2.678
139 137 2.786
140 138 3.004
141 139 2.697
142 140 2.642
143 141 2.624
144 142 2.684
145 143 2.717
146 144 2.743
147 145 2.740
148 146 2.825
149 147 2.849
150 148 2.991
151 149 2.297
152 150 3.084
153 151 2.459
154 152 3.214
155 153 2.260
156 154 2.550
157 155 2.621
158 156 2.660
159 157 2.739
160 158 2.574
161 159 3.078
162 160 2.735
163 161 2.607
164 162 2.793
165 163 2.798
166 164 2.584
167 165 2.637
168 166 2.627
169 167 2.726
170 168 2.656
171 169 2.788
172 170 2.674
173 171 2.782
174 172 2.766
175 173 2.613
176 174 2.724
177 175 2.770
178 176 2.709
179 177 2.700
180 178 2.600
181 179 2.686
182 180 2.747
183 181 2.841
184 182 2.601
185 183 2.666
186 184 2.846
187 185 2.599
188 186 2.806
189 187 3.126
190 188 2.521
191 189 2.634
192 190 2.716
193 191 2.584
194 192 2.714
195 193 2.923
196 194 2.277
197 195 2.706
198 196 2.606
199 197 2.758
200 198 2.747
201 199 2.958
202 200 2.798
203 201 2.387
204 202 2.596
205 203 2.861
206 204 2.801
207 205 2.521
208 206 2.869
209 207 2.884
210 208 2.515
211 209 2.592
212 210 3.723
213 211 2.474
214 212 2.736
215 213 2.774
216 214 2.769
217 215 3.007
218 216 2.768
219 217 2.716
220 218 2.694
221 219 2.722
222 220 2.862
223 221 2.587
224 222 2.833
225 223 2.792
226 224 2.746
227 225 2.585
228 226 2.741
229 227 2.585
230 228 2.802
231 229 2.674
232 230 2.742
233 231 2.718
234 232 2.881
235 233 2.707
236 234 2.585
237 235 2.810
238 236 2.948
239 237 2.718
240 238 2.773
241 239 2.571
242 240 2.788
243 241 2.702
244 242 2.634
245 243 2.788
246 244 2.678
247 245 2.914
248 246 2.527
249 247 2.637
250 248 2.877
251 249 2.693
252 250 2.644
253 251 2.670
254 252 2.700
255 253 2.877
256 254 2.638
257 255 2.617
258 256 2.774
259 257 2.660
260 258 2.731
261 259 2.671
262 260 2.838
263 261 2.726
264 262 2.671
265 263 2.744
266 264 2.735
267 265 2.684
268 266 3.850
269 267 2.120
270 268 2.669
271 269 2.729
272 270 2.712
273 271 2.817
274 272 2.926
275 273 2.678
276 274 2.786
277 275 2.927
278 276 2.493
279 277 2.846
280 278 2.610
281 279 2.644
282 280 2.577
283 281 2.939
284 282 2.727
285 283 2.595
286 284 2.819
287 285 2.778
288 286 2.774
289 287 2.628
290 288 2.711
291 289 2.699
292 290 2.715
293 291 2.763
294 292 2.710
295 293 2.892
296 294 2.721
297 295 2.575
298 296 3.608
299 297 2.811
300 298 2.739
301 299 2.751
302 300 2.608
303 301 2.913
304 302 3.052
305 303 2.862
306 304 2.653
307 305 2.727
308 306 2.646
309 307 2.888
310 308 3.338
311 309 2.743
312 310 2.639
313 311 2.681
314 312 2.846
315 313 2.766
316 314 3.184
317 315 2.834
318 316 2.728
319 317 2.581
320 318 2.770
321 319 3.351
322 320 3.157
323 321 2.508
324 322 2.835
325 323 2.754
326 324 2.411
327 325 3.398
328 326 2.802
329 327 2.611
330 328 3.058
331 329 2.407
332 330 2.640
333 331 3.645
334 332 2.729
335 333 2.580
336 334 2.902
337 335 2.523
338 336 3.005
339 337 3.173
340 338 2.840
341 339 2.610
342 340 2.576
343 341 2.706
344 342 3.405
345 343 2.849
346 344 2.678
347 345 2.796
348 346 2.782
349 347 2.636
350 348 3.475
351 349 2.798
352 350 2.745
353 351 2.564
354 352 2.792
355 353 2.714
356 354 3.496
357 355 2.841
358 356 2.465
359 357 2.708
360 358 2.692
361 359 2.934
362 360 3.304
363 361 2.561
364 362 2.689
365 363 2.749
366 364 2.780
367 365 2.940
368 366 3.025
369 367 2.706
370 368 2.881
371 369 2.619
372 370 2.716
373 371 3.133
374 372 2.874
375 373 2.816
376 374 2.414
377 375 2.737
378 376 2.797
379 377 2.651
380 378 2.920
381 379 3.032
382 380 2.606
383 381 2.710
384 382 2.649
385 383 2.700
386 384 3.469
387 385 2.224
388 386 2.567
389 387 2.733
390 388 2.511
391 389 2.950
392 390 3.979
393 391 2.798
394 392 2.487
395 393 2.740
396 394 2.741
397 395 2.718
398 396 3.291
399 397 2.769
400 398 2.697
401 399 2.796
402 400 2.641
403 401 2.652
404 402 3.151
405 403 2.673
406 404 2.741
407 405 2.606
408 406 2.665
409 407 2.859
410 408 2.739
411 409 2.586
412 410 2.910
413 411 2.745
414 412 2.698
415 413 2.670
416 414 2.797
417 415 2.829
418 416 2.643
419 417 2.776
420 418 2.772
421 419 2.530
422 420 2.567
423 421 2.826
424 422 2.782
425 423 2.738
426 424 2.640
427 425 2.967
428 426 2.600
429 427 3.020
430 428 2.502
431 429 2.642
432 430 2.625
433 431 2.770
434 432 2.659
435 433 2.717
436 434 2.698
437 435 2.780
438 436 2.992
439 437 2.686
440 438 2.560
441 439 3.048
442 440 2.721
443 441 2.507
444 442 2.694
445 443 2.695
446 444 2.892
447 445 3.443
448 446 2.633
449 447 2.908
450 448 2.823
451 449 2.346
452 450 2.599
453 451 2.914
454 452 2.774
455 453 3.391
456 454 2.384
457 455 2.585
458 456 2.667
459 457 2.704
460 458 2.674
461 459 2.755
462 460 2.718
463 461 2.804
464 462 2.583
465 463 2.694
466 464 2.797
467 465 3.079
468 466 2.434
469 467 2.723
470 468 3.671
471 469 2.211
472 470 2.567
473 471 2.701
474 472 2.717
475 473 2.657
476 474 2.581
477 475 2.889
478 476 3.006
479 477 2.595
480 478 2.703
481 479 2.498
482 480 2.694
483 481 2.915
484 482 2.655
485 483 2.686
486 484 2.833
487 485 2.717
488 486 2.723
489 487 2.451
490 488 2.913
491 489 2.784
492 490 2.684
493 491 2.651
494 492 2.809
495 493 2.819
496 494 2.676
497 495 2.851
498 496 2.775
499 497 2.638
500 498 2.520
501 499 2.824
502 500 2.930
503 501 2.757
504 502 2.608
505 503 2.808
506 504 2.508
507 505 2.819
508 506 2.654
509 507 2.748
510 508 2.705
511 509 2.798
512 510 2.559
513 511 2.694
514 512 2.821
515 513 2.706
516 514 2.612
517 515 2.928
518 516 2.475
519 517 2.830
520 518 2.658
521 519 2.784
522 520 2.572
523 521 2.935
524 522 2.688
525 523 2.600
526 524 2.675
527 525 2.828
528 526 2.774
529 527 2.848
530 528 2.764
531 529 2.379
532 530 2.962
533 531 2.989
534 532 2.528
535 533 2.639
536 534 2.906
537 535 2.543
538 536 2.582
539 537 2.949
540 538 2.813
541 539 2.504
542 540 2.754
543 541 2.638
544 542 2.695
545 543 2.662
546 544 2.922
547 545 2.616
548 546 2.810
549 547 2.675
550 548 2.551
551 549 2.894
552 550 2.754
553 551 2.632
554 552 2.748
555 553 2.827
556 554 2.751
557 555 2.759
558 556 2.684
559 557 2.766
560 558 2.948
561 559 2.461
562 560 2.621
563 561 2.772
564 562 2.985
565 563 2.582
566 564 2.897
567 565 2.574
568 566 2.661
569 567 2.742
570 568 2.797
571 569 2.632
572 570 2.755
573 571 2.878
574 572 2.759
575 573 3.812
576 574 2.651
577 575 2.862
578 576 2.577
579 577 2.634
580 578 3.173
581 579 2.418
582 580 2.714
583 581 2.746
584 582 2.794
585 583 2.774
586 584 2.583
587 585 3.659
588 586 3.222
589 587 2.489
590 588 2.868
591 589 2.558
592 590 2.586
593 591 2.599
594 592 2.930
595 593 2.401
596 594 2.540
597 595 2.889
598 596 2.733
599 597 2.664
600 598 2.877
601 599 2.588
602 600 2.677
603 601 2.897
604 602 2.672
605 603 2.780
606 604 2.740
607 605 2.780
608 606 2.493
609 607 2.794
610 608 2.631
611 609 2.768
612 610 2.870
613 611 2.626
614 612 2.862
615 613 2.602
616 614 2.868
617 615 2.627
618 616 2.638
619 617 2.702
620 618 2.904
621 619 2.918
622 620 2.487
623 621 3.225
624 622 2.350
625 623 2.820
626 624 2.837
627 625 2.408
628 626 2.593
629 627 3.526
630 628 2.554
631 629 2.746
632 630 2.684
633 631 2.729
634 632 2.762
635 633 2.768
636 634 3.034
637 635 2.514
638 636 2.732
639 637 2.739
640 638 2.692
641 639 2.832
642 640 2.810
643 641 2.484
644 642 2.632
645 643 3.284
646 644 2.311
647 645 3.079
648 646 2.656
649 647 2.683
650 648 2.622
651 649 2.668
652 650 2.779
653 651 2.796
654 652 2.874
655 653 2.427
656 654 2.644
657 655 2.872
658 656 2.867
659 657 2.404
660 658 2.917
661 659 2.773
662 660 2.857
663 661 2.789
664 662 2.443
665 663 2.651
666 664 2.839
667 665 2.814
668 666 2.575
669 667 2.796
670 668 2.709
671 669 2.873
672 670 2.670
673 671 2.936
674 672 2.496
675 673 2.608
676 674 2.641
677 675 2.793
678 676 2.765
679 677 2.736
680 678 2.679
681 679 2.987
682 680 2.884
683 681 2.469
684 682 2.774
685 683 2.948
686 684 2.598
687 685 3.478
688 686 2.856
689 687 4.623
690 688 3.038
691 689 2.507
692 690 2.717
693 691 2.682
694 692 2.807
695 693 2.778
696 694 2.699
697 695 2.802
698 696 2.605
699 697 2.650
700 698 2.789
701 699 2.791
702 700 3.015
703 701 2.539
704 702 2.790
705 703 2.698
706 704 2.637
707 705 2.657
708 706 2.800
709 707 2.720
710 708 2.702
711 709 2.845
712 710 2.583
713 711 2.780
714 712 2.782
715 713 2.593
716 714 2.695
717 715 2.747
718 716 2.871
719 717 2.671
720 718 2.676
721 719 2.991
722 720 2.727
723 721 2.576
724 722 2.539
725 723 2.770
726 724 2.712
727 725 2.818
728 726 2.615
+760
View File
@@ -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
1 frame ms
2 0 2.428
3 1 2.633
4 2 2.238
5 3 3.606
6 4 2.431
7 5 2.649
8 6 4.050
9 7 2.011
10 8 2.735
11 9 3.390
12 10 2.901
13 11 2.237
14 12 2.686
15 13 2.383
16 14 2.451
17 15 3.412
18 16 2.458
19 17 2.474
20 18 2.588
21 19 2.556
22 20 2.478
23 21 3.150
24 22 2.667
25 23 2.349
26 24 2.568
27 25 2.580
28 26 2.393
29 27 2.575
30 28 3.132
31 29 2.536
32 30 2.508
33 31 2.710
34 32 2.348
35 33 2.537
36 34 2.820
37 35 3.162
38 36 2.163
39 37 2.562
40 38 2.619
41 39 2.323
42 40 2.628
43 41 3.075
44 42 2.512
45 43 2.448
46 44 2.505
47 45 2.454
48 46 2.607
49 47 3.128
50 48 2.476
51 49 2.562
52 50 2.434
53 51 3.385
54 52 2.076
55 53 3.435
56 54 2.672
57 55 2.550
58 56 1.998
59 57 2.583
60 58 2.518
61 59 2.777
62 60 3.920
63 61 2.934
64 62 2.675
65 63 2.320
66 64 2.309
67 65 2.520
68 66 3.947
69 67 2.596
70 68 2.708
71 69 2.291
72 70 2.404
73 71 2.551
74 72 3.715
75 73 2.100
76 74 2.539
77 75 2.682
78 76 2.273
79 77 2.442
80 78 3.952
81 79 2.233
82 80 2.374
83 81 2.686
84 82 2.407
85 83 2.654
86 84 3.613
87 85 2.320
88 86 2.685
89 87 2.744
90 88 2.357
91 89 2.565
92 90 3.756
93 91 2.672
94 92 2.128
95 93 2.583
96 94 2.546
97 95 2.391
98 96 2.935
99 97 3.624
100 98 1.934
101 99 2.509
102 100 2.603
103 101 2.449
104 102 2.802
105 103 3.678
106 104 2.336
107 105 2.509
108 106 2.551
109 107 2.576
110 108 2.997
111 109 3.208
112 110 2.301
113 111 2.359
114 112 2.484
115 113 2.646
116 114 2.801
117 115 3.226
118 116 2.635
119 117 2.325
120 118 2.501
121 119 2.602
122 120 2.564
123 121 3.151
124 122 3.149
125 123 2.235
126 124 2.596
127 125 2.432
128 126 2.665
129 127 2.648
130 128 3.884
131 129 2.087
132 130 2.375
133 131 2.586
134 132 2.734
135 133 2.338
136 134 3.617
137 135 2.418
138 136 2.350
139 137 2.713
140 138 2.758
141 139 2.239
142 140 3.585
143 141 2.565
144 142 2.787
145 143 2.115
146 144 2.435
147 145 2.489
148 146 3.062
149 147 2.958
150 148 2.402
151 149 2.425
152 150 2.516
153 151 2.505
154 152 2.543
155 153 3.690
156 154 2.060
157 155 2.510
158 156 2.428
159 157 2.603
160 158 2.491
161 159 3.178
162 160 2.496
163 161 2.424
164 162 2.495
165 163 2.667
166 164 2.434
167 165 3.040
168 166 2.949
169 167 2.094
170 168 2.510
171 169 2.586
172 170 2.395
173 171 3.004
174 172 3.009
175 173 2.288
176 174 2.548
177 175 2.337
178 176 2.598
179 177 2.498
180 178 3.526
181 179 2.533
182 180 2.134
183 181 2.560
184 182 2.541
185 183 2.436
186 184 2.635
187 185 3.361
188 186 2.261
189 187 2.566
190 188 2.383
191 189 2.529
192 190 2.645
193 191 3.190
194 192 2.523
195 193 2.523
196 194 2.346
197 195 2.865
198 196 2.348
199 197 2.879
200 198 3.657
201 199 1.824
202 200 2.551
203 201 2.449
204 202 2.607
205 203 3.085
206 204 3.163
207 205 2.404
208 206 2.327
209 207 2.553
210 208 2.576
211 209 2.544
212 210 3.193
213 211 2.647
214 212 2.332
215 213 2.544
216 214 2.767
217 215 2.429
218 216 3.103
219 217 2.861
220 218 2.318
221 219 2.552
222 220 2.587
223 221 2.352
224 222 3.128
225 223 2.621
226 224 2.461
227 225 2.422
228 226 2.601
229 227 2.748
230 228 2.545
231 229 3.224
232 230 2.206
233 231 2.459
234 232 2.530
235 233 2.461
236 234 2.570
237 235 3.271
238 236 2.405
239 237 2.424
240 238 2.671
241 239 2.450
242 240 2.644
243 241 2.510
244 242 3.023
245 243 2.461
246 244 2.618
247 245 2.350
248 246 2.551
249 247 2.472
250 248 3.642
251 249 2.576
252 250 2.548
253 251 3.599
254 252 1.992
255 253 2.424
256 254 3.752
257 255 2.241
258 256 2.360
259 257 2.522
260 258 2.584
261 259 2.653
262 260 2.871
263 261 2.827
264 262 2.369
265 263 2.482
266 264 2.599
267 265 2.294
268 266 2.686
269 267 3.428
270 268 2.240
271 269 2.435
272 270 2.468
273 271 2.619
274 272 2.519
275 273 3.247
276 274 2.539
277 275 2.442
278 276 2.518
279 277 2.489
280 278 2.463
281 279 2.839
282 280 2.907
283 281 2.786
284 282 2.211
285 283 2.615
286 284 2.378
287 285 2.671
288 286 3.219
289 287 2.435
290 288 2.509
291 289 2.323
292 290 2.502
293 291 2.526
294 292 3.292
295 293 2.468
296 294 2.526
297 295 2.500
298 296 2.431
299 297 2.481
300 298 2.921
301 299 3.009
302 300 2.415
303 301 2.600
304 302 2.344
305 303 2.533
306 304 2.618
307 305 3.458
308 306 2.455
309 307 2.469
310 308 2.495
311 309 2.568
312 310 2.473
313 311 3.179
314 312 2.694
315 313 2.503
316 314 2.587
317 315 2.317
318 316 2.609
319 317 2.590
320 318 3.211
321 319 2.392
322 320 2.490
323 321 2.515
324 322 2.496
325 323 2.483
326 324 3.157
327 325 2.540
328 326 2.422
329 327 2.574
330 328 2.468
331 329 2.547
332 330 2.788
333 331 3.075
334 332 2.231
335 333 2.537
336 334 2.577
337 335 2.584
338 336 2.473
339 337 3.415
340 338 2.126
341 339 2.506
342 340 2.374
343 341 2.721
344 342 2.418
345 343 3.259
346 344 2.509
347 345 2.519
348 346 2.436
349 347 2.472
350 348 2.504
351 349 2.858
352 350 3.250
353 351 2.161
354 352 2.586
355 353 2.376
356 354 2.651
357 355 2.504
358 356 3.399
359 357 2.353
360 358 2.381
361 359 2.513
362 360 2.633
363 361 2.566
364 362 3.615
365 363 2.593
366 364 2.084
367 365 2.411
368 366 2.653
369 367 2.521
370 368 2.825
371 369 2.891
372 370 2.576
373 371 2.302
374 372 2.582
375 373 2.513
376 374 2.598
377 375 3.790
378 376 2.352
379 377 2.306
380 378 2.434
381 379 2.514
382 380 2.708
383 381 2.978
384 382 2.813
385 383 2.327
386 384 2.502
387 385 2.446
388 386 2.605
389 387 3.033
390 388 3.091
391 389 2.206
392 390 2.648
393 391 2.732
394 392 2.296
395 393 3.406
396 394 2.490
397 395 2.424
398 396 2.443
399 397 2.601
400 398 2.628
401 399 2.477
402 400 3.241
403 401 2.573
404 402 2.422
405 403 2.384
406 404 2.505
407 405 2.575
408 406 2.909
409 407 2.897
410 408 2.486
411 409 2.431
412 410 2.691
413 411 2.342
414 412 2.733
415 413 3.518
416 414 2.391
417 415 2.160
418 416 2.671
419 417 2.505
420 418 2.241
421 419 3.355
422 420 2.775
423 421 2.395
424 422 2.864
425 423 2.290
426 424 2.469
427 425 2.977
428 426 2.759
429 427 2.460
430 428 2.456
431 429 2.497
432 430 2.362
433 431 2.874
434 432 2.812
435 433 2.559
436 434 2.283
437 435 2.670
438 436 2.482
439 437 2.495
440 438 3.118
441 439 3.018
442 440 2.252
443 441 3.222
444 442 2.483
445 443 2.436
446 444 3.209
447 445 3.169
448 446 2.455
449 447 2.432
450 448 2.464
451 449 2.500
452 450 2.967
453 451 2.958
454 452 2.390
455 453 2.494
456 454 2.523
457 455 2.473
458 456 2.516
459 457 3.527
460 458 2.262
461 459 2.377
462 460 2.840
463 461 2.135
464 462 2.520
465 463 3.285
466 464 2.940
467 465 2.256
468 466 2.496
469 467 2.705
470 468 2.395
471 469 3.031
472 470 3.104
473 471 2.411
474 472 2.630
475 473 2.468
476 474 2.416
477 475 2.559
478 476 2.732
479 477 2.441
480 478 2.624
481 479 2.536
482 480 2.458
483 481 2.516
484 482 2.507
485 483 2.606
486 484 2.616
487 485 2.881
488 486 2.358
489 487 2.469
490 488 2.513
491 489 3.250
492 490 2.606
493 491 2.545
494 492 2.407
495 493 2.437
496 494 2.663
497 495 3.346
498 496 2.349
499 497 2.481
500 498 2.613
501 499 2.483
502 500 2.417
503 501 2.923
504 502 3.059
505 503 2.483
506 504 2.373
507 505 2.525
508 506 2.530
509 507 2.431
510 508 2.917
511 509 2.559
512 510 2.623
513 511 3.172
514 512 2.304
515 513 2.331
516 514 3.287
517 515 2.544
518 516 2.429
519 517 2.690
520 518 2.464
521 519 2.406
522 520 2.804
523 521 2.899
524 522 2.461
525 523 2.536
526 524 2.414
527 525 2.380
528 526 2.616
529 527 3.236
530 528 2.499
531 529 2.558
532 530 2.584
533 531 2.309
534 532 2.447
535 533 3.039
536 534 3.133
537 535 2.170
538 536 2.583
539 537 2.505
540 538 2.666
541 539 2.567
542 540 3.307
543 541 2.177
544 542 2.495
545 543 2.441
546 544 2.638
547 545 2.521
548 546 3.527
549 547 2.460
550 548 2.362
551 549 2.520
552 550 2.439
553 551 2.572
554 552 3.087
555 553 2.707
556 554 2.475
557 555 2.283
558 556 2.604
559 557 2.529
560 558 3.094
561 559 3.051
562 560 2.085
563 561 2.365
564 562 2.711
565 563 2.351
566 564 2.572
567 565 2.979
568 566 2.805
569 567 2.373
570 568 2.744
571 569 2.340
572 570 2.285
573 571 2.791
574 572 3.447
575 573 2.208
576 574 2.518
577 575 2.448
578 576 3.917
579 577 1.540
580 578 2.923
581 579 2.632
582 580 2.404
583 581 2.582
584 582 2.508
585 583 2.413
586 584 3.602
587 585 2.342
588 586 2.482
589 587 2.584
590 588 3.062
591 589 2.070
592 590 2.726
593 591 2.937
594 592 2.385
595 593 2.734
596 594 2.188
597 595 2.582
598 596 2.487
599 597 3.339
600 598 2.535
601 599 2.414
602 600 2.459
603 601 2.510
604 602 2.547
605 603 3.414
606 604 2.314
607 605 2.399
608 606 2.550
609 607 2.541
610 608 2.355
611 609 2.709
612 610 3.067
613 611 2.502
614 612 2.467
615 613 2.899
616 614 2.177
617 615 2.719
618 616 3.049
619 617 2.673
620 618 2.259
621 619 2.566
622 620 2.383
623 621 2.420
624 622 3.318
625 623 2.807
626 624 2.348
627 625 2.595
628 626 2.433
629 627 2.569
630 628 2.620
631 629 2.652
632 630 3.140
633 631 2.945
634 632 2.571
635 633 2.546
636 634 2.428
637 635 3.436
638 636 2.393
639 637 2.491
640 638 2.511
641 639 2.485
642 640 2.603
643 641 3.363
644 642 2.442
645 643 2.344
646 644 2.462
647 645 2.508
648 646 2.617
649 647 2.909
650 648 2.824
651 649 2.528
652 650 2.424
653 651 2.511
654 652 2.517
655 653 2.583
656 654 3.331
657 655 2.436
658 656 2.455
659 657 2.380
660 658 2.570
661 659 2.582
662 660 2.789
663 661 2.917
664 662 2.526
665 663 2.538
666 664 2.447
667 665 2.423
668 666 2.695
669 667 3.143
670 668 2.616
671 669 2.126
672 670 2.596
673 671 2.464
674 672 3.092
675 673 2.681
676 674 2.641
677 675 2.311
678 676 2.551
679 677 2.551
680 678 2.837
681 679 2.532
682 680 3.006
683 681 2.289
684 682 2.521
685 683 2.556
686 684 2.593
687 685 2.552
688 686 3.173
689 687 2.415
690 688 2.429
691 689 2.733
692 690 2.286
693 691 2.476
694 692 3.256
695 693 2.897
696 694 2.528
697 695 2.377
698 696 2.518
699 697 2.504
700 698 3.074
701 699 2.814
702 700 2.433
703 701 2.338
704 702 2.476
705 703 2.458
706 704 3.141
707 705 2.551
708 706 2.717
709 707 2.300
710 708 2.527
711 709 2.446
712 710 2.469
713 711 2.866
714 712 2.776
715 713 2.507
716 714 2.457
717 715 2.520
718 716 2.450
719 717 2.568
720 718 3.192
721 719 2.471
722 720 2.790
723 721 2.337
724 722 2.548
725 723 2.435
726 724 3.313
727 725 2.274
728 726 2.461
729 727 2.451
730 728 2.556
731 729 2.546
732 730 2.869
733 731 3.308
734 732 2.293
735 733 2.428
736 734 2.686
737 735 2.562
738 736 2.523
739 737 3.309
740 738 2.311
741 739 2.498
742 740 2.485
743 741 2.568
744 742 2.392
745 743 3.260
746 744 2.534
747 745 2.550
748 746 2.355
749 747 2.768
750 748 2.319
751 749 3.039
752 750 2.721
753 751 2.490
754 752 2.378
755 753 2.539
756 754 2.823
757 755 2.480
758 756 3.533
759 757 2.285
760 758 2.365
+32 -13
View File
@@ -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. 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. 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. 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. - 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. - 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 ## Check-record template
| Version | Check | Result | Evidence | Date | | Version | Check | Result | Evidence | Date |
@@ -40,8 +42,9 @@ 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` | Placement vs references | PASS (with deliberate simplifications) | Section below, 2026-11-08 | 2026-11-08 |
| `0.1.0` | Collision and packaging | PASS — no blocking collision, no invisible walls, no seam gaps (see audit note) | `-qaselftest` on the packaged build, 2026-11-08 | 2026-11-08 | | `0.1.0` | Collision and packaging | PASS — no blocking collision, no invisible walls, no seam gaps (see audit note) | `-qaselftest` on the packaged build, 2026-11-08 | 2026-11-08 |
| `0.1.0` | 60 FPS at 1440p | PASS — 2.42 ms avg (414 fps), p99 3.01 ms at 2560×1440, TSR on, VSync off, no FG | `-qaselftest` perf capture, `Docs/perf/0.1.0-walk-frametimes.csv` | 2026-11-08 | | `0.1.0` | 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.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` | 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) ## `0.1.0` — Placement vs references — record (2026-11-08)
@@ -125,6 +128,22 @@ Run on the packaged build (`GigLineQA`, Development, Vulkan SM5) with `-qaselfte
Committed frame-time series: `Docs/perf/0.1.0-walk-frametimes.csv`. 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) ## `0.1.0` — Fresh checkout build — record (2026-11-08)
Followed the documented steps in [`Docs/build.md`](build.md) exactly, into a clean directory with no prior state: Followed the documented steps in [`Docs/build.md`](build.md) exactly, into a clean directory with no prior state:
@@ -157,6 +176,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. - `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". - `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.** **Verdict: PASS.**
-80
View File
@@ -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
![Block from the north sidewalk](../../Screenshots/demo/review-demo-spawn-view.png)
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
![Street level](../../Screenshots/demo/review-demo-street-level.png)
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
![Crossing](../../Screenshots/demo/review-demo-crossing.png)
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
![North buildings](../../Screenshots/demo/review-demo-north-buildings.png)
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.
@@ -1,6 +1,10 @@
#include "GigLineCharacter.h" #include "GigLineCharacter.h"
#include "Animation/AnimSequence.h"
#include "Animation/AnimSingleNodeInstance.h"
#include "Camera/CameraComponent.h" #include "Camera/CameraComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "Components/CapsuleComponent.h" #include "Components/CapsuleComponent.h"
#include "EnhancedInputComponent.h" #include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h" #include "EnhancedInputSubsystems.h"
@@ -28,7 +32,7 @@ static TAutoConsoleVariable<int32> CVarGigLineSetupLogs(
AGigLineCharacter::AGigLineCharacter() 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). // 1.75 m tall, 0.42 m wide, human scale in Unreal units (1 m = 100 uu).
GetCapsuleComponent()->InitCapsuleSize(42.0f, 87.5f); GetCapsuleComponent()->InitCapsuleSize(42.0f, 87.5f);
@@ -51,9 +55,59 @@ AGigLineCharacter::AGigLineCharacter()
FirstPersonCamera->SetRelativeLocation(FVector(0.0f, 0.0f, 72.5f)); // eye height 1.60 m FirstPersonCamera->SetRelativeLocation(FVector(0.0f, 0.0f, 72.5f)); // eye height 1.60 m
FirstPersonCamera->bUsePawnControlRotation = true; FirstPersonCamera->bUsePawnControlRotation = true;
// An eye-level mesh is not needed for 0.1.0; the body is a capsule with a camera. // Visible body (0.2.0). The animated import carries its own skeleton, so the
GetMesh()->SetHiddenInGame(true); // 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/CharacterV2.CharacterV2"));
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/CharacterV2Human_rig_GigLine_Idle.CharacterV2Human_rig_GigLine_Idle"));
if (IdleFinder.Succeeded())
{
IdleAnimation = IdleFinder.Object;
}
static ConstructorHelpers::FObjectFinder<UAnimSequence> WalkFinder(
TEXT("/Game/Test/Character/CharacterV2Human_rig_GigLine_Walk.CharacterV2Human_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()->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 // Hard-reference the Enhanced Input assets with ConstructorHelpers so the
// cooker always includes them and they load during CDO construction. Runtime // cooker always includes them and they load during CDO construction. Runtime
@@ -78,6 +132,8 @@ AGigLineCharacter::AGigLineCharacter()
TEXT("/Game/Input/IA_Jump.IA_Jump")); TEXT("/Game/Input/IA_Jump.IA_Jump"));
static ConstructorHelpers::FObjectFinder<UInputAction> ResetF( static ConstructorHelpers::FObjectFinder<UInputAction> ResetF(
TEXT("/Game/Input/IA_Reset.IA_Reset")); TEXT("/Game/Input/IA_Reset.IA_Reset"));
static ConstructorHelpers::FObjectFinder<UInputAction> CamF(
TEXT("/Game/Input/IA_ToggleCamera.IA_ToggleCamera"));
static ConstructorHelpers::FObjectFinder<UInputAction> PauseF( static ConstructorHelpers::FObjectFinder<UInputAction> PauseF(
TEXT("/Game/Input/IA_Pause.IA_Pause")); TEXT("/Game/Input/IA_Pause.IA_Pause"));
@@ -89,6 +145,7 @@ AGigLineCharacter::AGigLineCharacter()
LookAction = LookF.Object; LookAction = LookF.Object;
JumpAction = JumpF.Object; JumpAction = JumpF.Object;
ResetAction = ResetF.Object; ResetAction = ResetF.Object;
ToggleCameraAction = CamF.Object;
PauseAction = PauseF.Object; PauseAction = PauseF.Object;
if (!MappingContext || !MoveAction || !MoveBackAction || !StrafeAction || !StrafeLeftAction if (!MappingContext || !MoveAction || !MoveBackAction || !StrafeAction || !StrafeLeftAction
@@ -138,6 +195,23 @@ void AGigLineCharacter::BeginPlay()
StartLocation = GetActorLocation(); StartLocation = GetActorLocation();
StartRotation = GetActorRotation(); 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 PlayerStart sits above grade so the capsule cannot spawn embedded, so
// the BeginPlay position is mid-air. Re-capture once the pawn has landed, // 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. // otherwise Reset drops the player 1.4 m back into the air every press.
@@ -178,6 +252,29 @@ void AGigLineCharacter::BeginPlay()
{ {
StartQASelfTest(); 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"));
}
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() void AGigLineCharacter::StartQASelfTest()
@@ -557,6 +654,10 @@ void AGigLineCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputCo
{ {
Input->BindAction(PauseAction, ETriggerEvent::Started, this, &AGigLineCharacter::TogglePause); Input->BindAction(PauseAction, ETriggerEvent::Started, this, &AGigLineCharacter::TogglePause);
} }
if (ToggleCameraAction)
{
Input->BindAction(ToggleCameraAction, ETriggerEvent::Started, this, &AGigLineCharacter::ToggleCamera);
}
if (JumpAction) if (JumpAction)
{ {
Input->BindAction(JumpAction, ETriggerEvent::Started, this, &AGigLineCharacter::StartJump); Input->BindAction(JumpAction, ETriggerEvent::Started, this, &AGigLineCharacter::StartJump);
@@ -644,6 +745,87 @@ void AGigLineCharacter::Look(const FInputActionValue& Value)
AddControllerPitchInput(Axis.Y * LookSensitivity * (bInvertLookY ? -1.0f : 1.0f)); 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() void AGigLineCharacter::ResetToStart()
{ {
// Reset is a level-restart in miniature: put the body back where it spawned, // Reset is a level-restart in miniature: put the body back where it spawned,
@@ -7,9 +7,11 @@
#include "InputActionValue.h" #include "InputActionValue.h"
#include "GigLineCharacter.generated.h" #include "GigLineCharacter.generated.h"
class UAnimSequence;
class UCameraComponent; class UCameraComponent;
class UInputAction; class UInputAction;
class UInputMappingContext; class UInputMappingContext;
class USpringArmComponent;
/** /**
@@ -29,6 +31,7 @@ public:
virtual void BeginPlay() override; virtual void BeginPlay() override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override; virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
virtual void Tick(float DeltaSeconds) override;
protected: protected:
/** Loads the Enhanced Input assets from /Game/Input; called from BeginPlay. */ /** Loads the Enhanced Input assets from /Game/Input; called from BeginPlay. */
@@ -38,6 +41,31 @@ protected:
void ResetToStart(); void ResetToStart();
void TogglePause(); void TogglePause();
public:
/** 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. * TEMP QA: scripted walk / reset / pause verification.
* *
@@ -109,6 +137,45 @@ protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera") UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
TObjectPtr<UCameraComponent> FirstPersonCamera; 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();
/** 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. */ /** Degrees per second per unit of mouse input; defaults to Unreal's usual 45. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera")
float LookSensitivity = 45.0f; float LookSensitivity = 45.0f;
+1 -1
View File
@@ -34,7 +34,7 @@ public:
* in-game"). Keep in sync with Docs/controls.md. * in-game"). Keep in sync with Docs/controls.md.
*/ */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Controls") 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. */ /** Licence line, including where the licence can be read. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attribution") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attribution")
@@ -1,5 +1,8 @@
#include "GigLineShotActor.h" #include "GigLineShotActor.h"
#include "GigLineCharacter.h"
#include "Kismet/GameplayStatics.h"
#include "Camera/CameraActor.h" #include "Camera/CameraActor.h"
#include "Camera/CameraComponent.h" #include "Camera/CameraComponent.h"
#include "Engine/World.h" #include "Engine/World.h"
@@ -32,6 +35,12 @@ void AGigLineShotActor::Tick(float DeltaSeconds)
if (bWaitingForSettle) 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; SettleCountdown -= DeltaSeconds;
if (SettleCountdown <= 0.f) if (SettleCountdown <= 0.f)
{ {
@@ -53,11 +62,47 @@ void AGigLineShotActor::Tick(float DeltaSeconds)
{ {
bWaitingForScreenshot = false; bWaitingForScreenshot = false;
UE_LOG(LogTemp, Log, TEXT("GigLineShotActor: captured %s"), *FScreenshotRequest::GetFilename()); 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; ++CurrentShot;
NextShot(); 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);
}
// 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() void AGigLineShotActor::NextShot()
{ {
if (!PlayerController) if (!PlayerController)
@@ -93,6 +138,20 @@ void AGigLineShotActor::NextShot()
Cam->SetFieldOfView(ShotFieldOfView[CurrentShot]); 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];
if (AGigLineCharacter* Character = Cast<AGigLineCharacter>(UGameplayStatics::GetPlayerPawn(this, 0)))
{
Character->SetReviewWalk(bFollowThisShot);
}
if (bFollowThisShot)
{
UpdateFollowCamera();
}
PlayerController->SetViewTarget(CurrentCamera); PlayerController->SetViewTarget(CurrentCamera);
SettleCountdown = SettleSeconds; SettleCountdown = SettleSeconds;
@@ -50,6 +50,18 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite) UPROPERTY(EditAnywhere, BlueprintReadWrite)
float SettleSeconds = 0.35f; 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). */ /** Quit the game after the last shot (disable to keep the game open). */
UPROPERTY(EditAnywhere, BlueprintReadWrite) UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool bQuitWhenDone = true; bool bQuitWhenDone = true;
@@ -57,6 +69,9 @@ public:
private: private:
void NextShot(); void NextShot();
/** Re-places the active camera at the follow offset behind the player pawn. */
void UpdateFollowCamera();
UPROPERTY() UPROPERTY()
TObjectPtr<class APlayerController> PlayerController; TObjectPtr<class APlayerController> PlayerController;
+67 -1
View File
@@ -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 ## 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). - 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. - 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.
+3 -2
View File
@@ -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.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.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.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 | | `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 |
| `1.0.0` | 35–40 | Stable neighborhood-scale walking/driving release | `1.0.x` maintenance and compatibility 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. Finish each milestone's release checks before expanding scope. Cut map size and simulation complexity before compromising the core walking/driving experience.
+27 -7
View File
@@ -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). 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 ## 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 | | 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. **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. **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. **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`. **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. **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.
+152 -14
View File
@@ -12,18 +12,138 @@ 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. 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 | | # | Shot | 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 | | 1 | [Spawn view](#1-spawn-view) | ☑ 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 | | 2 | [Street level](#2-street-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 | | 3 | [Crossing](#3-crossing) | ☑ 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 | | 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
![Spawn view: the whole south block from the north sidewalk](demo/review-demo-spawn-view.png)
- **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
![Street level: the hero colonnade at eye height](demo/review-demo-street-level.png)
- **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
![Crossing: the designated mid-block crossing with zebra bars](demo/review-demo-crossing.png)
- **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
![North buildings: TCL Chinese Theatre and Madame Tussaud's from the south](demo/review-demo-north-buildings.png)
- **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
![Retail frontage: 6800 Lee Drug and 6806–6820 from across the street](demo/review-demo-retail-frontage.png)
- **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
![Third-person body: the player pawn standing on the north sidewalk, seen from behind](demo/review-demo-third-person.png)
- **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
![Third-person front: the player body from the road, arms at the sides](demo/review-demo-third-person-front.png)
- **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.
---
## 8. Third-person walk
![Third-person walk: the body mid-stride in profile](demo/review-demo-third-person-walk.png)
- **File:** `demo/review-demo-third-person-walk.png`
- **View:** tracking camera, 4.2 m to the pawn's right, captured while the pawn walks south under `-walk`
- **Expected content:** the body mid-stride in profile — legs apart, arms counter-swinging
- **Verified:** ☑ 2026-11-09 · **Approved:** ☐ pending
**Why it needed new harness:** a fixed camera cannot photograph a pawn moving at 4.5 m/s. Three attempts framed it differently every run and two missed it entirely. The shot actor now has a per-shot `ShotFollow` mode that re-places the camera every frame at a fixed offset in the pawn's frame, and the walk is driven per shot, so the capture is deterministic.
**What it proves:** the walk clip plays and advances. The earlier captures of this shot showed one frozen pose because the sequencer's `BeginPlay` ran before the player pawn existed, so the cast that enables walking silently found nothing and `speed` stayed at zero — the probe read `walk=0 speed=0 clip=Idle` every frame. Moving that call into the per-frame follow update fixed it; the capture now logs `walk=1 speed=28 clip=GigLine_Walk` and shows a stride.
---
## The iteration loop (uncommitted) ## The iteration loop (uncommitted)
@@ -40,12 +160,30 @@ Note the comparison is only meaningful when the shot and the reference frame the
## Scene conventions ## 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. - **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. - **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 ## 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). 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.
- 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. | Issue | Where it is tracked | State |
- The El Capitan Theatre has no marquee or blade sign yet — the feature that makes it read as itself. | --- | --- | --- |
| 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`) |
| Third-person fingers splay rather than relax — the bind's finger spread is carried into the pose | `0.2.2` → skin and animation tuning | 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.
Binary file not shown.
+52 -47
View File
@@ -9,44 +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. - When all steps in a version section are done: write a `CHANGELOG.md` entry, then remove the version section from this file.
- Release gate: a version is tagged only on the owner's explicit go-ahead once its release checks pass, not automatically when the checkboxes close. - Release gate: a version is tagged only on the owner's explicit go-ahead once its release checks pass, not automatically when the checkboxes close.
## 0.1.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) ## 0.2.1 — Rig fixes (patch)
### Patch tasks ### Patch tasks
@@ -186,6 +148,7 @@ The `0.1.4` kit, materials and theatre trim are signed off; these are the geomet
- [ ] Create an evening lighting preset. - [ ] Create an evening lighting preset.
- [ ] Complete a visual polish pass across the neighborhood. - [ ] 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). - [ ] Add ambient audio (city ambience, traffic, basic SFX).
- [ ] Verify licenses for all added audio files. - [ ] Verify licenses for all added audio files.
@@ -289,7 +252,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. - [ ] Fix compatibility issues from the hardware testing matrix.
- [ ] Tag `v0.8.3` and write `CHANGELOG.md` entry; then remove this section. - [ ] 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 ### Freeze and audit
@@ -301,29 +306,29 @@ The `0.1.4` kit, materials and theatre trim are signed off; these are the geomet
### Release checks ### Release checks
- [ ] Beta feedback triaged; blockers logged for `0.9.x`. - [ ] Beta feedback triaged; blockers logged for `0.10.x`.
- [ ] Tag `v0.9.0` and write `CHANGELOG.md` entry; then remove this section. - [ ] 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 ### Patch tasks
- [ ] Fix blockers reported during the public beta. - [ ] 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 ### Patch tasks
- [ ] Fix regressions introduced by `0.9.x` fixes. - [ ] 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 ### Patch tasks
- [ ] Update all `Docs/` for accuracy before `1.0.0`. - [ ] 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) ## 1.0.0 — Stable neighborhood-scale walking/driving release (weeks 35–40)
+7
View File
@@ -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 | | `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). | | `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`. | | `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. | | `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_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. | | `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. | | `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. | | `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. | | `fbx_export_preset.py` | The **single** FBX export preset (`ue_metric_v1` / `ue_character_v1`): metres→centimetres via `FBX_SCALE_NONE`, axis mapping east +X / north −Y / up +Z, `mesh_smooth_type="FACE"` (smoothing groups survive the import). Every Blender exporter imports it instead of copying the argument block; `preset_kwargs()` is bpy-free and unit-tested. |
| `sibling_asset_audit.py` | Reads the sibling project's prop library from GLB headers into `Art/props/reuse-manifest.json`; copies nothing | | `sibling_asset_audit.py` | Reads the sibling project's prop library from GLB headers into `Art/props/reuse-manifest.json`; copies nothing |
@@ -40,6 +45,7 @@ Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side b
| Tool | Purpose | | Tool | Purpose |
| --- | --- | | --- | --- |
| `hero_facade_build.py` | Parametric hero façade; emits a JSON report naming the evidence behind every part | | `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 | | `mpfb_character_export.py` | MPFB human, standard rig, baked skin, FBX export |
## Unreal import and level building ## 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_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_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_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 | | `ue_character_import.py` | Skeletal import plus the material/texture chain Interchange skips |
## Visual review ## Visual review
+27
View File
@@ -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)]
+283
View File
@@ -0,0 +1,283 @@
#!/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.
# 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")
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)
action.use_fake_user = True
log(f"Idle: {frames} frames, spine/neck/head/arms, feet planted")
return action
def clip_walk(arm, frames):
"""One full stride: left contact at frame 1, right contact at the half-cycle."""
bones = pose_bones(arm)
action = bpy.data.actions.new("GigLine_Walk")
arm.animation_data.action = action
for frame in range(1, frames + 1):
t = 2.0 * math.pi * (frame - 1) / frames
swing = math.sin(t)
# Hip swing, opposite phase per leg; negative X is forward.
key_euler(bones["upperleg01.L"], frame, x=-HIP * swing)
key_euler(bones["upperleg01.R"], frame, x=HIP * swing)
# The knee only flexes while the leg is swinging forward, so it is driven
# by the half-rectified swing rather than the raw sine.
knee_l = KNEE * max(0.0, math.sin(t + math.pi))
knee_r = KNEE * max(0.0, math.sin(t))
key_euler(bones["lowerleg01.L"], frame, x=knee_l)
key_euler(bones["lowerleg01.R"], frame, x=knee_r)
# Ankles keep the foot roughly level: counter the hip plus half the knee.
key_euler(bones["foot.L"], frame, x=0.25 * HIP * swing - 0.35 * knee_l)
key_euler(bones["foot.R"], frame, x=-0.25 * HIP * swing - 0.35 * knee_r)
# Arms counter-phase the legs, elbows carry a constant relaxed bend.
key_euler(bones["upperarm01.L"], frame, x=ARM * swing)
key_euler(bones["upperarm01.R"], frame, x=-ARM * swing)
key_euler(bones["lowerarm01.L"], frame, x=ELBOW + 0.10 * swing)
key_euler(bones["lowerarm01.R"], frame, x=ELBOW - 0.10 * swing)
# Torso counter-rotation: the shoulders turn against the hips.
key_euler(bones["spine04"], frame, z=0.08 * swing)
key_euler(bones["neck01"], frame, z=-0.04 * swing)
action.use_fake_user = True
log(f"Walk: {frames} frames, stride {HIP:.2f} rad hip / {KNEE:.2f} knee / {ARM:.2f} arm")
return action
def main(argv=None) -> int:
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--out", default="/tmp/gigline/character-anim.fbx")
ap.add_argument("--report", default=None)
args = ap.parse_args(argv)
arm = next((o for o in bpy.data.objects if o.type == "ARMATURE"), None)
if arm is None:
raise SystemExit("no armature in the .blend")
mesh = next((o for o in bpy.data.objects if o.type == "MESH"), None)
log(f"rig: {arm.name}, {len(arm.pose.bones)} bones; mesh {mesh.name if mesh else 'none'} "
f"{len(mesh.data.vertices) if mesh else 0} verts")
bpy.context.scene.render.fps = FPS
clear_animation(arm)
idle = clip_idle(arm, CLIPS["Idle"])
walk = clip_walk(arm, CLIPS["Walk"])
bpy.ops.object.mode_set(mode="OBJECT")
# Export armature + mesh together: the preset's character flags carry the
# bake_anim settings that make Unreal create AnimSequences.
bpy.ops.object.select_all(action="DESELECT")
arm.select_set(True)
if mesh:
mesh.select_set(True)
bpy.context.view_layer.objects.active = arm
os.makedirs(os.path.dirname(args.out), exist_ok=True)
fbx_preset.export_character(args.out, selection=True)
log(f"exported {args.out}")
if args.report:
with open(args.report, "w", encoding="utf-8") as handle:
json.dump({
"fps": FPS,
"rig": arm.name,
"bones": len(arm.pose.bones),
"clips": [
{"action": idle.name, "frames": CLIPS["Idle"]},
{"action": walk.name, "frames": CLIPS["Walk"]},
],
"axis_evidence": "probe: upperleg01.L X 0.5 rad moves foot.L "
"Y -0.022 -> +0.362; Y twists, Z abducts",
"amplitudes_rad": {"hip": HIP, "knee": KNEE, "arm": ARM, "elbow": ELBOW},
"export": os.path.basename(args.out),
"log": PARTS,
}, handle, indent=2)
print(f"[anim] report -> {args.report}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+13
View File
@@ -49,12 +49,25 @@ _METRIC = {
} }
# Character exports differ only in the object set and the armature/path flags. # 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( _CHARACTER = dict(
_METRIC, _METRIC,
object_types={"ARMATURE", "MESH"}, object_types={"ARMATURE", "MESH"},
add_leaf_bones=False, add_leaf_bones=False,
path_mode="COPY", path_mode="COPY",
embed_textures=False, 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} PRESETS = {"ue_metric_v1": _METRIC, "ue_character_v1": _CHARACTER}
+33 -4
View File
@@ -174,6 +174,20 @@ def build():
cap_z0 = HEIGHT - ENTABLATURE_H - PARAPET_H - CAPITAL_H cap_z0 = HEIGHT - ENTABLATURE_H - PARAPET_H - CAPITAL_H
cylinder(f"Hero_Column_{i + 1}", x, -0.55, shaft_z0, cap_z0, cylinder(f"Hero_Column_{i + 1}", x, -0.55, shaft_z0, cap_z0,
COLUMN_D / 2.0, stone) 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. # 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 # 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). # still ends at HEIGHT - ENTABLATURE_H - PARAPET_H (proportion unchanged).
@@ -185,6 +199,9 @@ def build():
1.30, 1.30, abacus_h, stone) 1.30, 1.30, abacus_h, stone)
note("Hero_Column_x6", (COLUMN_D, COLUMN_D, HEIGHT - ENTABLATURE_H - PARAPET_H - PEDESTAL_H), note("Hero_Column_x6", (COLUMN_D, COLUMN_D, HEIGHT - ENTABLATURE_H - PARAPET_H - PEDESTAL_H),
f"6 columns at {COLUMN_BAY} m centres (sibling notes x measured frontage)") f"6 columns at {COLUMN_BAY} m centres (sibling notes x measured frontage)")
note("Hero_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), note("Hero_ColumnBase_x6", (1.36, 1.36, 0.22),
"assumption: Ionic base moulding; photographs show a wider base at the shaft foot") "assumption: Ionic base moulding; photographs show a wider base at the shaft foot")
note("Hero_Abacus_x6", (1.30, 1.30, 0.14), note("Hero_Abacus_x6", (1.30, 1.30, 0.14),
@@ -223,10 +240,22 @@ def build():
FRONTAGE_W, 1.2, PARAPET_H, stone) FRONTAGE_W, 1.2, PARAPET_H, stone)
note("Hero_Parapet", (FRONTAGE_W, 1.2, PARAPET_H), "assumption: parapet carries the motto band") 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. # Motto band: a recessed panel across the parapet with a moulded surround, as
cube("Hero_MottoBand", 0.0, -1.32, HEIGHT - PARAPET_H / 2.0, # the photographs show. The panel was a 8 cm stripe of dark stone; it is now a
FRONTAGE_W - 3.0, 0.08, 0.85, stone_dark) # real recess — surround frame standing 6 cm proud, panel set 12 cm behind it —
note("Hero_MottoBand", (FRONTAGE_W - 3.0, 0.08, 0.85), "photographs: motto panel above the colonnade") # 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 # Medallions either side of the inscription: discs on the parapet face, not
# pancakes on the coping (see disc_on_wall). # pancakes on the coping (see disc_on_wall).
+39 -9
View File
@@ -20,18 +20,40 @@ PROJ="$REPO/Game/GigLine/GigLine.uproject"
VERIFY="no" VERIFY="no"
[ "${1:-}" = "--verify" ] && VERIFY="yes" [ "${1:-}" = "--verify" ] && VERIFY="yes"
echo "== 1/3 stage the demo shot cameras ==" run_cmdlet() {
cd "$ENGINE" local script="$1" label="$2"
./Engine/Binaries/Linux/UnrealEditor-Cmd "$PROJ" \ echo "== $label =="
-run=pythonscript -script="$REPO/Tools/ue_build_review_map.py" \ ( cd "$ENGINE" && ./Engine/Binaries/Linux/UnrealEditor-Cmd "$PROJ" \
-nullrhi -unattended -nopause -stdout -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 tail -3 /tmp/ue-review-map-result.txt 2>/dev/null || true
echo "== 2/3 capture the demo map ==" echo "== 6/6 capture the demo map =="
./Engine/Binaries/Linux/UnrealEditor-Cmd "$PROJ" /Game/Hollywood/DemoMap \ # Clear the engine's screenshot folder first. The collect step below copies every
-game -windowed -ResX=1920 -ResY=1080 -nopause -nosplash -stdout # 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" mkdir -p "$REPO/Screenshots/demo"
rm -f "$REPO"/Screenshots/demo/review-*.png rm -f "$REPO"/Screenshots/demo/review-*.png
found=0 found=0
@@ -47,6 +69,14 @@ done
[ "$found" = "1" ] || { echo "ERROR: no demo screenshots found"; exit 1; } [ "$found" = "1" ] || { echo "ERROR: no demo screenshots found"; exit 1; }
ls -1 "$REPO/Screenshots/demo" 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 if [ "$VERIFY" = "yes" ]; then
echo "== verify: does each shot show what it should? ==" echo "== verify: does each shot show what it should? =="
cd "$REPO" cd "$REPO"
+377
View File
@@ -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())
+18
View File
@@ -52,6 +52,24 @@ class CharacterPresetTest(unittest.TestCase):
self.assertEqual(kwargs["mesh_smooth_type"], "FACE") 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): class OverrideTest(unittest.TestCase):
def test_overrides_win(self): def test_overrides_win(self):
kwargs = preset.preset_kwargs(mesh_smooth_type="OFF") kwargs = preset.preset_kwargs(mesh_smooth_type="OFF")
+32
View File
@@ -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()
+86
View File
@@ -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()
+33 -7
View File
@@ -123,12 +123,28 @@ def apply_to_actor(actor, kit, materials=None, prefix=None):
f"({width / 100:.1f} x {height / 100:.1f} m)") f"({width / 100:.1f} x {height / 100:.1f} m)")
return 0 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))) 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 bay_w = width / bays # actual bay width for this building
# Street face: the frontage runs along X, and the street is at smaller Y. # Street face: the frontage runs along X, and the street is at smaller Y.
face_y = mn.y face_y = mn.y
base_z = mn.z 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): def kit_material(name):
if materials is None: if materials is None:
@@ -140,11 +156,13 @@ def apply_to_actor(actor, kit, materials=None, prefix=None):
for bay in range(bays): for bay in range(bays):
cx = mn.x + bay_w * (bay + 0.5) cx = mn.x + bay_w * (bay + 0.5)
# ground floor: shopfront # ground floor: shopfront
for piece, dx in (("Kit_ShopPier_L", -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)): ("Kit_ShopPier_R", bay_w / 2 - 30.0, "R")):
if piece in kit: 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), 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 placed += 1
if "Kit_ShopGlass" in kit: if "Kit_ShopGlass" in kit:
if _spawn(kit["Kit_ShopGlass"], unreal.Vector(cx, face_y, base_z), 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 # upper storeys: window bays
for storey in range(storeys): for storey in range(storeys):
if "Kit_WindowBay" in kit: 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), if _spawn(kit["Kit_WindowBay"], unreal.Vector(cx, face_y, z),
f"{prefix}window_{bay}_{storey}", kit_material("Kit_WindowBay")): f"{prefix}window_{bay}_{storey}", kit_material("Kit_WindowBay")):
placed += 1 placed += 1
@@ -217,9 +235,17 @@ def main() -> None:
removed += 1 removed += 1
log(f"cleared {removed} previous kit actors") 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() targets = [a for a in unreal.EditorLevelLibrary.get_all_level_actors()
if a.get_actor_label().startswith(("Frontage_", "North_"))] if a.get_actor_label().startswith(("Frontage_", "North_"))
log(f"frontage actors: {len(targets)}") 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 total = 0
for actor in targets: for actor in targets:
total += apply_to_actor(actor, kit, materials) total += apply_to_actor(actor, kit, materials)
+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""Audit the DemoMap for staged actors that would render wrong.
Checks that are cheap and catch the failures this project actually had:
1. **No unmaterialed actors.** 604 actors once rendered with the default material
because the demo builder deleted and recreated the site materials, orphaning
every reference; the map looked merely "a bit flat".
2. **Placement spans.** Each verified frontage building is probed against
`Tools/ue_placement.py`; a building 13 m off its footprint still renders fine,
which is exactly why it needs a number, not an eye.
3. **No duplicate actor labels.** Two buildings' kit pieces sharing a label made
the outliner ambiguous and hid one of them.
UnrealEditor-Cmd <uproject> -run=pythonscript \
-script=Tools/ue_audit_scene.py -nullrhi -unattended -nopause -stdout
Result file: `/tmp/gigline/scene-audit.txt`. Exit code from the commandlet is
always 0, so **read the file** — `visual_loop.py --verify` does not depend on it.
"""
from __future__ import annotations
import collections
import os
import sys
import unreal
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import ue_placement # noqa: E402
MAP = "/Game/Hollywood/DemoMap"
RESULT = "/tmp/gigline/scene-audit.txt"
lines: list[str] = []
failures = 0
def log(message: str) -> None:
lines.append(message)
print(message)
def fail(message: str) -> None:
global failures
failures += 1
log("FAIL " + message)
def main() -> None:
if not unreal.EditorLevelLibrary.load_level(MAP):
log(f"could not load {MAP}")
return
actors = unreal.EditorLevelLibrary.get_all_level_actors()
# 1. materials
unmaterialed = []
for actor in actors:
comp = actor.get_component_by_class(unreal.StaticMeshComponent)
if not comp or not comp.static_mesh:
continue
if comp.get_material(0) is None:
unmaterialed.append(actor.get_actor_label())
log(f"mesh actors: {sum(1 for a in actors if a.get_component_by_class(unreal.StaticMeshComponent))}")
if unmaterialed:
fail(f"{len(unmaterialed)} actor(s) with no material, e.g. {unmaterialed[:6]}")
else:
log("OK every staged mesh has a material")
# 2. placement spans
for actor in actors:
label = actor.get_actor_label()
key = None
if label.startswith("Frontage_SM_"):
name = label[len("Frontage_SM_"):]
key = {"Hollywood_6922": "6922", "Hollywood_6902": "6902",
"ElCapitanTheatre": "6838", "Budget_6822": "6822",
"Hollywood_6806": "6806", "LeeDrug_6800": "6800"}.get(name)
elif label == ue_placement.HERO_ACTOR:
key = "hero"
if not key:
continue
origin, extent = actor.get_actor_bounds(False)
lo, hi = origin.x - extent.x, origin.x + extent.x
problem = ue_placement.describe_span_failure(key, lo, hi)
if problem:
fail(problem)
else:
log(f"OK {key}: X {lo:.0f}..{hi:.0f}")
# 3. duplicate labels
counts = collections.Counter(a.get_actor_label() for a in actors)
duplicates = {k: v for k, v in counts.items() if v > 1}
if duplicates:
sample = list(duplicates.items())[:6]
fail(f"{len(duplicates)} duplicated actor label(s), e.g. {sample}")
else:
log("OK no duplicate actor labels")
log(f"== {failures} failure(s) ==")
try:
main()
except Exception as exc: # noqa: BLE001 - surface in the result file
import traceback
log(f"FAILED: {type(exc).__name__}: {exc}")
log(traceback.format_exc())
os.makedirs(os.path.dirname(RESULT), exist_ok=True)
with open(RESULT, "w", encoding="utf-8") as handle:
handle.write("\n".join(lines) + "\n")
+51 -8
View File
@@ -23,6 +23,8 @@ import unreal
MAP = "/Game/Hollywood/DemoMap" MAP = "/Game/Hollywood/DemoMap"
# Repository root, so the texture generator can be invoked by path. # Repository root, so the texture generator can be invoked by path.
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import ue_placement # noqa: E402
RESULT = "/tmp/gigline/demo-map.txt" RESULT = "/tmp/gigline/demo-map.txt"
lines = [] lines = []
@@ -116,14 +118,23 @@ def ensure_material(mat_name, spec, textures=None):
does not read as one flat specular value. does not read as one flat specular value.
""" """
path = f"/Game/Hollywood/Site/{mat_name}" path = f"/Game/Hollywood/Site/{mat_name}"
if unreal.EditorAssetLibrary.does_asset_exist(path):
# Rebuilt every run so a look change cannot be masked by a stale asset.
unreal.EditorAssetLibrary.delete_asset(path)
mat = unreal.AssetToolsHelpers.get_asset_tools().create_asset(
mat_name, "/Game/Hollywood/Site", unreal.Material, unreal.MaterialFactoryNew())
lib = unreal.MaterialEditingLibrary lib = unreal.MaterialEditingLibrary
# Update the existing material IN PLACE rather than deleting and recreating it.
# Deleting the asset to force a look rebuild silently orphaned every reference:
# after one demo-builder run, all 598 kit actors and the theatre trim pointed at
# deleted materials and rendered with the default one (604 actors with no
# material), while the map itself looked merely "a bit flat".
mat = unreal.EditorAssetLibrary.load_asset(path)
if mat is None:
mat = unreal.AssetToolsHelpers.get_asset_tools().create_asset(
mat_name, "/Game/Hollywood/Site", unreal.Material, unreal.MaterialFactoryNew())
else:
# Clear the old graph so a removed node cannot survive a look change.
# `Material.expressions` is protected from Python, so use the editing
# library's own deleter rather than reading the array.
lib.delete_all_material_expressions(mat)
def node(cls): def node(cls):
return lib.create_material_expression(mat, cls) return lib.create_material_expression(mat, cls)
@@ -294,7 +305,8 @@ def build():
log(f"could not create {MAP}") log(f"could not create {MAP}")
return return
removed = clear(("Demo_", "PlayerStart", "Hero_Facade", "Frontage_", "ShotSequencer")) removed = clear(("Demo_", "PlayerStart", "Hero_Facade", "Frontage_", "RetailTrim_",
"ShotSequencer"))
log(f"cleared {removed} previous demo actors") log(f"cleared {removed} previous demo actors")
# --- lighting: fixed daytime ------------------------------------------ # --- lighting: fixed daytime ------------------------------------------
@@ -401,7 +413,7 @@ def build():
# the NORTH side of the street, which only looked right while it stood alone. # the NORTH side of the street, which only looked right while it stood alone.
hero = unreal.EditorAssetLibrary.load_asset("/Game/Hollywood/ElCapitan/SM_HeroFacade") hero = unreal.EditorAssetLibrary.load_asset("/Game/Hollywood/ElCapitan/SM_HeroFacade")
if hero: if hero:
actor = spawn(unreal.StaticMeshActor, unreal.Vector(-13336.0, 1307.0, 0.0), "Hero_Facade") actor = spawn(unreal.StaticMeshActor, unreal.Vector(*ue_placement.HERO_LOCATION_CM), ue_placement.HERO_ACTOR)
if actor: if actor:
actor.static_mesh_component.set_static_mesh(hero) actor.static_mesh_component.set_static_mesh(hero)
actor.static_mesh_component.set_mobility(unreal.ComponentMobility.STATIC) actor.static_mesh_component.set_mobility(unreal.ComponentMobility.STATIC)
@@ -586,6 +598,37 @@ def build():
staged_frontage += 1 staged_frontage += 1
log(f"staged {staged_frontage} frontage buildings") log(f"staged {staged_frontage} frontage buildings")
# --- retail frontage trim (0.1.6) -------------------------------------
# The two retail buildings beside the hero are modelled from their own
# photograph (Art/reference/retail-neighbours/); the trim is laid over the
# blockout volume, so it keeps the trim's own sampled materials rather than a
# look-dev override. 6822 Budget has no photograph and stays a bare volume.
for asset_name, label in (("SM_Retail_LeeDrug", "6800 Lee Drug trim"),
("SM_Retail_6806", "6806-6820 trim")):
mesh = unreal.EditorAssetLibrary.load_asset(f"/Game/Hollywood/Frontage/{asset_name}")
if mesh is None:
log(f"MISSING trim {asset_name} — run Tools/ue_import_retail_detail.py")
continue
# RetailTrim_ rather than Frontage_ so the kit assembler leaves it alone.
actor = spawn(unreal.StaticMeshActor, unreal.Vector(0, 0, 0), f"RetailTrim_{asset_name}")
if actor:
comp = actor.static_mesh_component
comp.set_static_mesh(mesh)
comp.set_mobility(unreal.ComponentMobility.MOVABLE)
# Map the trim's own slots onto the look-dev materials where the surface
# type matches, so the walls sit in the same noise/texture family as the
# rest of the block. The trim's colour slots (bronze, burgundy, mural,
# lettering) stay as authored: those tones came from the photograph.
TRIM_LOOK = {"M_Retail_Stucco": "M_Stucco", "M_Retail_White": "M_Stucco",
"M_Retail_Glass": "M_FacadeGlass"}
mapped = 0
for index, slot in enumerate(mesh.get_editor_property("static_materials")):
target = TRIM_LOOK.get(str(slot.get_editor_property("material_slot_name")))
if target and target in built:
comp.set_material(index, built[target])
mapped += 1
log(f"staged: {label} ({mapped} slot(s) onto look materials)")
# --- player start: north sidewalk, facing the hero frontage ------------- # --- player start: north sidewalk, facing the hero frontage -------------
# Signs were measured from the imported meshes rather than assumed: the FBX # Signs were measured from the imported meshes rather than assumed: the FBX
# preset flips north, so in Unreal the road spans Y -1382..+1047, the NORTH # preset flips north, so in Unreal the road spans Y -1382..+1047, the NORTH
+21
View File
@@ -288,6 +288,7 @@ try:
# the colonnade's underside. # the colonnade's underside.
# Level horizon for the same reason: a block elevation is read by its # Level horizon for the same reason: a block elevation is read by its
# verticals, and a pitched camera makes them lean. # verticals, and a pitched camera makes them lean.
("demo-third-person-walk", unreal.Vector(-12940, -980, 560), unreal.Vector(-13336, -980, 500), 55.0),
("demo-spawn-view", unreal.Vector(-13336, -3100, 620), unreal.Vector(-13336, 1300, 620), 48.0), ("demo-spawn-view", unreal.Vector(-13336, -3100, 620), unreal.Vector(-13336, 1300, 620), 48.0),
# Street level, from the north sidewalk: the old camera sat at Y -400, # Street level, from the north sidewalk: the old camera sat at Y -400,
# inside the facade body (which spans Y -1457..-200), so it looked at # inside the facade body (which spans Y -1457..-200), so it looked at
@@ -300,6 +301,12 @@ try:
# on the north sidewalk looking along the crosswalk itself, which is # on the north sidewalk looking along the crosswalk itself, which is
# the pedestrian's view and shows the zebra bars as bars. # the pedestrian's view and shows the zebra bars as bars.
("demo-crossing", unreal.Vector(-15250, -2500, 400), unreal.Vector(-14956, 400, 150)), ("demo-crossing", unreal.Vector(-15250, -2500, 400), unreal.Vector(-14956, 400, 150)),
# 0.1.6 retail frontage: 6800 Lee Drug (X -3340..-1417, face Y +1467)
# and 6806-6820 (X -6618..-3297, face Y +1252). Stand on the north
# sidewalk at 2 m, look south with a level horizon so the parapets and
# the cornice line read as horizontals. 68 deg covers both frontages at
# this distance; the ends crop rather than the elevation leaning.
("demo-retail-frontage", unreal.Vector(-4700, -2600, 210), unreal.Vector(-4700, 1400, 620), 66.0),
# the two north-side background buildings (TCL, Tussaud's) seen from # the two north-side background buildings (TCL, Tussaud's) seen from
# the south across Hollywood Blvd. Probed world-space (2026-11-08): # the south across Hollywood Blvd. Probed world-space (2026-11-08):
# TCL bbox X -22889..-18442, Y -9431..-1802; Tussaud's X -27071.. # TCL bbox X -22889..-18442, Y -9431..-1802; Tussaud's X -27071..
@@ -308,6 +315,16 @@ try:
# (X -22750) and look NORTH toward the mass. The old camera was at # (X -22750) and look NORTH toward the mass. The old camera was at
# X -22800 Y +2600 aiming at its own X, giving an 85%-sky frame. # X -22800 Y +2600 aiming at its own X, giving an 85%-sky frame.
("demo-north-buildings", unreal.Vector(-22750, 1600, 500), unreal.Vector(-22750, -5200, 350)), ("demo-north-buildings", unreal.Vector(-22750, 1600, 500), unreal.Vector(-22750, -5200, 350)),
# 0.2.0 third-person body. Stands where the pawn's own boom camera sits —
# 3.4 m behind the spawn point, 1 m above it — and looks at the pawn, so
# the visible body and its idle clip are what the shot is for.
# Demo_PlayerStart is at (-13336, -1150, 600), yaw 90 (facing +Y).
("demo-third-person", unreal.Vector(-13336, -1750, 780), unreal.Vector(-13336, -1150, 560), 58.0),
# Front view of the same pawn: the third-person shot only ever shows its
# back, so a pose defect there is invisible. The pawn spawns facing +Y
# (yaw 90), so the camera stands on the road side and looks back north.
("demo-third-person-front", unreal.Vector(-13336, -520, 700), unreal.Vector(-13336, -1150, 540), 55.0),
] ]
seq = spawn(unreal.GigLineShotActor, unreal.Vector(0, 0, 300), "ShotSequencer") seq = spawn(unreal.GigLineShotActor, unreal.Vector(0, 0, 300), "ShotSequencer")
if seq: if seq:
@@ -315,6 +332,10 @@ try:
seq.set_editor_property("shot_rotations", seq.set_editor_property("shot_rotations",
[unreal.MathLibrary.find_look_at_rotation(c[1], c[2]) for c in demo_shots]) [unreal.MathLibrary.find_look_at_rotation(c[1], c[2]) for c in demo_shots])
seq.set_editor_property("shot_names", [c[0] for c in demo_shots]) seq.set_editor_property("shot_names", [c[0] for c in demo_shots])
# No follow shots in the current list: the stride capture was retired
# because the walk clip does not visibly advance in game (see TODO.md).
# The follow flag stays wired for when it does.
seq.set_editor_property("shot_follow", [True] + [False] * (len(demo_shots) - 1))
seq.set_editor_property( seq.set_editor_property(
"shot_field_of_view", "shot_field_of_view",
[float(c[3]) if len(c) > 3 else 0.0 for c in demo_shots]) [float(c[3]) if len(c) > 3 else 0.0 for c in demo_shots])
+96
View File
@@ -0,0 +1,96 @@
# Import the character animation clips into Unreal (0.2.0).
#
# blender -b Art/blender/character/character-2026-11-07.blend \
# --python Tools/character_anim_build.py -- --out /tmp/gigline/character-anim.fbx
# <engine>/Engine/Binaries/Linux/UnrealEditor-Cmd <uproject> -run=pythonscript \
# -script=<repo>/Tools/ue_character_anim_import.py -nullrhi -unattended -nopause -stdout
#
# Imports the animated FBX next to the existing character assets and verifies the
# result rather than assuming it: an FBX whose export omitted `bake_anim` imports
# without error and produces a skeletal mesh but no AnimSequence, which is exactly
# the failure this tool must catch.
#
# Destination: /Game/Test/Character. Result file: /tmp/gigline/character-anim-import.txt
import glob
import os
import unreal
SRC = "/tmp/gigline/character-anim.fbx"
DEST = "/Game/Test/Character"
PREFIX = "CharacterV2"
RESULT = "/tmp/gigline/character-anim-import.txt"
lines = []
def log(msg):
lines.append(str(msg))
unreal.log(str(msg))
def main():
if not os.path.exists(SRC):
log(f"missing {SRC}; run Tools/character_anim_build.py first")
return
task = unreal.AssetImportTask()
task.filename = SRC
task.destination_path = DEST
task.destination_name = PREFIX
task.automated = True
task.save = True
task.replace_existing = True
ui = unreal.FbxImportUI()
ui.import_mesh = True
ui.import_as_skeletal = True
ui.import_animations = True
ui.import_materials = True
ui.import_textures = False
# UE 5.8 has no SKELETON_ONLY content type (FBXICT_ALL/GEOMETRY/SKINNING_WEIGHTS
# only), so the clips come in with the mesh and the verification below decides
# whether that worked. The mesh is replaced at the same path by design.
ui.skeletal_mesh_import_data.import_uniform_scale = 1.0
task.options = ui
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
# Verify: the clips are the deliverable, not the mesh.
registry = unreal.AssetRegistryHelpers.get_asset_registry()
found = []
for asset in registry.get_assets_by_path(DEST, recursive=False):
name = str(asset.asset_name)
if not name.startswith(PREFIX):
continue
obj = unreal.EditorAssetLibrary.load_asset(f"{DEST}/{name}")
if isinstance(obj, unreal.AnimSequence):
length = obj.get_play_length()
# UE 5.8 exposes only play length and sequence length here; the sampled
# frame count is derived so the report still states the clip duration.
frames = round(length * 30.0)
found.append(name)
log(f"AnimSequence {name:34} {length:.3f} s (~{frames} frames at 30 fps)")
elif isinstance(obj, unreal.SkeletalMesh):
skeleton = obj.get_editor_property("skeleton")
log(f"SkeletalMesh {name:34} skeleton={'ok' if skeleton else 'MISSING'}")
if not found:
log("FAILED: no AnimSequence was created — the FBX carried no baked animation")
lines.append("VERDICT: FAIL")
return
log(f"{len(found)} AnimSequence(s) imported: {', '.join(sorted(found))}")
lines.append("VERDICT: PASS")
try:
main()
except Exception as exc: # noqa: BLE001
import traceback
log(f"FAILED: {type(exc).__name__}: {exc}")
log(traceback.format_exc())
lines.append("VERDICT: FAIL")
unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True)
os.makedirs(os.path.dirname(RESULT), exist_ok=True)
with open(RESULT, "w", encoding="utf-8") as handle:
handle.write("\n".join(lines) + "\n")
+5 -1
View File
@@ -127,6 +127,10 @@ def build():
ia_reset.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN) ia_reset.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
save(ia_reset) save(ia_reset)
ia_cam = fresh("IA_ToggleCamera", unreal.InputAction, unreal.InputAction_Factory)
ia_cam.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
save(ia_cam)
ia_pause = fresh("IA_Pause", unreal.InputAction, unreal.InputAction_Factory) ia_pause = fresh("IA_Pause", unreal.InputAction, unreal.InputAction_Factory)
ia_pause.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN) ia_pause.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
save(ia_pause) save(ia_pause)
@@ -136,7 +140,7 @@ def build():
(ia_move, "Gamepad_LeftY"), (ia_strafe, "Gamepad_LeftX"), (ia_move, "Gamepad_LeftY"), (ia_strafe, "Gamepad_LeftX"),
(ia_look, "MouseX"), (ia_look, "MouseY"), (ia_look, "Gamepad_Right2D"), (ia_look, "MouseX"), (ia_look, "MouseY"), (ia_look, "Gamepad_Right2D"),
(ia_jump, "SpaceBar"), (ia_jump, "Gamepad_FaceButton_Bottom"), (ia_jump, "SpaceBar"), (ia_jump, "Gamepad_FaceButton_Bottom"),
(ia_reset, "R"), (ia_pause, "Escape"), (ia_reset, "R"), (ia_pause, "Escape"), (ia_cam, "V"),
] ]
mappings = [] mappings = []
+18 -2
View File
@@ -19,14 +19,19 @@ import unreal
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import ue_common # noqa: E402 import ue_common # noqa: E402
import ue_placement # noqa: E402
FBX = "/tmp/gigline/hero-facade.fbx" FBX = "/tmp/gigline/hero-facade.fbx"
DEST = "/Game/Hollywood/ElCapitan" DEST = "/Game/Hollywood/ElCapitan"
ACTOR_LABEL = "Hero_Facade" ACTOR_LABEL = ue_placement.HERO_ACTOR
LEVEL = "/Game/Hollywood/DemoMap" LEVEL = "/Game/Hollywood/DemoMap"
# Unreal centimetres: south-east corner of the façade, on the building line. # Unreal centimetres: south-east corner of the façade, on the building line.
PLACEMENT_CM = (-12024.5, -1450.0, 0.0) # Placement lives in Tools/ue_placement.py. It used to be a literal here, and it
# drifted from ue_build_demo_map.py: this file kept the pre-0.1.0 position
# (-12024.5, -1450) while the builder staged (-13336, 1307), so running the importer
# on its own moved the hero 13 m east and onto the north side of the boulevard.
PLACEMENT_CM = ue_placement.HERO_LOCATION_CM
RESULT = "/tmp/gigline/hero-facade-import.txt" RESULT = "/tmp/gigline/hero-facade-import.txt"
lines = [] lines = []
@@ -118,6 +123,17 @@ def measure_and_place():
comp.set_static_mesh(asset) comp.set_static_mesh(asset)
comp.set_mobility(unreal.ComponentMobility.STATIC) comp.set_mobility(unreal.ComponentMobility.STATIC)
log(f"placed {ACTOR_LABEL} at {PLACEMENT_CM} (cm) on {LEVEL}") log(f"placed {ACTOR_LABEL} at {PLACEMENT_CM} (cm) on {LEVEL}")
# Prove it landed on its verified footprint, not merely that it spawned.
box = asset.get_bounding_box() if asset else None
if box:
lo = box.min.x + PLACEMENT_CM[0]
hi = box.max.x + PLACEMENT_CM[0]
problem = ue_placement.describe_span_failure("hero", lo, hi)
if problem:
log(f"PLACEMENT ERROR: {problem}")
else:
log(f"span ok: X {lo:.0f}..{hi:.0f}")
_make_map_writable(LEVEL) _make_map_writable(LEVEL)
saved = unreal.EditorLoadingAndSavingUtils.save_map( saved = unreal.EditorLoadingAndSavingUtils.save_map(
unreal.EditorLevelLibrary.get_editor_world(), LEVEL) unreal.EditorLevelLibrary.get_editor_world(), LEVEL)
Loaded 100 of 103 files, more files were not shown because too many files have changed in this diff. Show more