Files
security-game/docs/blender-assets.md
T
zwitschi d8cfddbf89 Integrate the Blender character, gear and prop library
NpcAvatar
- select one of 8 library character variants by role/seed (with npc_base and the
  procedural rig as fallbacks), keep Rig/Head and the existing bone animation
- tint only skin/cloth/pants/accessory/hair; fixed detail materials keep their
  colour, npc_vest keeps its hi-vis vest, firefighter/guard use dark uniform tints
- attach caps, hoods, helmets, backpacks, SCBA tanks, lanyards and hand items at
  the documented head/back/chest/hand points (primitive fallbacks retained)

WorldBuilder
- prefer assets/models/blender/props/ for the six replacements and the new
  street dressing (street lamp/tree, palms, planters, A-frame, cone, barrier,
  stanchion, podium, handrail, wall lantern, double door, marquee, sedan)
- fit the same colliders/navigation obstacles and keep the placement validator;
  collect library emissives (bulbs, marquee face, wall lanterns, door LED, sedan
  lights) for set_night_mode()
- add a library triangle counter + 60k budget check (current ~27,986); toggle
  prop gallery to the library models; fix the marquee label placement

Tests/smoke
- character selection/gear attach/fixed-material tint tests; library prop load,
  emissive and triangle-budget tests; smoke_props routes a guest around a
  stanchion in open sidewalk (the old bench route is now crowded by trees)
- headless 222/222, Python 68/68, all smokes pass, compare 11 good / 0 bad

Docs: TODO Phase 3.5 moved to DONE, README/architecture/blender-assets/plan
describe the integrated runtime, MEMORY records the integration rules.
2026-11-01 14:57:02 +01:00

10 KiB
Raw Permalink Blame History

Blender asset library

Status: built, audited, and integrated (2026-11-01). NpcAvatar selects the character variants and attaches the gear; WorldBuilder loads the prop library with colliders and phase-driven emissives. See the Phase 3.5 integration in plan-graphics-v0.6.md.

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, hoodies and beanies; 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.

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 list
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.
  • 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 eight use the npc_base contract, so each one can replace npc_base.glb without code changes:

  • mesh node npc_base under an armature that imports as Skeleton3D
  • bones Head, HipL/R, KneeL/R, ShoulderL/R, ElbowL/R, HandL/R (plus Hips, Spine, Neck) at the same rest positions as npc_base. test_character_joints_line_up_with_npc_base checks this within 1 cm, so the animation proxies in _build_skinned_body() stay valid.
  • material slots skin, cloth, pants, accessory, hair carry the same neutral assets/textures/npc/<slot>_{albedo,normal}.png maps, so Materials.npc_slot() tints them the same way it tints npc_base.
  • budget ≤ 6000 triangles and ≤ 4000 vertices (glTF vertex count, after seam splits). The current range is 3,788–4,640 triangles and 3,200–3,960 vertices.

Construction: the torso and limbs are one continuous Skin-modifier mesh, subdivided twice, decimated to 36 % with X symmetry, and cut at the waist, cuffs, sleeves and neck so the material borders are clean edge loops. The head, face (nose, ears, eyes, brows, mouth), hair, hands, shoes and clothing layers are separate parts. Weights come from distance to the bone segments, limited by body region. Parts that should stay rigid (head, hands, shoes) get a single bone.

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.

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, so the origin of each GLB is its attach point:

Attach Godot parent / position Gear
head Rig/Head marker (0, 1.62, 0) cap, beanie, hood, sunglasses, firefighter_helmet
back Rig at (0, 1.10, 0), spine centre; items sit behind the body (Blender +Y = Godot −Z) backpack, shoulder_bag, scba_tank
chest Rig at (0, 1.30, 0), sternum lanyard
hand Hand proxy (±0.27, 0.78, 0.02); grip 5 cm below the origin phone, cup, clipboard, umbrella, camera, protest_sign, flashlight, wand, radio

The character previews in sheet_npc.jpg are built by importing the gear at exactly these points, which gives a quick visual check of the convention.

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) and have no textures. They are not yet AO-baked; that is still a Phase 4 task. Emissive materials are always on in the GLB. The integration should tie them to WorldBuilder.set_night_mode().

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 6000/4000, 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/test_blender_assets.py: catalogue and contract consistency with npc_pipeline.py, plus a clean audit 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.

Known limitations

  • The characters have no baked AO or 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.