Files
security-game/docs/blender-assets.md
T
zwitschi 04d211b90b
Full build / Import + headless tests (push) Successful in 40s
Full build / Full desktop build + optional release (push) Skipped
Release 0.9.9: full Quaternius roster, docs, changelog, version, slimmer export filters and checksums
2026-11-06 13:59:23 +01:00

15 KiB
Raw Blame History

Blender asset library

Architecture context: the asset contracts and fallback path are summarized in architecture.md.

Status: built, audited, and integrated (2026-11-01). The active character migration replaces the procedural body with a baked CC0 Quaternius base (ADR-0017); the current library remains the shipped fallback until the Quaternius 12-variant baseline passes review. NpcAvatar selects the character variants and attaches the gear; WorldBuilder loads the prop library with colliders and phase-driven emissives. The 0.9.4 neutral baseline and its acceptance criteria are in npc-visual-review.md; the committed 8 × 6 contact sheet is blender_previews/sheet_npc_baseline.jpg.

Generator: tools/blender_assets.py. Manifest (the source of truth for integration): assets/models/blender/manifest.json.

Art direction

The references were the AI mockups in docs/screens/designs/ and the current captures in docs/screenshots/.

  • People (targeting_npc, joke_shop_queue, studio_arrival, crowd_panic, responder_arrival): ordinary adults in sweaters, jackets, long coats and hoodies; a security guard in a dark uniform with a cap and belt; a firefighter in a dark turnout coat with yellow reflective bands, a helmet and an SCBA tank. The current skinned npc_base is a set of boxes and cylinders with a flat face, so silhouette was the main gap.
  • Props (prop_gallery, venue_overview, late_night_avenue, joke_shop_queue): the gallery mockup has an octagonal tapered bollard, a slatted bench with thin metal legs, an octagonal green litter bin with a slot and recessed panels, a stone planter with a lumpy shrub, a blue news box with a window and a slanted top, and a hydrant with caps and nozzles. The venue shots add ornate street lamps, palm trees, round stone pots with topiary, a chalkboard A-frame sign, a wall lantern, a bulb-lit marquee, glazed double doors, stair handrails and a dark sedan. Today's in-game props are primitive boxes, and the trees are cones.

Frozen fallback boundary

assets/models/npc/npc_base.blend is the tracked, frozen plain-primitive fallback. Prototype head/face/hand detail from the local part pipeline must not be saved into it; a separate contract decision (ADR or owner sign-off) is required before any prototype detail is absorbed. Working scenes live under tmp/npc-parts/ (gitignored) and are exported as local GLBs for review, never directly over the tracked .blend. The same rule already appears in asset-tooling.md#recommended-part-workflow.

Local part pipeline

The overhaul uses tools/npc_parts.py to build, extract, assemble, inspect and sheet the reusable head/face and hand parts before they are rolled into the 12 characters. It reuses the blender_assets.py builders (no second face/hand implementation) and stays local-only/headless. Part sources and preview GLBs live under tmp/npc-parts/<variant>/; they are not shipped assets — only the assembled character GLBs, AO/maps, the manifest and the review sheets are committed.

# Build one part fresh from the current builders (head|face|hand_l|hand_r)
flatpak run org.blender.Blender --background \
  --python "$PWD/tools/npc_parts.py" -- build --part face --variant npc_casual \
  --out "$PWD/tmp/npc-parts/npc_casual/face.glb"
# Pull the current shipped head/hands out of the built character GLB (comparison)
flatpak run org.blender.Blender --background \
  --python "$PWD/tools/npc_parts.py" -- extract --part hand_l --variant npc_casual \
  --out "$PWD/tmp/npc-parts/npc_casual/hand_l.glb"
# Assemble a variant (current fallback path; Quaternius migration in progress)
flatpak run org.blender.Blender --background \
  --python "$PWD/tools/npc_parts.py" -- assemble --variant npc_casual \
  --out "$PWD/tmp/npc-parts/npc_casual/assembled.glb"
# Inspect a part or assembled GLB (Blender import view)
flatpak run org.blender.Blender --background \
  --python "$PWD/tools/npc_parts.py" -- inspect --file "$PWD/tmp/npc-parts/npc_casual/face.glb"
# Contact sheet / before-after comparison (outside Blender; stdlib + Pillow)
python3 tools/npc_parts.py sheet --dir tmp/npc-parts/npc_casual \
  --out docs/blender_previews/sheet_parts.jpg
python3 tools/npc_parts.py compare --before DIR --after DIR --out sheet.jpg

