Files
zwitschi 2ba40f3578
Full build / Import + headless tests (push) Successful in 41s
Full build / Full desktop build + optional release (push) Skipped
Reorganize docs into category folders and drop the retired head-review page
Group the top-level docs into docs/project, docs/design, docs/assets and
docs/process, each with its own README index. Delete the superseded
docs/screens/head-review page and sheets. Rewrite every markdown link,
absolute docs path and tool/test reference to the new locations, fix the
pre-existing broken anchors and remove the dead Joke Shop photo-board link.
check_docs OK, Python tooling 167/167.
2026-11-06 20:13:03 +01:00

305 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Blender asset library
Architecture context: the asset contracts and fallback path are summarized in
[`architecture.md`](../project/architecture.md#content-and-asset-pipeline-architecture).
Status: built, audited, and integrated (2026-11-01). The active character migration
replaces the procedural body with a baked CC0 Quaternius base
([ADR-0017](../adr/0017-quaternius-npc-base.md)); 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 [`architecture.md`](../project/architecture.md#character-and-gear-asset-contract);
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/<variant>/`; 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 (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`](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/<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 library variants share the
[character and gear asset contract](../project/architecture.md#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`](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.
**Hair part library.** Each base ships one hair shell and the build reshapes it in place
(`QUATERNIUS_HAIR_PARTS`): `short` is identity, `buzz` squashes toward the hairline, `long` stretches
from the crown. The role picks the style through `CHARACTERS[...]["hair"]`, and the T-pose cache is
keyed on it. `bald` is only allowed on the `casual` and `hoodie` bases, where the skin closes the
skull; the modular Worker's head stops at the hairline, so removing its hair would open the crown.
**Face detail.** None of the three Quaternius heads ships a mouth or eye whites, so the build adds
both: `_add_quaternius_mouth()` places a lip block on the measured skin surface, and
`_add_quaternius_eye_whites()` renames the eye quads to `sclera` and adds a shrunken `pupil` copy in
front of each. Both are fixed-colour (not tint slots), so they survive the role palettes.
**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`](../project/architecture.md#character-and-gear-asset-contract).
The shipped matrix is **fifteen pieces** since the hat removal (owner, 2026-11-06): `cap` and
`hood` are retired, and `firefighter_helmet` is the only head item.
| Attach | Pieces |
| --- | --- |
| head | `firefighter_helmet`, `sunglasses` |
| back | `backpack`, `shoulder_bag`, `scba_tank` |
| chest | `lanyard` |
| hand | `phone`, `cup`, `clipboard`, `umbrella`, `camera`, `protest_sign`, `flashlight`, `wand`, `radio` |
### Hand-item fit on the Quaternius rig
The hand attach point moved from Godot y 0.78 (procedural wrist) to **y 1.017** (Quaternius
wrist), so `fit` measures hand gear against the new wrist instead of the authored attach offset.
Run `blender_assets.py fit`; per item it reports the grip drop, the lowest point at rest, whether
that clears the hip line (0.95) and `size_vs_head` — the proportion that makes "reads oversized"
measurable on the 0.20 m head.
| Item | Drop (m) | Lowest at rest | Clears hip | Longest | × head |
| --- | --- | --- | --- | --- | --- |
| `camera` | 0.053 | 0.964 | yes | 0.138 | 0.69 |
| `phone` | 0.105 | 0.912 | no | 0.150 | 0.75 |
| `cup` | 0.110 | 0.907 | no | 0.142 | 0.71 |
| `radio` | 0.108 | 0.909 | no | 0.203 | 1.01 |
| `flashlight` | 0.077 | 0.940 | no | 0.243 | 1.14 |
| `wand` | 0.125 | 0.892 | no | 0.400 | 1.87 |
| `umbrella` | 0.201 | 0.816 | no | 0.900 | 4.48 |
| `clipboard` | 0.375 | 0.642 | no | 0.305 | 1.63 |
| `protest_sign` | 0.250 | 0.767 | no | 1.230 | 6.12 |
Falling below the hip line is normal for a carried object; the number that matters is whether the
item was *authored* for the new grip. Only the `clipboard` was: its grip sat in the middle of the
board, so it swung through the thigh. It now grips the top edge (0.22 × 0.30 m) like a carried
clipboard, and `HAND_ITEM_AUTHORED_SIZE` in the tool flags any future drift past 10 %.
## 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/assets/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 [`3d-model-review.md`](3d-model-review.md).