# 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//after godot --path . --script res://src/test/capture_realism.gd -- --out=res://docs/screens/realism//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/ --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 ` 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 (day, Hollywood surface close-ups, Joke Shop surface close-ups, portrait, evening, late, Joke Shop day, 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 texture-adoption run compared 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. The character pass added the portrait shot and recorded the new faces, hands and sleeve cuffs. 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`.