build --part head produces the bare head shell; build --part face is the shell plus nose/eyes/mouth/ears; build --part hand_l|hand_r are the left/right hand geometry. extract pulls the current shipped geometry out of the built character GLB by vertex group (Head for head/face, HandL/HandR for the hands), so it shows what is actually shipped rather than what the builders would emit fresh. The assembled GLB must still pass blender_assets.py inspect/audit/fit before any part is accepted.

Optional MCP

Blender MCP can be used for interactive head/face/hand part exploration, but it is not required. The headless commands below remain the build and audit path; see asset-tooling.md#optional-interactive-blender-mcp for installation, connection and safety.

Running it

# Build every asset (or --only a,b,c). Writes GLBs, .blend sources, manifest.
flatpak run org.blender.Blender --background \
  --python "$PWD/tools/blender_assets.py" -- build
# Cycles previews (CPU, ~3 s each) -> docs/blender_previews/<name>.png (gitignored)
flatpak run org.blender.Blender --background \
  --python "$PWD/tools/blender_assets.py" -- render
python3 tools/blender_assets.py sheet   # -> docs/blender_previews/sheet_<category>.jpg
python3 tools/blender_assets.py audit   # stdlib only; exit 1 on any issue
python3 tools/blender_assets.py fit     # head/clothing fit + bag-body clearance, per character
python3 tools/blender_assets.py texels  # 256² slot-map density (authored + 4× fabric), per character
python3 tools/blender_assets.py list
# import a GLB, apply node transforms, report geometry/bones/UVs/skinning + contract failures
flatpak run org.blender.Blender --background \
  --python "$PWD/tools/blender_assets.py" -- inspect --file "$PWD/tmp/downloads/x.glb"
godot --headless --path . --import      # after any rebuild

The flatpak cannot read /tmp and its cwd is /app/blender, so the script derives every path from its own location. A full build takes about 5 s; a full render about 2.5 min.

Layout

Kind Path Count
Characters (skinned) assets/models/blender/npc/<name>.glb 8
Gear (static) assets/models/blender/npc_gear/<name>.glb 18
Props (static) assets/models/blender/props/<name>.glb 20
Editable sources assets/models/blender_src/<name>.blend (.gdignored) 46
Review sheets docs/blender_previews/sheet_{npc,npc_gear,props}.jpg 3

Conventions

  • Blender is Z-up. The glTF exporter converts to Godot Y-up.
  • Front = Blender −Y = Godot +Z, the face direction that NpcAvatar.yaw_towards() assumes.
  • Foliage (street_tree, planter_box, round_planter) is a cluster of many small flat-shaded clumps plus outer clumps that break the silhouette, not one smooth blob. That is how the realism pass made the street greenery read as leaves; the three assets carry fresh AO maps (street_tree is 2,592 triangles).
  • Grounded assets have their origin on the floor (min y ≈ 0). The two exceptions are wall_lantern (origin on the wall plate, at mount height) and marquee (origin at the wall where the canopy meets it, at canopy height).
  • Units are metres, at the same scale as the current world (1.82 m characters).

Characters

All library variants share the character and gear asset contract: npc_base mesh node, five tint slots, +Z visual front, 24,000/24,000 character budget and the same 256² slot-map/AO rules. The shipped roster is on the 62-joint Quaternius skeleton (ADR-0018); the procedural fallback built by build --source procedural keeps the 13 core bones. Both are accepted by audit, inspect, the runtime bone map and the tests.

The three Quaternius sources (casual, hoodie, worker) live in assets/models/blender_src/quaternius/ as build inputs. build (default --source quaternius) does a two-pass bake per character: a cached T-pose round trip that joins without welding, maps the upstream materials onto the five slots and unwraps the degenerate source UVs, then an arm-down rest bake exported with export_apply=False. The per-character slot map, triangle counts and measurements are in npc-model-sourcing.md#shipped-roster-099-owner-chose-the-full-swap.

Role selection and look notes (tint + gear now carry the role, not the mesh):

Character layer_for() / look Suggested roles Preview gear
npc_casual none: sweater/tee, short sleeves tourist, confused visitor, intoxicated, guest backpack, camera
npc_jacket jacket: collar + placket authorized staff, protester, guest lanyard, clipboard
npc_coat coat: knee-length coat, long hair influencer, trespasser, violent threat sunglasses, shoulder bag, phone
npc_vest vest: hi-vis vest, reflective bands delivery cap, clipboard
npc_dress dress: slim build, bun, tights performer shoulder bag
npc_hoodie hood accessory: hood up, beard, pocket trespasser, mental crisis, violent threat cup
npc_firefighter responder: broad build, banded turnout coat, boots responder helmet, SCBA tank, flashlight
npc_guard uniform: belt, buckle, badge, shoulder patches, radio player guard / venue security cap, wand

