Files
security-game/docs/asset-tooling.md
T
zwitschi 4bb2896819
Full build / Import + headless tests (push) Successful in 53s
Full build / Full desktop build + optional release (push) Successful in 4m27s
Verify the 0.9.6 integration checkpoint
2026-11-03 06:13:07 +01:00

362 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Asset Pipeline and Tooling
Everything the game renders is produced or gated by the scripts below. Architecture-level contracts,
data flow and failure fallbacks are summarized in
[`architecture.md`](architecture.md#content-and-asset-pipeline-architecture); this page is the
operational reference. Development-only outputs (AI concept sheets, Blender sources, raw AI fabrics)
are gitignored; the audited game assets are committed.
## Image Asset Tools
Install the Pillow dependency used by the existing texture generator and the asset tools:
```bash
python3 -m pip install -r requirements-tools.txt
```
Generate a role-sized image (generation requires `OPENROUTER_API_KEY`):
```bash
python3 tools/asset_pipeline.py generate --prompt "weathered red brick" --role tile --output assets/textures/brick_extra.png
```
Generated images are resized to the selected role profile. Non-square requests use
the validated `openai/gpt-5-image-mini` source ratio and crop to the final role dimensions.
Transparent glyphs can be recolored while preserving their alpha mask with
`--foreground '#ffffff'`.
Crop uses the image center by default; `--x` and `--y` select an explicit crop origin. Resize defaults to aspect-preserving `contain`; use `--fit cover` to crop or `--fit stretch` to distort to the exact dimensions.
```bash
python3 tools/asset_pipeline.py crop assets/textures/source.png assets/textures/crop.png --width 256 --height 128
python3 tools/asset_pipeline.py resize assets/textures/source.png assets/images/scenes/scene.png --width 1920 --height 1080 --fit cover
python3 tools/asset_pipeline.py seamless assets/textures/rough_tile.png assets/textures/tile.png
```
Audit accepts image files or directories. `--role auto` infers common roles from paths and filenames; explicit roles include `tile`, `texture`, `icon`, `card`, `banner`, `backdrop`, `background`, `overlay`, `app_icon`, and `screenshot`.
```bash
python3 tools/asset_pipeline.py audit assets/textures
python3 tools/asset_pipeline.py audit assets/images/scenes --role background --json
python3 -m unittest discover -s tools -p 'test_*.py'
```
The audit classifies measurable fit issues as `good` or `bad` based on role-specific dimensions, aspect ratio, alpha requirements, and tile-edge continuity. It lists perspective/projection as a visual review item because image semantics cannot be judged reliably by these deterministic checks.
## Prop models
Street furniture (bollards, benches, bins, planters, newspaper boxes, a hydrant)
is real 3D geometry generated by `tools/model_pipeline.py` and written to
`assets/models/generated/*.glb`. Godot imports each file as a `PackedScene`, and
`WorldBuilder` instantiates it with a procedural fallback when the model or its
import is missing, so the world still builds on a fresh checkout.
```bash
# Print the catalogue (parts, triangle count, whether a model/concept exists)
python3 tools/model_pipeline.py list
# Rebuild the models (deterministic, offline) and validate them
python3 tools/model_pipeline.py build
python3 tools/model_pipeline.py audit
# Optional: an image model supplies the colour palette per prop
# (requires OPENROUTER_API_KEY; the repo .env is read automatically)
python3 tools/model_pipeline.py concept
python3 tools/model_pipeline.py concept --only bench --force
```
Each prop is a small list of boxes and cylinders, so `build` needs no network and
the output is byte-identical between runs. `concept` asks an image model for a
studio-style reference; `dominant_colors` and `assign_roles` then map that image's
colours onto material roles (darkest → metal, warm saturated → wood, light grey →
stone, green → foliage). glTF stores colours in linear space, so the tool converts
the sRGB palettes before writing them. Re-run `build` and `godot --headless
--path . --import` after changing a prop. See
[ADR-0012](adr/0012-surface-materials-and-generated-props.md).
Surface materials share one definition in `src/game/materials.gd`: named
roughness/metallic profiles per surface, plus the environment tune-up (ACES
tonemapping, glow, fog, colour grading) that the phase lighting applies.
## NPC character model
NPCs are moving from the procedural capsule rig to a skinned, UV-mapped base
character authored in Blender ([ADR-0013](adr/0013-blender-authored-npc-characters.md)).
`tools/npc_pipeline.py` builds `assets/models/generated/npc_base.glb` plus the
per-slot material maps. `NpcAvatar` instantiates that skinned model, tints its
five material slots from the seed-driven outfit, and drives the bones from the
existing animation code; the procedural rig remains the offline fallback (see
[ADR-0013](adr/0013-blender-authored-npc-characters.md) and
[`blender-assets.md`](blender-assets.md)).
```bash
# Print the frozen contract (slots, bone names, triangle budget)
python3 tools/npc_pipeline.py list
# Rebuild the skinned base mesh + neutral maps in Blender and validate it
flatpak run org.blender.Blender --background --python "$PWD/tools/npc_pipeline.py" -- build
python3 tools/npc_pipeline.py audit
# Layer the AI clothing albedos (shirt/trousers) over the procedural weave, and
# derive per-fabric outfit colours for the avatar palette
python3 tools/npc_pipeline.py prepare
python3 tools/npc_pipeline.py palette
```
`prepare` resizes the AI fabric tiles (kept locally in `assets/textures/npc/ai/`,
which is gitignored) onto the 256² `cloth`/`pants` albedos; `build` bakes them into
the `.glb` and regenerates the procedural normal + packed ORM maps at the same
256² size. Runtime fabric materials repeat the weave 4× (`NPC_FABRIC_UV_SCALE`)
and skip the per-character baked AO, which otherwise magnified into large
blotches; skin, hair and fixed gear keep AO. `palette` writes
`assets/textures/npc/palette.json` using `model_pipeline.dominant_colors`.
## Blender asset library (loaded by the game)
`tools/blender_assets.py` models a larger library procedurally in Blender for the
v0.6.0 integration: **8 skinned NPC characters** (casual, jacket, coat, hi-vis vest,
dress, hoodie, firefighter, guard), each a drop-in for `npc_base.glb` (same bones,
joint positions and tint slots); **18 gear pieces** authored at head/hand/back/chest
attach points (caps, hood, helmet, backpack, SCBA tank, lanyard, phone, cup,
clipboard, umbrella, camera, sign, flashlight, wand, radio, …); and **20 props**
(octagonal bollard and bin, slatted bench, hydrant, news box, planters, street lamp,
street and palm trees, A-frame sign, wall lantern, cone, crowd barrier, stanchion,
handrail, podium, double door, marquee, sedan).
`NpcAvatar` selects a character by role/seed, tints the five contract slots, and
attaches gear at the documented points; `WorldBuilder` loads the library props
with colliders/avoidance and drives their emissive parts from the day/night
cycle. The single `npc_base.glb` and primitive props remain offline fallbacks.
Contract and integration notes: [`blender-assets.md`](blender-assets.md).
| Characters | Props |
| --- | --- |
| ![characters](../docs/blender_previews/sheet_npc.jpg) | ![props](../docs/blender_previews/sheet_props.jpg) |
```bash
flatpak run org.blender.Blender --background --python "$PWD/tools/blender_assets.py" -- build
flatpak run org.blender.Blender --background --python "$PWD/tools/blender_assets.py" -- render
python3 tools/blender_assets.py sheet # docs/blender_previews/sheet_*.jpg
python3 tools/blender_assets.py audit # no Blender needed
godot --headless --path . --import
```
## Confirmed game assets
Everything below is audited and shipped in the game; development-only outputs
(AI concept sheets, Blender sources, raw AI fabrics) are gitignored.
| | |
| --- | --- |
| ![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/v2/picks/asphalt.png) | ![awning](../assets/textures/ai/game_ready/v2/picks/awning_canvas.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 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`.
- **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`.
Audit them all:
```bash
python3 tools/asset_pipeline.py audit assets/textures/ai/game_ready
python3 tools/model_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
```
Judges default to `--model auto`: `tools/openrouter_judge.py` reads the live
OpenRouter catalogue, keeps image-input models that support `response_format`,
and ranks them by advertised token/image price. The run falls through the list
on provider/model errors and aborts on an invalid key. Use
`GIG_LINE_JUDGE_MODEL` or `--model <id>` for a pinned comparison.
`capture_npc_review.gd` is the pre-build NPC gate: it stages all eight
library character models at portrait, conversation and crowd distance on both
promoted maps in day and night lighting, with the normal crowd hidden. It uses
the review-only `NpcAvatar.character_override` to include the guard model, which
has no spawned NPC role:
```bash
godot --path . --script res://src/test/capture_npc_review.gd -- \
--out=res://tmp/npc-review
```
Contact sheets can be assembled from the files (the output is local QA and is
not committed). The runtime applies small attach-space offsets to the lanyard
and firefighter helmet so the badge clears the jacket and the brim does not sit
over the eyes; these are `NpcAvatar.GEAR_OFFSETS`, not changes to the gear
contract.
`tools/npc_proportions.py` checks the eight character GLBs against the shared
height/head/shoulder/hip/torso/leg/arm/foot guide; the current results are in
[`npc-proportion-audit.md`](npc-proportion-audit.md). It reads the exported GLBs
and does not require Blender.
`capture_npc_baseline.gd` is the 0.9.4 neutral baseline: eight characters ×
front/three-quarter/profile/back/full-body/crowd = 48 frames with a fixed
camera, studio lighting and no venue/HUD/animation. The raw PNGs stay in
`tmp/`; `tools/npc_baseline.py sheet` builds the committed contact sheet, and
`tools/npc_baseline.py compare --before DIR --after DIR` builds the review
comparison after a model change. See
[`npc-visual-review.md`](npc-visual-review.md).
```bash
godot --path . --script res://src/test/capture_npc_baseline.gd -- \
--out=res://tmp/npc-baseline
python3 tools/npc_baseline.py sheet --dir tmp/npc-baseline
```
`capture_realism.gd` runs nine stages in this order: day, Hollywood surface close-ups,
portrait, evening, late, Joke Shop day, Joke Shop surface close-ups, Joke Shop night,
studio; `--stages=day,late` limits it while tuning; `--flashlight` turns on the guard
lamp for night stages (used by the Desktop HQ A/B). The `portrait` shot freezes one
NPC on open sidewalk and frames the face, which is the only capture that can judge
avatar detail (`--portrait-type=N` picks a role). For the 0.9.5 texture audit, run
the surface stages directly; the HUD is hidden for them:
```bash
godot --path . --script res://src/test/capture_realism.gd \
-- --out=res://tmp/surfaces --stages=surfaces,surfaces_joke
```
The generated realism runs under
`docs/screens/realism/` are **local, gitignored QA artifacts**; commit only the
resulting decisions/docs, not the run folders. The 0.9.5 texture/UI audit checked actual screen sizes: all screen art is
1280×720, access cards are 256×128 and HUD icons are 64×64; no runtime texture
was oversized or unused. The 24-NPC smoke now reports 168 meshes / 131,616
triangles / 6.94 ms. Compressed payloads are 24.55 MB of surface picks, 22.67 MB
of screen art and 0.48 MB of NPC maps.
The 0.9.5 realism judge ran once per shot with `z-ai/glm-5.3-flash`
(`python3 tools/scene_concepts.py realism --run docs/screens/realism/2026-11-02-tex
--judge`). Its mean `after` scores were overall 1.8, lighting 2.3, materials 1.8,
texture detail 1.2, backdrop 2.0 and characters 1.2; the Pillow metric gap was
0.306. The judge still ranks the painted concept targets above the stylized build,
particularly lit night facades and dense backdrop detail. That is expected: the
pass validated close-range surfaces and promoted the texture repairs, not a
photoreal renderer. The 0.9.5 before/after close-up sheet is
[`blender_previews/sheet_texture_refinement_0.9.5.jpg`](blender_previews/sheet_texture_refinement_0.9.5.jpg);
the texture-adoption run itself was the earlier 0.8-style candidate comparison,
and its report remains local/gitignored.
For the 0.9.6 integration checkpoint the same judge was pinned to
`z-ai/glm-5.3-flash` and run once per shot on the 10-shot set: mean overall 2.1,
lighting 2.4, materials 2.1, texture 1.7, backdrop 2.3, characters 1.8, Pillow
gap 0.291. No `review` verdicts were produced by the screen judge, so there was
no manual layout verdict to convert into a fix; the remaining realism comments
are the accepted stylized gaps.
Build release artifacts from a clean checkout or detached worktree. A normal
working tree may contain ignored `tmp/` captures and old `build/` artifacts, and
Godot can pack enough of those into exports to inflate a 0.9.6 package from
~160 MB to 1.6–3.1 GB. The 0.9.6 clean worktree produced 161.5 MB Linux,
197.1 MB Windows, 148.0 MB macOS, 97.8 MB Web and 241.5 MB Desktop HQ.
The Forward+ A/B compared the Compatibility entrance with the optional Desktop HQ
preset. The gear-fit run checked the sunglasses, bag and helmet against measured
head/skirt bounds with `python3 tools/blender_assets.py fit`; it also fixed the
portrait yaw and hid the focus overlay so the close-up shows the avatar.
Regenerate a run locally with the capture and `realism --run ...` commands above;
the reports and sheets are intentionally not part of the release repository.
## Screen design references
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
`architecture.md`) into runtime reference art under `assets/images/ui/` (with a
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`
(an in-game panel overlaid on a softened scene), and the generator appends a
shared art-direction suffix so the frames read as one coherent game. Requests use
a 3:2 landscape source and crop to the 16:9 capture dimensions.
```bash
# Export the built-in scenarios to editor-authorable .tres resources
godot --headless --path . --script res://tools/export_venues.gd
# List the catalogue (name, kind, whether a reference design exists)
python3 tools/screen_designs.py list
# Deterministic wireframes (offline, no API key); existing designs are kept
# unless --force is passed, so an offline run never replaces an AI mockup
python3 tools/screen_designs.py generate --draft
python3 tools/screen_designs.py generate --draft --only wand --force
# Realistic 3D scenes and in-game screen designs via OpenRouter
# (requires OPENROUTER_API_KEY)
python3 tools/screen_designs.py generate
# UI-state gallery (menu, pause, settings, review, radio, wheel, crowd, responder)
godot --path . --script res://src/test/capture_ui_states.gd -- --out=res://docs/screenshots
# Prop gallery (studio line-up) for close-range prop checks
godot --path . --script res://src/test/capture_prop_gallery.gd
# Compare docs/screenshots/*.png against the designs
python3 tools/screen_designs.py compare
python3 tools/screen_designs.py compare --json
# Ask a vision model whether each capture realises its design's screen and layout
# (requires OPENROUTER_API_KEY; the repo .env is read automatically)
python3 tools/screen_designs.py judge
python3 tools/screen_designs.py judge --model openai/gpt-4o-mini --json
python3 tools/screen_designs.py judge --only 03- # one capture
python3 tools/screen_designs.py judge --dry-run # build requests, no network
```
The catalogue covers 21 screens: the thirteen scene captures plus the start menu,
pause overlay, settings, shift review, radial menu, emergency radio, crowd panic,
and responder arrival. All 21 have an AI reference in `assets/images/ui/`
(snapshot in `docs/screens/designs/`) and a committed capture, so every entry is
compared or judged against the running game.
`compare` flags gross drift: wrong aspect ratio, undersized frame, near-blank
frame, and large layout/palette/detail distance. It is a regression gate, not a
semantic judge — pixel and perceptual metrics cannot decide whether a frame's
composition is *right*.
`judge` covers that gap: it sends each capture together with its concept mockup to
a vision model and asks whether the capture realises the same screen and layout
intent. Art style, fidelity, fonts, and placeholder text are explicitly ignored.
Verdicts are classed as `match`, `review` (a mismatch below `--fail-confidence`,
default 0.6 — expected when a concept mockup is more stylised than the game), or
`diff` (a confident mismatch, which exits non-zero). Regenerate the designs after changing screen layout, then re-run
`compare` after regenerating the game screenshots with
`src/test/capture_screenshots.gd` and `src/test/capture_ui_states.gd`.
For a one-command drift check, run `tools/check_screenshots.sh`. It renders the
scene captures, UI-state captures and prop gallery into `docs/screenshots/` (using
`xvfb-run` when no display is available) and then runs `screen_designs.py compare`.