Reorganize docs into category folders and drop the retired head-review page
Full build / Import + headless tests (push) Successful in 41s
Full build / Full desktop build + optional release (push) Skipped

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.
This commit is contained in:
zwitschi committed 2026-11-06 20:13:03 +01:00
1 parent 045ed2a58f
commit 2ba40f3578
57 files changed
+406 -414

No files matched your search

+304
View File
@@ -0,0 +1,304 @@
# 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).