Fixed-colour materials. Detail materials outside the five slots (hivis, reflective, leather, steel, patch, plastic_black, rubber) carry a 4×4 colour texture. The current _apply_outfit_materials() sets albedo_color = WHITE on any slot it does not recognise, and these keep their colour through that. When integrating, tint only the five contract slots.

Head gear fit. Head gear is authored for the shipped Quaternius head: HEAD_GEAR_SCALE = 0.64 (0.201 m base head against the 0.315 m procedural one) and HEAD_GEAR_LIFT = 0.07 (its centre sits higher). The procedural fallback therefore wears slightly undersized caps — cosmetic and fallback-only.

Runtime tint notes (implemented). Only the five contract slots are tinted; fixed detail materials keep their authored colour. On npc_vest the vest is the fixed hivis material and cloth is the undershirt, so the yellow override is skipped. npc_firefighter/npc_guard use dark turnout/uniform tints instead of the role's TYPE_COLORS.

Gear attach points

Gear is authored in attach-point space; the exact contract and runtime fit offsets are in architecture.md#character-and-gear-asset-contract.

Props

Prop Replaces / role Notes
bollard, bench, trash_bin, planter_box, newspaper_box, fire_hydrant assets/models/generated/<name>.glb same footprint and height as the generated set, so colliders and PROP_PLACEMENTS still fit
street_lamp the box lamps in _build_street_furniture() 5 m; the arm points to −X with the bulb at (−0.85, 4.8, 0). Rotate 180° for the other side of the street. Emissive bulb
street_tree the cone trees 3.6 m, with a tree grate
traffic_cone the cone prop accessory 0.69 m
palm_tree new (boulevard) 7 m, 14 fronds
round_planter new (mockup pots) stone pot with topiary
a_frame_sign new (comedy club queue) chalkboard
wall_lantern new (club door) wall-mounted; emissive bulb
crowd_barrier, stanchion new (queue / crowd control) 2.2 m bike-rack barricade; chrome post with velvet rope
handrail new (steps) 2 m run that climbs 0.8 m along +X
security_podium new (door check-in) tablet screen is mildly emissive
double_door the procedural entrance door 2.0 × 2.7 m; emissive lock_led on the card reader
marquee the procedural marquee 6 m canopy and sign box; emissive bulb + sign_face. Overlay the text with the existing Label3D
sedan new (studio arrival) 4.6 m; emissive headlight, red tail lamps

Props use plain PBR factors (base colour, roughness, metallic, emission) plus the committed 256² baked AO maps (<name>_ao.png) wired by WorldBuilder; emissive materials are tied to WorldBuilder.set_night_mode(). Characters carry baked AO and the shared neutral material maps described above.

Checks

  • python3 tools/blender_assets.py audit: container, POSITION/NORMAL/ TEXCOORD_0, UVs in [0, 1], no zero-area triangles (Godot warns about them during LOD generation), budgets (characters 24000/24000, gear 1500, props 4000 triangles), grounding, and an editable .blend. For characters it also checks skinning, bones, slots, the npc_base mesh node and the height.
  • tools/tests/test_blender_assets.py: catalogue and contract consistency with npc_pipeline.py, plus a clean audit, a texel-density gate (all five slots, 40–400 px/m authored, 250 px/m fabric floor) and full manifest coverage when the library is built.
  • src/test/tests/test_blender_assets.gd: every asset imports as a scene with geometry, characters keep their bones and slots, joints match npc_base, the bones accept NpcAvatar's pose composition, and props are grounded.
  • tools/test_model_review.py: every CHARACTERS/GEAR/PROPS entry has a scorecard row and verdict in docs/3d-model-review.md (coverage gate).
  • src/test/tests/test_world.gd: every library prop that blocks walking has a StaticBody3D + NavigationObstacle3D, checked against the catalogue (collider/proxy gate).

Known limitations

  • Characters carry a baked <name>_ao.png (red channel, wired by _apply_outfit_materials()), but no ORM texture of their own. They reuse the tiling slot maps, so the runtime SurfaceMaterials recipes still supply roughness.
  • There is a single body topology per build (average, slim, broad) and no shape keys. Height variety still comes from outfit_for()["height_scale"].
  • Skirts and coats follow the hip and knee bones, so a full stride can clip the legs through the hem.
  • Preview colours (PREVIEW_TINTS) only stand in for outfit_for(). The GLBs stay neutral.
  • The 0.9.4 baseline found three P1 portrait issues. The tourist's photo prop now only appears in photo/film poses and the backpack straps were refit as flattened chest bands. The face carries the 0.9.4 sculpt (socketed sclera, chin/cheekbone, nose, lips) but its seg=18/rings=12 head still reads somewhat faceted at portrait range; a subdivision/deeper-sculpt prototype is gated on the owner scorecard review. The full ranked list and measurements are in npc-visual-review.md.