Commit the finished generator output from the parallel Blender pass: 8 skinned NPC variants, 18 gear pieces, and 20 props under assets/models/blender/, editable sources (gdignored), manifest, review sheets, and the integration notes in docs/blender-assets.md. - audit: all 46 assets clean (characters 3,788-4,640 tris, under 6000/4000) - tests: Python manifest/audit contract and Godot import/bone/slot/ground checks - docs: README library section, architecture/plan references, TODO Phase 3.5 status, MEMORY integration rules - .gitignore: per-asset preview PNGs, extracted GLB textures, blend1 backups Nothing in the game loads the library yet; integration follows." && git log --oneline -1
10 KiB
Blender asset library
Status: built and audited (2026-11-01). Nothing in the game loads these assets yet.
They are staged for 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 skinnednpc_baseis 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 arewall_lantern(origin on the wall plate, at mount height) andmarquee(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_baseunder an armature that imports asSkeleton3D - bones
Head,HipL/R,KneeL/R,ShoulderL/R,ElbowL/R,HandL/R(plusHips,Spine,Neck) at the same rest positions asnpc_base.test_character_joints_line_up_with_npc_basechecks this within 1 cm, so the animation proxies in_build_skinned_body()stay valid. - material slots
skin,cloth,pants,accessory,haircarry the same neutralassets/textures/npc/<slot>_{albedo,normal}.pngmaps, soMaterials.npc_slot()tints them the same way it tintsnpc_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. _build_skinned_body() currently paints cloth
hi-vis yellow for the vest layer. On npc_vest the vest is already the fixed
hivis material and cloth is the undershirt, so skip that override for this
model. For npc_firefighter and npc_guard, tint cloth/pants dark (turnout /
uniform navy) and not with 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, thenpc_basemesh node and the height.tools/test_blender_assets.py: catalogue and contract consistency withnpc_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 matchnpc_base, the bones acceptNpcAvatar'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
SurfaceMaterialsrecipes 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 foroutfit_for(). The GLBs stay neutral.