Add a repeatable screenshot capture and compare smoke

Both capture helpers now accept --out=res://docs/screenshots, and
tools/check_screenshots.sh renders the staged set (using xvfb-run when headless)
before running screen_designs.py compare. Record the judge result: 7 match,
4 review, 0 diff. Headless 226/226 and the smoke both pass in a clean worktree.
This commit is contained in:
zwitschi committed 2026-11-01 16:43:10 +01:00
1 parent b39fee6176
commit d96f53c285
6 files changed
+159 -171

No files matched your search

+51 -23
View File
@@ -49,13 +49,26 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
## Handoff (read this first) ## Handoff (read this first)
- **Visual-alignment pass planned (2026-11-01):** `TODO.md` gains a "Visual - **Status + active passes (2026-11-01, moved from `TODO.md`):** v0.6.0 shipped — the Blender
alignment with rendered mockups" section. Goal: restructure generated-image character/gear/prop library is integrated, AO maps are baked and wired, and the environment
storage, move the reference mockups out of `docs/screens/designs/` into a textures are lifted to 1024² with normal maps (see `DONE.md`). Headless **222/222**, Python
runtime-accessible root (e.g. `assets/images/`), load the proposed UI/scene **81**; the scheduled presentation pass is complete. Two passes remain in `TODO.md`:
art at runtime (start menu, selection screens, HUD, panels), and make the game * **Visual alignment with rendered mockups:** the AI references in `docs/screens/designs/`
match the mockups — verified by `screen_designs.py compare` + `judge`. Not define the intended look of every screen. The pass reorganizes generated-image storage,
started; no code moved yet. loads the proposed UI/scene art at runtime (`src/game/ui/art.gd`, `assets/images/`), and
aligns the selection screens, HUD, and scene visuals with those mockups — verified with
`screen_designs.py compare` + `judge`.
* **Realism pass** (plan: `PLAN.md` → Visual Quality Direction → Realism pass): replace the
flat skyline backdrop and the flat surface tiles with AI-generated realistic backgrounds
and textures, add example 3D scene frames as art direction, and measure with the
screenshot tools. Rule: never overwrite an existing image — new outputs get new paths
(`next_free_path` -vN suffix). Raw → `assets/textures/ai/v2/raw/` (gitignored); candidates
→ `assets/textures/ai/game_ready/v2/` (gitignored pool, only validated picks tracked,
ADR-0014); scene refs → `docs/screens/concepts/`; trials → `docs/screens/realism/<run>/`.
Realism report 2026-11-01-v2 mean judge scores: backdrop 2.6 → 3.6 (v2 day + night
skyline) → **4.0** (v2 day + dusk skyline for the Evening phase); texture detail 1.2 →
1.6–1.75; overall 1.6 → 2.0. Scores swing ±1–2 between runs (trend only); the
`characters` score moved 3.0 → 1.0 with identical NPCs, showing the noise.
- **Phase 2 — AI texturing (this agent, DONE 2026-11-01).** Phase 1 assets were - **Phase 2 — AI texturing (this agent, DONE 2026-11-01).** Phase 1 assets were
already in the tree (untracked) and are preserved. Generated six seamless AI already in the tree (untracked) and are preserved. Generated six seamless AI
@@ -138,22 +151,36 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
flat plane, so avoidance (not a baked mesh) routes guests around props. Placements live in flat plane, so avoidance (not a baked mesh) routes guests around props. Placements live in
`PROP_PLACEMENTS` with `placement_problems()` validation; variants come from `variant_for(index)`. `PROP_PLACEMENTS` with `placement_problems()` validation; variants come from `variant_for(index)`.
`smoke_props.gd` must be windowed because the guard reads captured-mouse input. `smoke_props.gd` must be windowed because the guard reads captured-mouse input.
- **Realism pass tooling (2026-11-01):** `tools/scene_concepts.py` (`list`/`generate`/`process`/ - **Realism pass (2026-11-01, completed + adopted):** `tools/scene_concepts.py`
`audit`/`realism`) never overwrites an image (`next_free_path` adds `-vN`; `process` skips (`list`/`generate`/`process`/`promote`/`audit`/`realism`) never overwrites an image
existing targets). Raw AI output: `assets/textures/ai/v2/raw/` (gitignored). Candidate pool: (`next_free_path` adds `-vN`; `process` skips existing unless `--new`; `promote` keeps an
`assets/textures/ai/game_ready/v2/` (gitignored except the validated picks un-ignored in existing pick). Raw AI output: `assets/textures/ai/v2/raw/` (gitignored). Candidate pool:
`.gitignore`: skyline day-v4/dusk-v3/night-v4 plus brick/pavers/asphalt/asphalt_wet + normals). `assets/textures/ai/game_ready/v2/` (gitignored) and the **tracked runtime assets** are
Concepts: `docs/screens/concepts/` (committed, `.gdignore`, targets in its README). Runs: `.../v2/picks/{skyline_day,skyline_dusk,skyline_night,brick_facade,sidewalk_pavers,asphalt,
`docs/screens/realism/<run>/` (PNG captures gitignored; `report*.{json,md}` + `sheet.jpg` asphalt_wet,awning_canvas}.png` (+ `_normal`). Concepts `docs/screens/concepts/` (committed,
committed). `src/test/capture_realism.gd` swaps textures at runtime by the material's `.gdignore`, targets in its README). Runs `docs/screens/realism/<run>/` (PNG captures
*original* albedo path (`--swap/--night-swap/--tint/--normal=OLD=NEW`) and writes to gitignored; `report.{json,md}` + `sheet.jpg` committed).
`--out=res://...`. Changing game code is not needed for an A/B test. Gotchas: `src/test/capture_realism.gd` runs five stages (day 0 s / evening 620 s / late 760 s /
gpt-5-image-mini honours `background=transparent` for the 3:2 skyline requests, but it writes joke_shop 700 s / studio 700 s; `--stages=` subset) and swaps textures by the material's
garbled "HOLLYWOOD" lettering unless the prompt forbids signs. Skyline strips are 2:1 to match *original* albedo path. **Gotcha:** in the SceneTree script, do not overwrite `_phase` after
one UV repeat of the ring. `elapsed_s = 620` gives the *Evening* phase (pink sky), not night. `_begin_stage()` sets `done`, or the run loops forever between stage/shoot (`_queue` empty).
The gemini-flash-lite realism scores are noisy (±1–2), and the judge sometimes returns a 400 **Gotcha:** `elapsed_s = 620` is *Evening* (dusk), 700+ is Late. Godot 4.3
"API key not valid" from the provider, which the one retry covers. None of the v2 assets are `StandardMaterial3D` has no `fog_enabled`; the skyline is a `ShaderMaterial` with
wired into the game yet. `render_mode unshaded, cull_disabled, fog_disabled` + `ALPHA_SCISSOR_THRESHOLD` so the
panorama's own haze shows. Backdrop plane is 90 m tall with 2 UV repeats (~200×90 m per
repeat, near the 2:1 panorama aspect). Legacy `assets/textures/skyline.png` is no longer
loaded but stays on disk for the `test_world` asset check.
**Phase look:** day sun 0.62 / exposure 0.6 / ambient 0.22; evening sun 0.12 / exposure
0.72 / dark violet sky; late exposure 0.95 / ambient 0.10 with the wet asphalt street and a
bright skyline tint (1.5) so the night windows read. Stripes: `stripe_period()` (detrended
autocorrelation) + `align_stripes()` (resample to whole periods) replace the edge crossfade
for `awning_canvas`; `audit_striped_tile()` compares the wrap step with the harshest interior
step.
**Judge:** `google/gemini-2.5-flash-lite` currently answers HTTP 400 "API key not valid" for
this key, so runs use `--model google/gemini-2.5-flash`; scores are noisy (±1–2), so
`realism --repeats 3` averages and `summarize()` reports a like-for-like mean over the shots
every set captured. Final: overall 0.8→1.0, backdrop 0.6→1.6, metric gap 0.444→0.348.
`screen_designs.py judge` gained one retry for the same transient 400.
- **Blender asset library (v0.6.0, integrated 2026-11-01):** - **Blender asset library (v0.6.0, integrated 2026-11-01):**
`tools/blender_assets.py` (`build`/`render` in Blender; `audit`/`sheet`/`list` stdlib) writes `tools/blender_assets.py` (`build`/`render` in Blender; `audit`/`sheet`/`list` stdlib) writes
`assets/models/blender/{npc,npc_gear,props}/*.glb`, `manifest.json` (integration source of `assets/models/blender/{npc,npc_gear,props}/*.glb`, `manifest.json` (integration source of
@@ -212,6 +239,7 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
- Radio, wand, and body-camera timeline panels use their reference art as backdrops; forms sit on transparent, padded panels over the covered mockup controls. - Radio, wand, and body-camera timeline panels use their reference art as backdrops; forms sit on transparent, padded panels over the covered mockup controls.
- HUD target names use a boxed chip under the crosshair; escalation has a five-segment bar below the text label. - HUD target names use a boxed chip under the crosshair; escalation has a five-segment bar below the text label.
- Staged captures were regenerated from HEAD in a clean worktree; the main working tree's `docs/screenshots` is being rewritten by the realism pass, so do not overwrite it from that run. - Staged captures were regenerated from HEAD in a clean worktree; the main working tree's `docs/screenshots` is being rewritten by the realism pass, so do not overwrite it from that run.
- `tools/check_screenshots.sh` is the repeatable capture+compare smoke; both capture scripts accept `--out=res://docs/screenshots`. Judge verdicts on the staged set were 7 match / 4 review / 0 diff.
## Current State ## Current State
+49 -7
View File
@@ -287,11 +287,11 @@ Everything below is audited and shipped in the game; development-only outputs
| | | | | |
| --- | --- | | --- | --- |
| ![brick facade](assets/textures/ai/game_ready/brick_facade.png) | ![concrete](assets/textures/ai/game_ready/concrete.png) | | ![brick facade](assets/textures/ai/game_ready/v2/picks/brick_facade.png) | ![sidewalk pavers](assets/textures/ai/game_ready/v2/picks/sidewalk_pavers.png) |
| ![asphalt](assets/textures/ai/game_ready/asphalt.png) | ![awning](assets/textures/ai/game_ready/awning.png) | | ![asphalt](assets/textures/ai/game_ready/v2/picks/asphalt.png) | ![awning](assets/textures/ai/game_ready/v2/picks/awning_canvas.png) |
| ![skyline](assets/textures/ai/game_ready/skyline.png) | ![vignette](assets/textures/ai/game_ready/vignette.png) | | ![skyline day](assets/textures/ai/game_ready/v2/picks/skyline_day.png) | ![skyline night](assets/textures/ai/game_ready/v2/picks/skyline_night.png) |
- **Surface textures:** the six tiles above, loaded by `src/game/materials.gd` and `WorldBuilder`. - **Surface textures:** the v2 picks above (brick, sidewalk pavers, dry and wet asphalt, awning canvas) plus their normal maps, loaded by `src/game/materials.gd` and `WorldBuilder`; the three skyline panoramas are switched per shift phase. The first-generation tiles and `assets/textures/skyline.png` are no longer loaded.
- **UI art:** `assets/textures/ai/game_ready/icons/` (flashlight, door lock, wand, REC) and `assets/textures/ai/game_ready/cards/` (ID, wristband, ticket, credential, guest list), loaded by `src/game/ui/hud.gd`. - **UI art:** `assets/textures/ai/game_ready/icons/` (flashlight, door lock, wand, REC) and `assets/textures/ai/game_ready/cards/` (ID, wristband, ticket, credential, guest list), loaded by `src/game/ui/hud.gd`.
- **Generated models:** `assets/models/generated/*.glb` — bollard, bench, trash bin, planter, newspaper box, hydrant, and the NPC base — plus their colour atlases. - **Generated models:** `assets/models/generated/*.glb` — bollard, bench, trash bin, planter, newspaper box, hydrant, and the NPC base — plus their colour atlases.
- **NPC material maps:** `assets/textures/npc/*_{albedo,normal,orm}.png` and `palette.json`. - **NPC material maps:** `assets/textures/npc/*_{albedo,normal,orm}.png` and `palette.json`.
@@ -304,12 +304,50 @@ python3 tools/model_pipeline.py audit
python3 tools/npc_pipeline.py audit python3 tools/npc_pipeline.py audit
``` ```
### Realism pass: scene concepts, skylines and textures
The skyline, surface tiles and phase lighting were rebuilt against AI concept frames. The
concepts live in [`docs/screens/concepts/`](docs/screens/concepts/README.md); the validated
picks the game loads are in `assets/textures/ai/game_ready/v2/picks/` (day/dusk/night
panoramas, brick, sidewalk pavers, dry and wet asphalt, awning canvas, each with a normal
map).
`tools/scene_concepts.py` generates and processes new candidates, promotes the validated ones
to stable names, audits them, and scores captures against the concepts. It never overwrites an
image: new files get a `-vN` suffix, and `process` skips existing candidates unless `--new`.
```bash
python3 tools/scene_concepts.py generate --group scene # or backdrop / texture, --variants N
python3 tools/scene_concepts.py process && python3 tools/scene_concepts.py audit
python3 tools/scene_concepts.py promote # validated picks -> v2/picks/
godot --headless --path . --import
# A/B a candidate without touching game code: the same cameras, textures swapped at runtime.
godot --path . --script res://src/test/capture_realism.gd -- --out=res://docs/screens/realism/<run>/after
godot --path . --script res://src/test/capture_realism.gd -- --out=res://docs/screens/realism/<run>/try \
--swap=res://assets/textures/ai/game_ready/v2/picks/brick_facade.png=res://assets/textures/ai/game_ready/v2/brick_facade-v2.png
# Score before/after against the concepts (Pillow detail metrics + vision judge).
python3 tools/scene_concepts.py realism --run docs/screens/realism/<run> --judge \
--model google/gemini-2.5-flash --repeats 3
```
`capture_realism.gd` runs five stages (day, evening, late, Joke Shop, studio); `--stages=day,late`
limits it while tuning. The latest run,
[`docs/screens/realism/2026-11-01-adopt/`](docs/screens/realism/2026-11-01-adopt/report.md),
compares the pre-adoption captures with the current build: on the shots both sets captured the
judge's like-for-like means rose on every sub-score (overall 0.8 → 1.0, backdrop 0.6 → 1.6) and
the Pillow metric gap fell 0.444 → 0.348.
![realism run](docs/screens/realism/2026-11-01-adopt/sheet.jpg)
### Screen design references ### Screen design references
Screen mockups and screenshot comparison use `tools/screen_designs.py`. It turns the Screen mockups and screenshot comparison use `tools/screen_designs.py`. It turns the
screen descriptions in `docs/` (README captions, ADRs 0007–0010, `PLAN.md`, and screen descriptions in `docs/` (README captions, ADRs 0007–0010, `PLAN.md`, and
`architecture.md`) into reference images under `docs/screens/designs/`, then uses `architecture.md`) into runtime reference art under `assets/images/ui/` (with a
those images as the expected state to check the game's captures. reference-only snapshot in `docs/screens/designs/`), then uses those images as
the expected state to check the game's captures.
Each screen is tagged `scene` (a realistic first-person 3D world view) or `ui` Each screen is tagged `scene` (a realistic first-person 3D world view) or `ui`
(an in-game panel overlaid on a softened scene), and the generator appends a (an in-game panel overlaid on a softened scene), and the generator appends a
@@ -368,6 +406,10 @@ default 0.6 — expected when a concept mockup is more stylised than the game),
`compare` after regenerating the game screenshots with `compare` after regenerating the game screenshots with
`src/test/capture_screenshots.gd`. `src/test/capture_screenshots.gd`.
For a one-command drift check, run `tools/check_screenshots.sh`. It renders the
staged captures and prop gallery into `docs/screenshots/` (using `xvfb-run` when
no display is available) and then runs `screen_designs.py compare`.
### Setup ### Setup
1. Download the Godot 4.3 editor (official build) and put `godot` on your PATH. 1. Download the Godot 4.3 editor (official build) and put `godot` on your PATH.
@@ -484,7 +526,7 @@ Implemented so far:
- Godot 4.3 project scaffold, input map, procedural world builder - Godot 4.3 project scaffold, input map, procedural world builder
- Venue-driven simulation core: `ShiftCoordinator`, `VenueScenario`, `EventScheduler`, `NPCController` + FSM, `AccessVerifier`, `EscalationManager`, `RadioSystem`, `IncidentRecorder`, `ReputationEvaluator`, `CrowdSystem` - Venue-driven simulation core: `ShiftCoordinator`, `VenueScenario`, `EventScheduler`, `NPCController` + FSM, `AccessVerifier`, `EscalationManager`, `RadioSystem`, `IncidentRecorder`, `ReputationEvaluator`, `CrowdSystem`
- Three authored scenarios (Hollywood Boulevard building, The Joke Shop, Late Night TV Studio) with access policies, NPC archetypes, and event pools - Three authored scenarios (Hollywood Boulevard building, The Joke Shop, Late Night TV Studio) with access policies, NPC archetypes, and event pools
- Headless unit tests: **208 passing** (access rules and venue-correct access loadout, escalation, radio cooldown/battery, NPC reaction determinism, door lock, flashlight perception, persistence round-trip, settings round-trip, input remaps, navmesh, step colliders, skyline alpha, scenario data, full-shift simulation, wand screening, responder dispatch, localization strings, focus cone, action wheel, session shell, PLAN verbs and incident recording, crowd presentation, emergency aftermath, body-camera timeline, NPC theater poses, audio, accessibility, authored scenario content, string formatting, `.tres` venue round-trip, surface materials and environment, focus ranking and outfit/gait helpers, generated prop models, web export preset, prop atlas material, player movement, NPC character layers/LOD/queue, input bindings) - Headless unit tests: **227 passing** (access rules and venue-correct access loadout, escalation, radio cooldown/battery, NPC reaction determinism, door lock, flashlight perception, persistence round-trip, settings round-trip, input remaps, navmesh, step colliders, skyline alpha, scenario data, full-shift simulation, wand screening, responder dispatch, localization strings, focus cone, action wheel, session shell, PLAN verbs and incident recording, crowd presentation, emergency aftermath, body-camera timeline, NPC theater poses, audio, accessibility, authored scenario content, string formatting, `.tres` venue round-trip, surface materials and environment, focus ranking and outfit/gait helpers, generated prop models, web export preset, prop atlas material, player movement, NPC character layers/LOD/queue, input bindings)
- Simulation extras: protester archetype, CALL_SOMEONE reaction, door lock/secure mechanic (visual indicator), flashlight with night perception bonus, LATE shift phase reaching the violent-threat event - Simulation extras: protester archetype, CALL_SOMEONE reaction, door lock/secure mechanic (visual indicator), flashlight with night perception bonus, LATE shift phase reaching the violent-threat event
- First-person prototype with interaction HUD, access inspection, wand screening, radio report form (incl. emergency hold-to-confirm), dispatched responder avatars, aim-free targeting, venue cycling between all three scenarios, color-coded escalation feedback, procedural audio cues, navigation-mesh NPC paths, JSON-backed localized strings, persisted shift history, debug overlay, and shift review - First-person prototype with interaction HUD, access inspection, wand screening, radio report form (incl. emergency hold-to-confirm), dispatched responder avatars, aim-free targeting, venue cycling between all three scenarios, color-coded escalation feedback, procedural audio cues, navigation-mesh NPC paths, JSON-backed localized strings, persisted shift history, debug overlay, and shift review
- Control pass: focus cone on NPC heads with a billboarded blue circle in front of the face and name, action wheel (LMB/Space) with de-escalation-first slices, mouse-look freeze while a panel is open, acceleration-based walking, and named input actions (`action_menu`, `confirm`, `cancel`, `inspect`, `wand`) - Control pass: focus cone on NPC heads with a billboarded blue circle in front of the face and name, action wheel (LMB/Space) with de-escalation-first slices, mouse-look freeze while a panel is open, acceleration-based walking, and named input actions (`action_menu`, `confirm`, `cancel`, `inspect`, `wand`)
+20 -132
View File
@@ -1,9 +1,10 @@
# TODO # TODO
Godot 4.3 prototype. Latest release **v0.6.0** (Blender character/gear/prop library, baked AO, environment lift). Godot 4.3 prototype. Latest release **v0.6.0** (Blender character/gear/prop library, baked AO, environment lift).
Headless suite **222/222** plus **81** Python tooling tests; the session, PLAN-verb, crowd, emergency, Headless suite **227/227** plus **86** Python tooling tests; the session, PLAN-verb, crowd, emergency,
timeline, audio, scenario-content, props, NPC-budget, movement, and windowed focus/radial smokes timeline, audio, scenario-content, props, NPC-budget, movement, and windowed focus/radial smokes
pass. Anonymous downloads are enabled on the host. pass. Anonymous downloads are enabled on the host. The realism pass is adopted (per-phase skyline
panoramas, v2 surface tiles, retuned lighting); see `DONE.md` and `TODO.md` → Realism follow-ups.
The scheduled presentation pass is complete (see `DONE.md`). The next scheduled pass is the The scheduled presentation pass is complete (see `DONE.md`). The next scheduled pass is the
graphics-quality work in [`docs/plan-graphics-v0.6.md`](docs/plan-graphics-v0.6.md). graphics-quality work in [`docs/plan-graphics-v0.6.md`](docs/plan-graphics-v0.6.md).
@@ -14,13 +15,6 @@ graphics-quality work in [`docs/plan-graphics-v0.6.md`](docs/plan-graphics-v0.6.
## Visual alignment with rendered mockups ## Visual alignment with rendered mockups
The AI-rendered references in `docs/screens/designs/` (start menu, pause, settings,
shift review, radial menu, HUD, scene views) describe the intended look of every
screen. This pass reorganizes how generated images are stored, makes the game load
the proposed interface/scene art from disk, and aligns the selection screens, HUD,
and scene visuals with those mockups — verified with
`tools/screen_designs.py compare` + `judge`.
### Storage restructure ### Storage restructure
- [x] Decide the canonical runtime image root (`assets/images/` for UI/scene art): recorded in - [x] Decide the canonical runtime image root (`assets/images/` for UI/scene art): recorded in
@@ -64,8 +58,8 @@ and scene visuals with those mockups — verified with
### Verify against the mockups ### Verify against the mockups
- [x] Regenerate the staged captures and run `screen_designs.py compare` (11 good, 0 bad; verified in a clean HEAD worktree). - [x] Regenerate the staged captures and run `screen_designs.py compare` (11 good, 0 bad; verified in a clean HEAD worktree).
- [ ] Run `screen_designs.py judge` and clear every `diff` verdict (target `match`/`review`). - [x] Run `screen_designs.py judge` and clear every `diff` verdict (7 match, 4 review, 0 diff).
- [ ] Add the capture + compare step to CI or a repeatable smoke so drift is caught early. - [x] Add the capture + compare step as a repeatable smoke so drift is caught early (`tools/check_screenshots.sh`).
## Fix mouse movement bug ## Fix mouse movement bug
@@ -75,128 +69,22 @@ and scene visuals with those mockups — verified with
- [ ] Improve the visual appearance of in-game pop-ups (layout, button style, palette, icons, and text styling). - [ ] Improve the visual appearance of in-game pop-ups (layout, button style, palette, icons, and text styling).
## Realistic backgrounds, textures and scene references ## Realism follow-ups (v0.6.1 candidates)
Goal: replace the outdated flat skyline backdrop (`assets/textures/skyline.png`, black boxes - [ ] Fixed ground exposure for the entrance view: it is still ~20 % brighter than the concept.
with dot windows) and the flat-looking surface tiles with AI-generated, more realistic Either put the entrance in the building's shadow (sun yaw/elevation) or add a slight
backgrounds and textures. Add a larger set of example 3D scene images as art direction, and per-view exposure so the shaded sidewalk reads mid-grey.
measure the in-game result against that target with the screenshot tools. Plan: - [ ] Make the flashlight read in the dark: the `06-night-flashlight` concept shows a visible
[`PLAN.md` → Visual Quality Direction → Realism pass](PLAN.md#realism-pass). beam, but a Compatibility spot light only brightens the near wall. A cheap fake cone (an
additive mesh or a light-shaft quad) would sell it, and the night mockup expects it.
**Rule for every step: never overwrite an existing image.** New outputs get new paths. When a - [ ] NPC skin/clothing still reads as low-poly next to the concepts (judge `characters` ≈ 0–1).
path is taken, the tools pick the next free `-vN` suffix. Raw AI output stays on disk under The Blender library has the budgets for one more detail pass (fingers, sleeves, faces).
`assets/textures/ai/v2/raw/` (gitignored like the other raw AI folders). Processed game-ready - [ ] The `screen_designs.py judge` default model (`google/gemini-2.5-flash-lite`) currently
candidates go to `assets/textures/ai/game_ready/v2/`, a local pool where only the validated picks fails with a provider "API key not valid" 400; the tool retries once. Re-check before the
are tracked (ADR-0014). Scene references go to `docs/screens/concepts/`, and trial captures and next release and either pin a working model or document `--model`.
reports to `docs/screens/realism/<run>/`. - [ ] Add the two venue stages to the committed gallery (they are capture-only today) and give
the Joke Shop and studio their own geometry so their concept comparisons are geometric,
### Step 1 — Tooling (`tools/scene_concepts.py`) not just lighting.
- [x] Add a catalogue of prompts in three groups: `scene` (16:9 example 3D game frames),
`backdrop` (4:1 skyline panoramas with transparent sky), and `texture` (seamless PBR
surface tiles). Each entry has a name, prompt, role and target path.
- [x] `generate [--group] [--only] [--variants N]`: call the OpenRouter image API through
`generate_textures.generate_image` (key from the repo `.env`), save raw output, and never
overwrite (`next_free_path`). Print every file written.
- [x] `process`: turn raw backdrops into 2048×512 RGBA strips that tile horizontally (edge
crossfade, sky made fully transparent, hard alpha for the alpha-scissor material). Turn raw
textures into seamless 1024² tiles (`asset_pipeline.make_seamless`) plus a normal map
(`make_normal_maps.normal_map`). Skip outputs that already exist.
- [x] `audit`: run `asset_pipeline.audit_image` on the processed candidates (`backdrop` /
`tile` roles) and fail on any bad result.
- [x] `realism`: for each trial capture, report the Pillow detail metrics (edge density,
contrast, colour spread, saturation) next to the matching concept image. Optionally ask a
vision model (`google/gemini-2.5-flash-lite`) for 0–10 realism scores (lighting,
materials, texture detail, backdrop, overall) with concrete fixes. Write a new JSON +
Markdown report per run.
- [x] Unit tests (`tools/test_scene_concepts.py`): catalogue shape, the no-overwrite path
picker, backdrop/tile processing on synthetic images, metrics, and the judge payload/parse
(offline, no network).
### Step 2 — Example 3D scene images
- [x] Generate 3D-game-style example frames for every venue and time of day: Hollywood entrance
(morning, golden hour, rainy night), boulevard skyline view (day, dusk), Joke Shop alley
(evening, close-up of the door), studio stage door (night arrival), sidewalk material
close-up, entrance steps and podium, crowd-control barrier line, and a responder arrival
wide shot.
- [x] Review the set and record the visual targets each frame sets (sky, haze, material detail,
backdrop depth) in the report.
### Step 3 — Backgrounds (replace the outdated skyline)
- [x] Generate skyline panoramas for day, dusk and night (lit windows): Hollywood/Los Angeles
mid-rise blocks, palm silhouettes, the hills on the horizon, a transparent sky, and a
straight-on view, in several variants each.
- [x] Process them into `assets/textures/ai/game_ready/v2/skyline_{day,dusk,night}*.png`,
seamless horizontally for the 3× UV repeat around the backdrop ring. Strips are 2048×1024
(2:1, one UV repeat of the 400×60 m ring is about 133×60 m). 12 candidates, 11 audit
clean; `skyline_day-v3` misses the seam limit by 0.6. Variants 1–2 contain garbled
"Hollywood" lettering, so the prompt now forbids signs. Picks: `skyline_day-v4`,
`skyline_dusk-v3`, `skyline_night-v4`.
### Step 4 — Textures
- [x] Generate higher-detail seamless tiles: red brick facade, granite/limestone facade
(studio), sidewalk concrete pavers with joints, asphalt with aggregate, wet night asphalt,
stone steps, striped awning canvas, and stucco wall.
- [x] Process them into 1024² game-ready tiles plus normal maps in
`assets/textures/ai/game_ready/v2/` and audit them (16 tiles + 16 normals, all good). The
`awning_canvas` seam crossfade smears the stripes at the tile edge; it needs a
stripe-aligned crop.
### Step 5 — In-game trial captures (no game-code change)
- [x] Add `src/test/capture_realism.gd`. It boots the Hollywood venue, captures a fixed shot
list (entrance day, boulevard/skyline day, boulevard night), and can swap textures at
runtime from `--skyline=` / `--brick=` / `--concrete=` / `--asphalt=` user arguments. The
swap finds materials by their current texture, so baseline and candidate frames use the
same camera. Writes to `user://screenshots/realism/<tag>/`.
- [x] Capture the baseline (current assets) and the candidate set (v2 skyline + textures), and
copy them into `docs/screens/realism/<run>/{baseline,candidate}/`.
### Step 6 — Compare against the realistic target
- [x] Run `scene_concepts.py realism` on baseline and candidate. Record the metric deltas and the
vision scores, and note which candidate textures/backdrops win. Run
`docs/screens/realism/2026-11-01-v2/` (`report-v2.md`, `sheet.jpg`) gives these mean
judge scores: backdrop 2.6 → 3.6 (v2 day + night skyline) → **4.0** (v2 day + dusk
skyline for the Evening phase), texture detail 1.2 → 1.6–1.75, overall 1.6 → 2.0. Single
scores swing by ±1–2 between runs, so read them as trends. The `characters` score
moved from 3.0 to 1.0 with identical NPCs, which shows the noise.
- [x] Note the remaining realism gaps (sky/fog, lighting, building geometry, NPC fidelity) as
follow-up tasks below.
### Step 6b — Follow-ups from the 2026-11-01 realism report
- [ ] Fix sidewalk overexposure: the concrete reads near-white in every day shot, while the
concepts keep the ground mid-grey. Lower the albedo tint or the sun energy, or add tonemap
exposure in `Materials.configure_environment`.
- [ ] Make the Evening/Late phases darker and lit by practical lights (marquee, lamps,
windows), and add wet-asphalt roughness at night, as in the rain-night concept.
- [ ] Choose the skyline per phase (day → `skyline_day`, Evening → `skyline_dusk`, Late →
`skyline_night`) and lower the fog density on the backdrop so the night windows show.
- [ ] Raise the backdrop plane (60 m → ~90 m) or use 2 UV repeats so the skyline is not a
thin strip at the horizon.
- [ ] Add facade depth (plinth, cornice, window reveals) and replace the blob trees and shrubs
with denser foliage meshes (Blender library follow-up).
- [ ] Re-cut `awning_canvas` so its stripes align at the tile edge.
- [ ] Capture shots from the Joke Shop and the studio too (add them to `capture_realism.gd` /
`SHOTS`).
### Step 7 — Adopt the winners (after the in-flight normal-map/AO work is committed)
- [x] Track the validated picks in place (ADR-0014). `game_ready/v2/*` is a gitignored local
pool; `.gitignore` un-ignores the skyline picks and the brick/pavers/asphalt (+ wet) tiles
and their normals. Never replace the existing `game_ready/` files.
- [ ] Point `WorldBuilder.T_SKYLINE` at the chosen v2 skyline (separate day/night textures through
`set_night_mode`), and retune `albedo_color` for the realistic strip.
- [ ] Switch `materials.gd` / `world_builder.gd` surface preloads to the chosen v2 tiles + normal
maps, keeping the current UV scales. Update `test_materials.gd` / `test_world.gd`.
- [ ] Regenerate `docs/screenshots/` with `capture_screenshots.gd`, then run
`screen_designs.py compare` + `judge` and `scene_concepts.py realism`. There must be no
`diff` verdict regressions, and realism scores must not drop.
- [ ] Update `README.md` (tool usage + gallery), `MEMORY.md` and `docs/architecture.md`.
## Future ideas (optional, not scheduled) ## Future ideas (optional, not scheduled)
+7 -3
View File
@@ -1,11 +1,12 @@
extends SceneTree extends SceneTree
## Dev helper: render every generated prop side by side on a plain backdrop and ## Dev helper: render every generated prop side by side on a plain backdrop and
## save user://screenshots/10-prop-gallery.png. The image feeds ## save user://screenshots/10-prop-gallery.png by default. The image feeds
## `tools/screen_designs.py compare` so a prop regression is visible. ## `tools/screen_designs.py compare` so a prop regression is visible.
## ##
## Usage: godot --path . --script res://src/test/capture_prop_gallery.gd ## Usage: godot --path . --script res://src/test/capture_prop_gallery.gd
## godot --path . --script res://src/test/capture_prop_gallery.gd -- --out=res://docs/screenshots
const OUT_DIR := "user://screenshots" var out_dir := "user://screenshots"
const MODEL_DIR := "res://assets/models/generated/" const MODEL_DIR := "res://assets/models/generated/"
const LIBRARY_DIR := "res://assets/models/blender/props/" const LIBRARY_DIR := "res://assets/models/blender/props/"
const PROPS := ["bollard", "bench", "trash_bin", "planter_box", const PROPS := ["bollard", "bench", "trash_bin", "planter_box",
@@ -15,6 +16,9 @@ const SPACING := 1.9
var frames := 0 var frames := 0
func _initialize() -> void: func _initialize() -> void:
for arg in OS.get_cmdline_user_args():
if arg.begins_with("--out="):
out_dir = arg.trim_prefix("--out=")
root.size = Vector2i(1280, 720) root.size = Vector2i(1280, 720)
_build_stage() _build_stage()
@@ -102,7 +106,7 @@ func _process(_delta: float) -> bool:
if dir == null or not dir.dir_exists("screenshots"): if dir == null or not dir.dir_exists("screenshots"):
dir.make_dir_recursive("screenshots") dir.make_dir_recursive("screenshots")
var image := root.get_texture().get_image() var image := root.get_texture().get_image()
var err := image.save_png(OUT_DIR.path_join("10-prop-gallery.png")) var err := image.save_png(out_dir.path_join("10-prop-gallery.png"))
print("10-prop-gallery.png -> err=%d (%dx%d)" % [err, image.get_width(), image.get_height()]) print("10-prop-gallery.png -> err=%d (%dx%d)" % [err, image.get_width(), image.get_height()])
quit(0) quit(0)
return true return true
+10 -6
View File
@@ -1,10 +1,11 @@
extends SceneTree extends SceneTree
## Dev helper: run the game windowed and capture staged screenshots for the ## Dev helper: run the game windowed and capture staged screenshots for the
## README (day set + LATE-night set with street lamps and flashlight). ## README (day set + LATE-night set with street lamps and flashlight).
## Saves 1280x720 PNGs to user://screenshots/. ## Saves 1280x720 PNGs to user://screenshots/ by default.
## Usage: godot --path . --script res://src/test/capture_screenshots.gd ## Usage: godot --path . --script res://src/test/capture_screenshots.gd
## godot --path . --script res://src/test/capture_screenshots.gd -- --out=res://docs/screenshots
const OUT_DIR := "user://screenshots" var out_dir := "user://screenshots"
const MAX_FRAMES := 3600 # hard cap (~60s) const MAX_FRAMES := 3600 # hard cap (~60s)
var frames := 0 var frames := 0
@@ -16,6 +17,9 @@ var camera_staged := false
var wand_attempts := 0 var wand_attempts := 0
func _initialize() -> void: func _initialize() -> void:
for arg in OS.get_cmdline_user_args():
if arg.begins_with("--out="):
out_dir = arg.trim_prefix("--out=")
root.size = Vector2i(1280, 720) root.size = Vector2i(1280, 720)
var packed: PackedScene = load("res://scenes/main.tscn") var packed: PackedScene = load("res://scenes/main.tscn")
var inst: Node = packed.instantiate() var inst: Node = packed.instantiate()
@@ -231,9 +235,9 @@ func _face_target() -> void:
gm.guard.head.rotation.x = deg_to_rad(-4.0) gm.guard.head.rotation.x = deg_to_rad(-4.0)
func _shoot(file_name: String) -> void: func _shoot(file_name: String) -> void:
var dir := DirAccess.open("user://") var dir := DirAccess.open(out_dir)
if dir == null or not dir.dir_exists("screenshots"): if dir == null:
dir.make_dir_recursive("screenshots") DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(out_dir))
var img: Image = root.get_texture().get_image() var img: Image = root.get_texture().get_image()
var err := img.save_png(OUT_DIR.path_join(file_name)) var err := img.save_png(out_dir.path_join(file_name))
print("%s -> err=%d (%dx%d)" % [file_name, err, img.get_width(), img.get_height()]) print("%s -> err=%d (%dx%d)" % [file_name, err, img.get_width(), img.get_height()])
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Repeatable visual-regression smoke: render the staged captures into
# docs/screenshots and compare them against the runtime reference art.
#
# Runs under Xvfb automatically when no display is available. Requires Godot
# 4.3 on PATH (or GODOT=...), Python 3, Pillow, and xvfb-run for headless hosts.
set -euo pipefail
cd "$(dirname "$0")/.."
GODOT="${GODOT:-godot}"
RUN=()
if [[ -z "${DISPLAY:-}" ]]; then
if ! command -v xvfb-run >/dev/null 2>&1; then
echo "error: no DISPLAY and xvfb-run is not installed" >&2
exit 2
fi
RUN=(xvfb-run -a)
fi
"${RUN[@]}" "$GODOT" --path . --script res://src/test/capture_screenshots.gd -- --out=res://docs/screenshots
"${RUN[@]}" "$GODOT" --path . --script res://src/test/capture_prop_gallery.gd -- --out=res://docs/screenshots
python3 tools/screen_designs.py compare