The transient settle at Z 78 was the first physics-asset recreation; repeating the
delete + rebuild + assign cycle flipped the pawn to simulating=0 and stuck at spawn,
with the same box centre -18 that had settled. The delete+recreate is the failure,
not the box: delete_asset does not fully release the package, so NewObject reuses
stale state and the rebuilt body stops simulating.
- Plugin enablement (5e328f5) stays the solid fix (4 wheels).
- The physics asset needs a proper fix: set PhysicsType=PhysType_Simulated and update
in place, not delete+recreate.
- ue_vehicle_import.py SOURCE is stale (/tmp/gigline/hatch vs /tmp/gigline/vehicle for
the skeletal FBX), so the wired regeneration is never reached.
- Driving is still untested.
The Chaos vehicle never existed: ChaosVehiclesPlugin is EnabledByDefault=false and
was absent from GigLine.uproject, so FChaosVehicleManager was null and
OnCreatePhysicsState skipped CreateVehicle() (read from the engine source). The
pawn stayed at setups=4 wheels=0 output=null and the mesh fell back to the regular
rigid-body solver, which is what teleported upward.
- Enable ChaosVehiclesPlugin in GigLine.uproject. CreateVehicle() now runs:
'wheels 4', 'output present', 'setups=4 wheels=4'.
- Drop the constructor's SetSimulatePhysics(true) / collision-profile override: the
Chaos movement component drives the mesh through async physics, and forcing the
regular solver kept the body out of the vehicle solver.
The plugin fix turns the teleport into a stable 'float at Z 545' (540 cm above the
floor) in both probe maps - suspension tuning is the next bug.
Rebuilt and re-ran the vehicle probe workflow:
- Control (no terrain) falls and settles (300 -> 78 cm).
- Terrain rises (313 -> 545 cm) with body velocity pinned to 0 and zero overlaps.
- Turning physics OFF at 3 s stops the rise, so the Chaos vehicle solver is
teleporting the body (transform set directly, no velocity) - not an external
SetActorLocation.
- 0 wheels in both runs, so the missing wheels are not the trigger; the terrain is
what flips fall to rise.
Ruled out the asset pipeline with commandlets: the FBX, the imported
SK_Vehicle_Sedan (476x228x144 cm), the physics asset (452x205x40) and Terrain_DEM
(380x210 m) are all correct; the logged '1 cm bounds' is a stale component read,
not a bad mesh.
Next: bisect step 2 - a stock AWheeledVehiclePawn beside AGigLineVehicle in the
terrain probe map.
Banner lettering (JIMMY KIMMEL LIVE): new Tools/gen/generate_banner_texture.py
draws the exact title with Pillow + Liberation Sans Bold - an image model would
mangle a trademark string, and Qwen-Image 2.1 is research-only. Gold JIMMY /
KIMMEL / LIVE stacked on a crimson ground. ue_hero_facade_import.py generalises
import_albedo to import_texture and wires the banner into M_Facade_Banner_Ref as
Base Color (tiling 1.0), replacing the flat dark tint with a brighter red per the
owner's 'less dark, more bright'.
Re-rendered all three ReviewMap staging shots (front, oblique, portals) and
updated their Screenshots/README.md status rows.
The hero facade rework is now closed at 'surface detail that reads at 45 m'. The
remaining elevation work - volute capital depth, lattice grille depth, and the
full realism pass - is deferred to 0.9.0 per the owner, and the 0.9.0 hero item
records the hand-off.
The motto band and moulded surround had the same burial as the medallions: offset
0.06 left their front face at Y -0.44, inside the 1.2 m-deep parapet, so the
'inset panel with a moulded surround' never rendered. Only the inscription (moved
to 0.40 in the previous commit) read.
- Band recessed at offset 0.34 (Y -0.16), surround proud at 0.43 (Y -0.07),
inscription stays 0.40 (Y -0.10).
- Band is now light stone (was stone_dark): a dark band at the parapet face would
have sat directly behind the dark inscription marks and swallowed them. The
reference is dark incised lettering on a pale band.
- Removed the dead duplicate 'Hero_MottoBand 0.40' entry - the earlier 0.06 match
won, so the 0.40 was silently ignored.
Rendered and judged; the advisory judge no longer flags the band.
The medallions were not just flat - they were buried. seat_on_front put
Hero_Medallion at offset 0.08 (front Y -0.42) while the 1.2 m-deep parapet stands
at Y -0.15, so both discs sat inside the parapet and never rendered. The motto
band and surround have the same burial (offset 0.06) and are a separate follow-up.
- Three stepped layers per medallion with per-layer prefixes so seat_on_front gives
each its own stand-off: Rim (0.55 m, dark, offset 0.36 -> Y -0.14), Field
(0.38 m, light, offset 0.43 -> Y -0.07), Boss (0.15 m, dark, offset 0.50 -> Y 0.00).
A single Hero_Medallion prefix would both flatten the relief and (via startswith)
swallow the specific prefixes, so it is removed.
- Moved from +/-6.2 (inside the inscription text, which spans +/-6.87) to +/-9.8.
- Fixed Tools/review/hero_iterate.sh, which still had the pre-tools-move
'cd dirname/..' and landed in Tools/ instead of the repo root.
Rendered and judged; the advisory two-model judge (no, 0.93) no longer flags the
medallions. Owner review pending.
Records what cost time today, with the evidence:
- the four traps of the folder move: sibling imports (sys.path needs both the tools root AND the script's own
folder), shell scripts resolving REPO relative to themselves, stale result files passing as success, and
Unreal hanging rather than failing when a second editor is alive - plus the pkill-matches-its-own-command-line
gotcha;
- measured proof that LevelEditorSubsystem.new_level() cannot replace a map in one session (create -> delete ->
create returns False, and delete_asset leaves the .umap), so emptying actors is the workable step; and that a
falling actor count (600 -> 409) was the fix that audits clean;
- two-model judging: the reconciliation rule, the 404 that retired claude-3.5-haiku, and the observation that
this judge is consistently pessimistic on our content so the model disagreement is the signal;
- local ComfyUI: it is a Stability Matrix install nothing here starts, the documented graphs are UI format that
/prompt rejects, albedo-only output, no seamless sampler in 0.44, and per-model licence rules;
- hero facade: detail was modelled inside the wall, the hero's materials are not the kit's, Unreal keeps a
material on re-import, materials must be saved themselves, and project_dir() is not the repo root;
- the render chain is build -> import -> render, and e2e_asset_check.py is the command that runs it all.
One directory with sixty scripts stopped being navigable, and the grouping is also the sandbox boundary:
Tools/unreal/ commandlets and the two map builders, with ue_common.py and ue_placement.py as the
shared imports (they need the engine)
Tools/blender/ builders, exporters, the FBX preset, the MPFB character pipeline, blender_screenshots
(they need the Blender Flatpak)
Tools/review/ the visual loop: render_screenshots.sh, judge_screenshot.py, hero_compare.py,
build_comparison_md.py, e2e_asset_check.py, hero_iterate.sh, visual_loop.py,
visual_targets.json
Tools/gen/ image and texture generation: the ComfyUI driver, deterministic texture writers,
reference fetching
Tools/input/ OS-level input injection
root _common.py, check-lfs.sh, addon_compat_probe.py, sibling_asset_audit.py, tests/
Every moved script now resolves the repository root as ../.. and puts the tools root plus its siblings on
sys.path, so 'import ue_common' still works from unreal/. The test suite bootstrap does the same for the five
folders. render_screenshots.sh takes the folder as a third argument to run_cmdlet, and the e2e driver,
hero_iterate.sh and the comparison generator were repointed.
51 README rows now link to their new location, the README gains a Layout section naming each folder's
sandbox, and the architecture notes and Blender README follow the moves.
Verified: 108 tests pass, prettier is clean, judge_screenshot.py and hero_compare.py run from their new
homes, and the e2e run was aborted mid-way (it had been rebuilding an empty DemoMap for 15 minutes) and is
being rerun against the new layout.
The 94 symlinks in ~/.agents/skills and the full second copy in
~/.pi/agent/skills/blender-skills were the same packs under two roots, so Pi
reported a collision for every one of them on every start and silently dropped
the loser.
- The 93 upstream packs, the shared references/ notes and the LICENSE now live
once, flat and one level down, in the common agent folder ~/.pi/agent/skills/.
- The repository keeps only the project-specific pack:
.agents/skills/blender-skills/blender-director/. Its ../references/ links now
point at ~/.pi/agent/skills/references/ because it no longer sits beside them.
Its SKILL.md declared metadata.role twice (specialist, orchestrator), which is
fatal YAML - the pack had never actually loaded.
- AGENTS.md and Docs/toolchain.md describe the split and warn against copying the
common packs back into the repository.
Verified by walking all three discovery roots and parsing every frontmatter:
97 skills loaded, 0 malformed, 0 collisions.
Every .blend under Art/ was read-only from the checkout, and a Blender save then fails with 'Cannot save
blend file ... is not writable'. In Tools/hero_facade_build.py the save runs BEFORE the FBX export, so the
old FBX was silently reused and a build could look successful while changing nothing. All 20 .blend and
.blend1 files are now writable and 'find Art -name *.blend ! -perm -u+w' returns nothing.
Re-rendered the full capture set afterwards. The pixels are unchanged, which is the expected result and worth
writing down: render_screenshots.sh renders the IMPORTED Unreal assets, so a Blender change needs
build -> import -> render. Chmod alone cannot change an image.
Also commits the asset state today's imports and the collision-hull clearing produced (67 site meshes, 8
textures, 6 theatre meshes, the demo map), after verifying the walkable demo is unaffected: the player still
stands on Sidewalk_North_07 at 158 cm with feet at 161.
The road was never mis-modelled: a 279 m strip imports with ONE auto-generated convex hull, and a
convex hull's top is the strip's maximum height. A downward trace therefore landed at 366 cm - a flat
lid about 2 m above the visible road, which sits at roughly 152 cm there. That single fact explains
every failed sidewalk fix, the 'road above its own sidewalk' reading, and why the player always ended
up on Road_Hollywood.
Slicing the road the way the sidewalks already were fixes it, and the cross-section is now continuous:
Terrain_DEM 154
Sidewalk_North_07 158
Road_Hollywood_07 158 (was 366)
Two changes had to land together: the road exported in slices, and the map stopped staging the single
Road_Hollywood asset - which still sat on top of the slices and won every trace until removed.
All 23 captures regenerated; the player and the car both stand on a continuous ground plane.
Found the mechanism behind the 2 m disagreement. street_mesh.py builds the road across -10.5..+10.5 m
and the north sidewalk +10.5..+15.8 m, so along the straight the two bands meet exactly at the gutter.
The strips are generated along a curved centreline, and the fan-out at the bend pushes the road's outer
edge to -13.8 m while the sidewalk occupies -17.3..-11.8 m - a 2 m overlap where the road surface is
366 cm and the sidewalk 158 cm.
That is the surface a downward trace at the player's Y kept finding, and the reason the scene read as
the road hovering above the pavement.
The fix is a clamp in strip() so a row's outer offset cannot pass the nominal gutter, or trimming the
road's ends - one function, then re-export, re-import and the 14 s probe confirms it. Recorded in
MEMORY.md and TODO.md. The demo reads correctly meanwhile because both objects stand on the sidewalk.
Regenerated the street blend from OSM and the DEM (the code was already correct: 279.6 m centreline,
saved clean) and sampled both surfaces at the player's X in metres:
Road_Hollywood y -9.0..-9.1, z 0.87..0.94
Sidewalk_North y +11.9..+12.0, z 1.51..1.59
The road is the south band at about 0.9 m and the north sidewalk is about 1.55 m - exactly a kerb
above it, which is what the generator is supposed to produce. The source is correct.
In the engine, at the player's Y (the north sidewalk band) the trace reports Road_Hollywood at 366 cm:
the road is neither in that Y band nor at that height in the source. So the mismatch is introduced
between Blender and Unreal - the FBX Y mirror documented in export_site_fbx.py, or the importer's
transform - not by the geometry or the placement code.
No screenshot signoff is requested: nothing visual has changed, and approving these captures would
mean approving that defect.
Added a -profile probe: one downward trace every 20 m along the street at the player's Y, naming
the surface each time. It settles the question the bounding boxes could not:
x=-14336..-2336 z=366 Road_Hollywood (flat)
x=-336 z=302 Road_Hollywood
x=1664 z=5 Terrain_DEM
The road is FLAT at 366 cm across the demo block, while the sidewalk slices follow the DEM and
descend 380 -> 104 cm. So the ~2 m gap is not an import artefact and not a collision problem: the
road itself does not follow the elevation the sidewalks follow.
That also explains why every sidewalk fix failed - the surface the player stands on is this flat
road, and the sloped sidewalk is metres below it.
Next: how rows is built in Art/blender/street/street_mesh.py. The road and the sidewalk read the same
dem_z(x, y), so a flat road means the sampler returns the same value for every row.
Checked the obvious escape route and closed it: there are no duplicate Demo_Road_Hollywood actors after
several import rounds - one actor, identity transform, mesh Z -44..366. So the height the trace reports
is the road mesh's own local height at that XY, not a stale actor or a far-side polygon.
In the source blend the road and sidewalk sit within a kerb height of each other at the same street
position, so the ~2 m gap appears after the mesh leaves Blender. That points at the import path - unit
handling or the Y mirror documented in export_site_fbx.py - rather than the strip generation, and it is
the next thing to test.
Two findings, one a bug and one the real cause.
Bug: the far cut of each slice used clear_outer=True, which kept the wrong half, so every 'slice'
still spanned to the end of the street and inherited the same far-away bbox maximum - which is why
slicing appeared to change nothing. Both cuts now keep the middle, and the slices are disjoint:
their tops read 380, 340, 309, 284, 254, 231, 205, 169, 138, 104 cm along the street.
Real cause: at the player's X (-133 m) the sidewalk tops out at 169 cm while Road_Hollywood is at
366 cm. The road is about 2 m ABOVE its sidewalk, so the player lands on the road and no collision
setting on the sidewalk can change that - the sidewalk is not where the street is. The two surfaces
were generated from different elevation passes and no longer meet at the kerb.
That is site-pipeline data, not placement. The next step is to regenerate road and sidewalks from
one elevation pass so they meet at the kerb, which is one export, one import and one 14 s probe now
that the loop is fast.
The ground measurement was the slowest part of every iteration: the game ran until the shell's
timeout because nothing told it to stop. -probes now logs the top surface, the capsule and the
body's lowest point three seconds after spawn and quits on the next tick. A run went from minutes
to 13.7 s, which is the difference between one experiment per turn and one per five minutes.
Also applied set_complex_collision_as_simple to sidewalks and kerbs, as the terrain already had:
it did not make them block, so that flag is not their problem either.
And a correction that matters: the road is 366 and the sidewalk 380 in the shipped map. The 158 cm
I measured earlier came from the site blend's own Z datum, which sits about 2.2 m below the map's
because the terrain was added after that blend was written. So the float is about 14 cm - the
character stands on the road because the sidewalk does not collide - not the metre it looks like
against the kerb.
The site exporter now splits sidewalks and kerbs into ~20 m pieces
(SEGMENTED / SEGMENT_M in Art/blender/site/export_site_fbx.py), 56 slices in all, so each has a
bounding box that means something where it stands instead of reporting the highest point on a
279 m slope. They import and stage (63 site meshes in the demo map).
The hypothesis behind it is dead, and that is the useful part: with the invisible sidewalk
stopgap retired, the player now falls to Road_Hollywood at 366 - the slices do not block either.
So the 279 m aspect ratio was never the cause; a 20 m slice of the same mesh fails identically.
The sidewalk geometry itself is what does not collide, and the next place to look is its shape -
an open surface with no volume, inverted winding or zero-thickness triangles - not the import
flags and not the size.
The stopgap retirement stands: the player walks on the road, which is real geometry that does
collide, rather than on an invisible ledge.
Records the finding rather than the tool that failed to fix it: the staged Sidewalk_North mesh
carries QueryAndPhysics and CTF_USE_SIMPLE_AND_COMPLEX with auto-generated collision, and still
lets both a trace and a capsule through. The likely cause is a degenerate convex hull for a
279 m x 8.6 m x 3.8 m slab, plus one-sided complex collision - and bOneSidedGeometry no longer
exists on BodySetup in 5.8, so the flag cannot be flipped from Python.
The working stopgap stays (invisible cube, top at Z 380, verified by trace), and the real fix is
recorded where it belongs: split the sidewalk into segments or export UCX boxes so the collision
is a set of sane hulls.
Deletes Tools/ue_fix_ground_collision.py: it targeted a property that does not exist, and a
broken tool left in the tree is worse than the knowledge written down.
The skin was never grey. A red test colour reached the render and the slot was confirmed
holding M_Character_Skin, so the material and the assignment were both fine - the body
was simply standing in building shadow and lit almost entirely by the blue skylight
(intensity 2.6). Warm skin under blue fill reads blue-grey.
Raising the sun from pitch -62 to -74, same camera:
face (99, 98, 100) -> (131, 121, 117)
arms (67, 74, 81) -> (71, 78, 85)
This touches every approved shot, so it sits on the branch for the owner's approval
rather than being folded in. Also records that the Unreal commandlet silently drops
unknown switches, which is why a --unlit-test flag never took effect.
Solidify thickness went from 1 cm to 2 cm and the shell is fattened 8 mm along its own
normals afterwards. That closed the shoulders, hips and knees where the body was
showing through the cloth.
Evaluation against the blueprint, same shot and box before and after:
mean deltaE 31.9 -> 21.8
palette deltaE 25.4 -> 19.7
aspect delta 1.24 -> 1.22
Also fixes a crash I introduced: binding the materials in Tick used
ConstructorHelpers::FObjectFinder, which is fatal outside a constructor - the game died
with 'FObjectFinders can't be used outside of constructors' and the render run failed
with signal 11. The binding now lives in BeginPlay and uses LoadObject.
Not fixed, and recorded: the arms and face still render neutral grey, so
M_Character_Skin itself is suspect rather than the assignment.
Follows the character-artist clothing workflow literally: duplicate the body's own
faces for the garment base, solidify for cloth thickness, and keep each piece a
separate mesh. Attached as leader-pose components beside the hair, so no second rig.
- Tools/character_clothing_build.py builds three garments from the body mesh, trimmed
geometrically (height band plus a width limit) instead of by dominant bone: the bone
filter left holes wherever a face's strongest bone was outside the allowed list.
- Tools/ue_character_clothing_import.py imports them as CharacterTee / CharacterJeans /
CharacterShoes, each arriving with its own single material slot.
- The pawn attaches all three, hidden with the body in first person.
Three traps recorded in MEMORY.md, each of which cost a build: MPFB weights its helper
geometry to groups that dominate every vertex, Blender refuses to apply a modifier to a
mesh with shape keys, and object-mode polygon selection is discarded on entering edit
mode - the delete wiped the whole garment.
Known defects, in TODO.md: the garments have coverage holes at shoulders, hips and
knees, and the hair reads light and wispy since its material was repointed to the
masked strand sheet.
Two tools that turn character work into something measurable.
- Tools/comfyui_character_blueprint.py generates a four-view sheet (front, side, back,
head) plus a contact sheet from one description, with fixed seeds and a JSON record
of prompts and prompt ids. First sheet: 'GigLine walker', lean pedestrian in a navy
tee, jeans and sneakers.
- Tools/character_match.py compares a rendered character against a sheet: figure
aspect, fill and coverage deltas, mean CIE76 colour distance, dominant-palette
distance, and a side-by-side montage. The deltas stay separate on purpose - a single
similarity score between a photoreal sheet and a low-detail body would be a number
with no meaning.
- comfyui_generate.py grew wait_for_image() so both tools share the polling path
instead of duplicating it.
- 13 unit tests for the colour maths, masks and palette; 95 tool tests pass.
First real result, recorded rather than smoothed over: aspect delta 1.24, fill delta
0.12, mean deltaE 31.9, palette deltaE 25.4. The montage shows the reason - the model
is a nude body and the blueprint wears a tee and jeans, so clothing dominates every
metric. Figure segmentation is explicit (--render-box): auto-detection picked backdrop
blobs on every attempt, and a background plate is the proper fix.
Follows the character-artist skill on two points it is explicit about: hair for a
game character is a card system, and swappable parts stay separate meshes.
- Tools/character_hair_build.py builds SM_Char_Hair: a scalp cap plus 18 cards in
three rings, quads only, every vertex weighted 1.0 to the head bone, UV-mapped for
the strand sheet from Tools/generate_character_textures.py. 321 verts / 404 tris.
- Tools/ue_character_hair_import.py imports it as its own skeletal mesh, which is the
point: joined into the body, Unreal collapses the second material slot and the hair
renders with the skin material.
- The pawn carries it as a second USkeletalMeshComponent on SetLeaderPoseComponent, so
it follows the head bone with no second rig and hides with the body in first person.
Still wrong and recorded: the cards point outward, so the silhouette reads as a spiked
crown rather than a short cut. The next pass orients them tangentially along the
skull with a second overlapping row. Tools/ue_character_materials.py also segfaults in
the headless commandlet when editing a material graph, so the generated textures are
not wired yet and the hair keeps its Blender base colour.
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.
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.
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.
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.
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.
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.
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.