14 KiB
Asset Pipeline and Tooling
Everything the game renders is produced or gated by the scripts below. 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:
python3 -m pip install -r requirements-tools.txt
Generate a role-sized image (generation requires OPENROUTER_API_KEY):
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.
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.
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.
# 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.
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).
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
the graphics plan).
# 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 cloth/pants albedos; build bakes them into the
.glb and regenerates the procedural normal + packed ORM maps. 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.
| Characters | Props |
|---|---|
![]() |
![]() |
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.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
- Surface textures: the v2 picks above (brick, sidewalk pavers, dry and wet asphalt, awning canvas) plus their normal maps, loaded by
src/game/materials.gdandWorldBuilder; the three skyline panoramas are switched per shift phase. The first-generation tiles andassets/textures/skyline.pngare no longer loaded. - UI art:
assets/textures/ai/game_ready/icons/(flashlight, door lock, wand, REC) andassets/textures/ai/game_ready/cards/(ID, wristband, ticket, credential, guest list), loaded bysrc/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}.pngandpalette.json.
Audit them all:
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/; 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.
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 six stages (day, portrait, evening, late, Joke Shop, studio);
--stages=day,late limits it while tuning. 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). The texture-adoption run,
docs/screens/realism/2026-11-01-adopt/,
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. The character pass
(2026-11-01-npc/) adds the portrait shot and
records the new faces, hands and sleeve cuffs.
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.
# 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
# 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 eleven README captures plus the start menu,
pause overlay, settings, shift review, radial menu, emergency radio, wand
screening, Joke Shop queue, TV Studio arrival, crowd panic, responder arrival,
body-camera timeline, and the escalation HUD. All 21 have an AI reference in
assets/images/ui/ (snapshot in docs/screens/designs/); the eleven screens with
staged captures are also 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.
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.








