224 lines
13 KiB
Markdown
224 lines
13 KiB
Markdown
# 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.
|
||
|
||
## 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/<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` (`.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 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 at 256², so
|
||
`Materials.npc_slot()` tints them the same way it tints `npc_base`. Cloth and
|
||
pants repeat the weave 4× (`NPC_FABRIC_UV_SCALE`) and intentionally skip the
|
||
per-character baked AO; skin, hair and fixed gear keep it.
|
||
- budget ≤ **8,000 triangles and ≤ 8,000 glTF vertices** for the eight library
|
||
variants (vertex count after seam splits; `CHAR_MAX_TRIS`/`CHAR_MAX_VERTS` in
|
||
`tools/blender_assets.py`, raised from 6,000/5,000 on 2026-11-02). The frozen
|
||
`npc_base` fallback keeps its original 6,000/4,000 contract. The current range
|
||
is 4,482–5,474 triangles and 4,260–5,494 vertices, leaving room for the
|
||
remaining grip and clothing detail.
|
||
|
||
Construction: the torso and limbs are one continuous Skin-modifier mesh,
|
||
subdivided twice, decimated to 26 % with X symmetry, and cut at the waist, cuffs,
|
||
sleeves and neck so the material borders are clean edge loops. The head is a
|
||
separate part carrying the face detail: brow ridge, sclera/iris/upper-lid per eye,
|
||
cheekbones, a nose with a bridge and tip, ears with lobes, and upper and lower
|
||
lips. Hands have four fingers plus a thumb; sleeves end in a cuff ring.
|
||
Hair, 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, 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.
|
||
`NpcAvatar.GEAR_OFFSETS` applies two final-context nudges after attachment:
|
||
the lanyard moves 4 cm forward so its badge clears the jacket, and the
|
||
firefighter helmet moves 3 cm up so its brim sits above the eyes. The
|
||
`fit` report remains an asset-space measurement of the untouched GLBs.
|
||
|
||
## 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 (library characters 8000/8000, base fallback 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/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`](npc-visual-review.md).
|