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:
1 parent
b39fee6176
commit
d96f53c285
6 files changed
+159
-171
No files matched your search
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -287,11 +287,11 @@ Everything below is audited and shipped in the game; development-only outputs
|
|||||||
|
|
||||||
| | |
|
| | |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|  |  |
|
|  |  |
|
||||||
|  |  |
|
|  |  |
|
||||||
|  |  |
|
|  |  |
|
||||||
|
|
||||||
- **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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
### 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`)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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()])
|
||||||
Executable
+22
@@ -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
|
||||||
Reference in new issue
Block a user