Files
security-game/docs/blender-assets.md
T
zwitschi 1ee32215b0
Full build / Import + headless tests (push) Successful in 38s
Full build / Full desktop build + optional release (push) Skipped
Detail the NPC avatars and finish the realism follow-ups
Blender library
- Rebuild the face: sclera, iris and upper lid per eye, brow ridge, cheekbones,
  nose bridge and tip, ears with lobes, split upper/lower lips.
- Give each hand four fingers plus a thumb and end every sleeve in a cuff ring.
- Raise CHAR_MAX_VERTS 4000 -> 5000 (16 avatars still cost ~6.9 ms of a 50 ms
  frame), decimate the body to 26 %; characters are now 3,974-5,026 tris and
  3,744-4,807 verts. Rebuild all 8 variants and re-bake their AO maps.

Capture tooling
- Add a `portrait` stage to capture_realism.gd: freeze one NPC, park the crowd
  out of frame, stand the subject on open sidewalk, turn it into the camera and
  put the guard on the sunlit side. New --portrait-type=N picks the role.
- Add the npc_portrait_day concept and map the shot to it, so avatar detail is
  scored against a portrait instead of a wide street frame.

Flashlight
- Drop the fake additive beam (Materials.light_beam + the cone mesh): on GL
  Compatibility a first-person camera inside the cone renders it as a filled
  disc with a bright far ring. Keep the tight spot (14 deg, 20 m, energy 4.2)
  and its headless test; the visible shaft is now planned behind a Forward+
  preset.

Docs and task tracking
- Move the finished realism follow-ups (ground exposure, flashlight, NPC detail,
  judge re-check) to DONE.md and expand TODO.md with step-by-step plans for NPC
  gear fit, venue geometry plus the committed gallery, and the Forward+ preset.
- Record the decisions, gotchas and portrait recipe in MEMORY.md; update the
  Blender contract, the concept README and the capture section of README.md.
- Refresh the gallery captures affected by the avatar rebuild.

Checks: headless 228/228, Python 86/86, blender_assets audit exit 0,
scene_concepts audit all GOOD, screen_designs compare 11 good / 0 bad,
smoke_npc_budget OK, boot clean (only the known headless mesh warning).
2026-11-01 17:46:25 +01:00

193 lines
11 KiB
Markdown
Raw 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
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. See the Phase 3.5 integration in
[`plan-graphics-v0.6.md`](plan-graphics-v0.6.md).
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, 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 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 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` (`.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, so
`Materials.npc_slot()` tints them the same way it tints `npc_base`.
- budget ≤ 6000 triangles and ≤ 5000 vertices (glTF vertex count, after seam
splits; `CHAR_MAX_VERTS` in `tools/blender_assets.py`, raised from 4000 for
the 2026-11-01 face/hand pass). The current range is 3,974–5,026 triangles
and 3,744–4,807 vertices.
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, 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/5000, 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/test_blender_assets.py`: catalogue and contract consistency with
`npc_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 match
`npc_base`, the bones accept `NpcAvatar`'s pose composition, and props are
grounded.
## 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.