Files
security-game/docs/asset-tooling.md
T
zwitschi 5b306b1c52
Full build / Import + headless tests (push) Successful in 40s
Full build / Full desktop build + optional release (push) Skipped
Add the Commons reference-board research tool
2026-11-03 14:51:31 +01:00

27 KiB
Raw Blame History

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; 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:

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 ADR-0013 and blender-assets.md).

# 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.

Sourcing and expansion research (no external model is shipped yet): npc-model-sourcing.md compares Quaternius, Kenney, Khronos and MakeHuman sources against the character contract and records the 2026-11-03 direction (procedural generator first, Quaternius reference, MakeHuman deferred).

Characters Props
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
python3 tools/blender_assets.py fit     # gear-vs-body fit report
python3 tools/blender_assets.py texels  # per-slot texel density (px/m) of the characters
godot --headless --path . --import

Intake inspection (inspect)

audit reads accessor-local bounds and ignores node transforms, so it is the right release gate but a poor first look at a downloaded or newly authored GLB. inspect imports the file into Blender, clears parents and applies object transforms, then reports what the scene actually contains:

# one asset, a batch, or an external download; repeat --file as needed
flatpak run org.blender.Blender --background \
  --python "$PWD/tools/blender_assets.py" -- inspect \
  --file "$PWD/assets/models/blender/npc/npc_casual.glb" \
  --file "$PWD/tmp/downloads/SomeCharacter.glb"
# machine-readable, for intake scripts (write to a file: Blender prints a banner first)
... -- inspect --file "$PWD/tmp/x.glb" --json --out "$PWD/tmp/x.json"
# force a contract family instead of inferring it from the path
... -- inspect --file "$PWD/tmp/x.glb" --category npc

It prints triangles (with the category budget), raw and seam-split vertex counts, Godot-space bounds with height/feet for characters, whether transforms had to be applied, mesh nodes and armatures, bone names, materials, UV layers, skinning and animation count. Hard contract failures (issues) fail the command; advisory findings (warnings) do not. The category is inferred from the path: blender/npc → npc, blender/npc_gear → npc_gear, blender/props and generated → props, anything else → external (measured but with no project budget or rig contract).

Caveats:

  • The glTF importer adds a bone-display mesh (Icosphere, used as pose_bone.custom_shape). inspect excludes custom-shape helpers so they never pollute bounds or the unskinned list.
  • Bounds and bone rests are converted from Blender Z-up to Godot Y-up (x, z, -y), matching the contract bands and the audit's coordinate space.
  • Numbers are Blender-side measurements. Triangles match audit on project assets; the seam-split vertex count can differ by ±1 because the importer rounds normals. audit stays the single final gate: it is stdlib-only, runs in CI, and matches what the engine imports.
  • inspect never writes or adapts the file; scale/grounding/weld/retarget work is the separate (not yet built) adapt step.
  • --json also captures the importer's INFO log in stats.import_log instead of printing it. Blender still prints its own version banner before the script runs, so intake scripts should use --json --out <file> rather than parsing stdout.

Reference-board research (refboard)

tools/refboard.py searches, fetches and documents Wikimedia Commons photos of a real location for the screens/refs/ boards. It is stdlib-only and never commits anything itself: downloads land in tmp/, and moving a file into a board folder is a deliberate manual step that goes through the sidecar licence gate.

# search: keyword, geosearch (the reliable "photos of this exact place"), or category
python3 tools/refboard.py search --source geo "34.10118|-118.34009" --limit 60 --months 12
python3 tools/refboard.py search "El Capitan Entertainment Centre" --source search
python3 tools/refboard.py search "Hollywood Masonic Temple" --source category --json
# fetch the usable rows into tmp/ (review-only rows need --include-review)
python3 tools/refboard.py search ... --json > /tmp/manifest.json
python3 tools/refboard.py fetch --from /tmp/manifest.json --dir tmp/research/<block> --max 12
# emit SIDECAR + README table rows from the fetched manifest
python3 tools/refboard.py rows --from tmp/research/<block>/meta.json

Licence gate: Public domain, CC0 and CC BY are committable with attribution; CC BY-SA rows are flagged review and stay research-only until the share-alike decision is written into the sidecar (--allow-ccbysa marks them committable after that review); anything else is skipped. --months 12 flags recent captures so a "current reality" photo can be distinguished from the older geometry evidence. Network gotchas baked in: a descriptive User-Agent, batched titles= metadata queries and ~3 s download spacing to avoid Commons HTTP 429s.

Texel density (texels)

texel_density() and the texels command compute resolution * sqrt(UV area / world area) per material slot straight from the GLB, so the number does not depend on island packing. The NPC slot maps are 256², and cloth/pants are reported twice: authored density and effective density with the runtime 4× repeat from Materials.NPC_FABRIC_UV_SCALE. Current values and interpretation are in 3d-model-review.md.

python3 tools/blender_assets.py texels            # all eight characters
python3 tools/blender_assets.py texels --json     # machine-readable
python3 tools/blender_assets.py texels --only npc_coat

Local map development (planned)

The Local map development track in TODO.md is not implemented yet; there are no map-generation commands to run. The planned pipeline will evaluate:

  • offline OpenStreetMap extracts for footprints and licence-compatible 3D building massing (OSM Buildings, Overture, CityGML/LOD1–2);
  • downloaded reference imagery from Wikimedia Commons, own/team photography or other clearly licensed sources, with author/licence/attribution and privacy redaction recorded per set;
  • AI-assisted composition boards for palette, facade rhythm and material study, with model/prompt/output provenance and human acceptance;
  • Gaussian splatting as a development-only capture/reference/texture/proxy tool, version-pinned offline and outside CI, until an ADR proves runtime safety.

No online map/photo/splat service is a build or play dependency. Committed map assets will need provenance sidecars, ODbL/photo-licence review and the same WorldBuilder venue-budget/navigation/collider gates as the current stylized geometry. The detailed task plan and exit criteria live in TODO.md.

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 sidewalk pavers
asphalt awning
skyline day skyline night
  • 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:

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

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:

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. It reads the exported GLBs and does not require Blender.

capture_npc_pose.gd is the pose/deformation harness for the 3D re-evaluation: idle, walk-contact, walk-pass and seated poses of all eight variants, full-body plus shoulder close-ups. It is currently blocked: the applied pose is visible in the rig and bone values but not in the captured frames, so do not use its sheets as pose evidence. The reproduction and what was ruled out are in 3d-model-review.md §"Pose evidence is blocked".

The baseline sheet tool accepts a custom view list, which is also how pose sheets are tiled:

# baseline set (six views, eight variants)
python3 tools/npc_baseline.py sheet --dir tmp/npc-baseline
# pose set produced by capture_npc_pose.gd
python3 tools/npc_baseline.py sheet --dir tmp/npc-pose \
  --views idle,walk_contact,walk_pass,sit --out tmp/npc-pose/sheet_pose.jpg

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.

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:

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; 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.

# 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.