# Blender asset library Architecture context: the asset contracts and fallback path are summarized in [`architecture.md`](architecture.md#content-and-asset-pipeline-architecture). 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. The 0.9.4 neutral baseline and its acceptance criteria are in [`npc-visual-review.md`](npc-visual-review.md); the committed 8 × 6 contact sheet is [`blender_previews/sheet_npc_baseline.jpg`](blender_previews/sheet_npc_baseline.jpg). Generator: [`tools/blender_assets.py`](../tools/blender_assets.py). Manifest (the source of truth for integration): [`assets/models/blender/manifest.json`](../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`](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//`; they are **not shipped assets** — only the assembled character GLBs, AO/maps, the manifest and the review sheets are committed. ```bash # 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 (currently rebuilds the procedural character) 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`](asset-tooling.md#optional-interactive-blender-mcp) for installation, connection and safety. ## Running it ```bash # 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/.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_.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/.glb` | 8 | | Gear (static) | `assets/models/blender/npc_gear/.glb` | 18 | | Props (static) | `assets/models/blender/props/.glb` | 20 | | Editable sources | `assets/models/blender_src/.blend` (`.gdignore`d) | 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](architecture.md#character-and-gear-asset-contract): `npc_base` mesh node, required 13 core bones (optional extras allowed), five tint slots, +Z visual front, 16,000/16,000 character budget and the same 256² slot-map/AO rules. Role selection and look notes: | 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; the exact contract and runtime fit offsets are in [`architecture.md#character-and-gear-asset-contract`](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/.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 (`_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 16000/16000, 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 `_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`](npc-visual-review.md).