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.
523 lines
33 KiB
Markdown
523 lines
33 KiB
Markdown
# 3D model review (2026-11-03)
|
||
|
||
Status: **baseline measured; scorecard open.** This is the working document for the unversioned
|
||
`3D model re-evaluation` track in [`TODO.md`](../../TODO.md). It records what was measured, what the
|
||
Blender skill guidance says the targets are, the ranked candidate gaps, and the decisions that
|
||
still need the project owner. Nothing here re-opens the accepted 0.9.4 limits by itself.
|
||
|
||
Related documents: [`npc-model-sourcing.md`](npc-model-sourcing.md) (sources, shipped roster and
|
||
build steps), [`blender-assets.md`](blender-assets.md) (asset contract),
|
||
[`asset-tooling.md`](asset-tooling.md) (commands). The 0.9.4 procedural baseline this page was
|
||
measured against no longer ships; its before/after sheets stay in `blender_previews/`.
|
||
|
||
## Method and skill routing
|
||
|
||
The review is screenshot- and measurement-backed, following `qa-review` (screenshot set, defect
|
||
severity, explicit verdict) rather than an opinion pass:
|
||
|
||
| Family | Skills loaded | Criteria taken from them |
|
||
| --- | --- | --- |
|
||
| Routing and pipeline | `blender-director` | Plan → blockout → detail → validate → export; MCP steps translated to headless flatpak runs |
|
||
| Characters | `character-artist`, `retopology`, `rigging` | Eye/mouth/jaw/shoulder loops, quad-dominant deformation surfaces, 3-loop joints, no triangles on primary deformers |
|
||
| UVs and textures | `uv-workflow`, `texture-workflow`, `materials` | Texel-density tiers (hero 512–1024, standard 256–512, background 128–256 px/m), consistent density, no overlapping UVs |
|
||
| Budgets and structure | `asset-optimization`, `lod-pipeline` | Triangle/vertex budgets, naming, LOD tiers |
|
||
| Props, gear, foliage | `prop-artist`, `hard-surface`, `vegetation-artist` | Silhouette read, bevel/chamfer language, collision proxies, carded foliage |
|
||
| Look and verdict | `lookdev`, `qa-review` | Day/night beauty captures, ship / ship-with-notes / no-ship verdict |
|
||
|
||
Project contract and gates stay authoritative; see `AGENTS.md` §Local Blender skills. Blender MCP
|
||
is optional interactive tooling (see [`asset-tooling.md#optional-interactive-blender-mcp`](asset-tooling.md#optional-interactive-blender-mcp)); this review's evidence
|
||
came from the headless `tools/blender_assets.py` / `tools/npc_pipeline.py` pipelines plus the
|
||
Godot capture harnesses, so it remains reproducible without MCP.
|
||
|
||
## What was captured and measured (2026-11-03)
|
||
|
||
| Evidence | Command | Result |
|
||
| --- | --- | --- |
|
||
| Contract audit | `python3 tools/blender_assets.py audit --json` | 46/46 assets clean; library 59,012 tris |
|
||
| Imported-GLB inspection | `blender_assets.py inspect` (all 46) | All clean; transforms applied, helpers ignored |
|
||
| Gear fit | `python3 tools/blender_assets.py fit --json` | Head width 0.315 m on all eight; clothing radius 0.190–0.260 m |
|
||
| Proportions | `python3 tools/npc_proportions.py report --json` | All eight inside the stylized guide |
|
||
| Texel density | `python3 tools/blender_assets.py texels --json` | New measurement, table below |
|
||
| Neutral baseline | `capture_npc_baseline.gd` → `tmp/model-baseline` (48 frames) + `npc_baseline.py sheet` | 48 frames, none blank; sheet in `tmp/` only |
|
||
| Final context | `capture_npc_review.gd` → `tmp/model-review` (96 frames) | 96 frames, none blank; raw frames stay local |
|
||
| Performance | `smoke_npc_budget.gd` | 24 avatars, 168 meshes, 152,464 tris, 6.94 ms of 50 ms |
|
||
|
||
Raw captures and JSON stay in the gitignored `tmp/`; committed review images remain the
|
||
`docs/blender_previews/sheet_*.jpg` set from 0.9.4/0.9.5 until a fix actually lands.
|
||
|
||
## Character measurements
|
||
|
||
Contract numbers from `audit` + `inspect`; density from `texels` (256² slot maps, authored and
|
||
with the runtime 4× fabric repeat).
|
||
|
||
| Character | Tris / verts | Height / feet | Materials | Authored td (px/m) | Fabric effective (px/m) | Verdict |
|
||
| --- | --- | --- | --- | --- | --- | --- |
|
||
| `npc_casual` | 5,038 / 4,756 | 1.824 / +0.000 | 7 | 106.8–122.2 | 429–440 | Contract pass; face prototype |
|
||
| `npc_jacket` | 5,428 / 5,071 | 1.824 / +0.000 | 7 | 100.3–113.7 | 409–413 | Contract pass; face prototype |
|
||
| `npc_coat` | 6,030 / 5,802 | 1.824 / +0.000 | 7 | 75.3–81.4 | 307–310 | Contract pass; face prototype |
|
||
| `npc_vest` | 5,536 / 5,375 | 1.819 / +0.000 | 9 | 85.5–99.3 | 362–366 | Contract pass; face prototype |
|
||
| `npc_dress` | 5,456 / 5,128 | 1.825 / +0.000 | 7 | 81.9–89.0 | 338–339 | Contract pass; face prototype |
|
||
| `npc_hoodie` | 5,860 / 5,492 | 1.856 / +0.000 | 7 | 89.5–98.6 | 359–364 | Contract pass; face prototype |
|
||
| `npc_firefighter` | 5,900 / 5,902 | 1.819 / +0.000 | 9 | 64.0–75.1 | 287–290 | Contract pass; face prototype (helmet shadow hides the lower lip) |
|
||
| `npc_guard` | 5,602 / 5,684 | 1.819 / +0.000 | 11 | 90.0–100.8 | 374–374 | Contract pass; face prototype |
|
||
| `npc_crowd_a` | 5,200 / 4,734 | 1.825 / +0.000 | 7 | 107–120 | 431–435 | Contract pass; crowd prototype |
|
||
| `npc_crowd_b` | 4,734 / 4,353 | 1.809 / +0.000 | 7 | 107–118 | 434–439 | Contract pass; crowd prototype |
|
||
| `npc_crowd_c` | 5,674 / 5,440 | 1.824 / +0.000 | 7 | 76–83 | 317 | Contract pass; crowd prototype |
|
||
| `npc_crowd_d` | 5,814 / 5,300 | 1.856 / +0.000 | 7 | 92–106 | 377–381 | Contract pass; crowd prototype |
|
||
| `npc_base` (fallback) | 1,204 / 2,488 | 1.836 / +0.000 | 5 | 70.1–72.9 | 4× on fabric | Contract pass; compact fallback under the 16,000/16,000 cap |
|
||
|
||
Blender-side `inspect` counts can differ by ±1 vertex from the glTF accessor count in `audit`
|
||
(normal-split rounding); both are recorded and neither changes a verdict.
|
||
|
||
Readings:
|
||
|
||
- Every character is inside 16,000/16,000 with the farthest well below the new cap and 5,494
|
||
verts (69%), so there is headroom for targeted topology detail without a cap change.
|
||
- Surface area tracks clothing volume: the firefighter coat is the largest body (4.28 m²) and has
|
||
the lowest authored density; `npc_casual` is the smallest (2.44 m²) and the highest.
|
||
- **Authored density (64–122 px/m) sits below the skill's background tier (128–256 px/m).** The
|
||
runtime tint controls most of the look: `skin`, `hair`, `accessory` and the fixed detail slots
|
||
are neutral maps tinted at runtime, not painted skin, so density mostly controls subtle
|
||
weave/noise. Fabric is effective 287–440 px/m (standard tier) because of the 4× repeat. This is
|
||
a recorded note, not a defect, until a portrait shows texture starvation.
|
||
- Material counts 7–11 per character are fixed detail slots (eyes, rubber, hivis, reflective,
|
||
leather, metal), not extra tint slots.
|
||
|
||
## Baseline inventory (2026-11-04)
|
||
|
||
Recorded for the character baseline before the Quaternius migration. `npc_base.blend` is the
|
||
frozen fallback; the 12 characters are currently assembled procedurally by
|
||
`tools/blender_assets.py`. The active plan is now the CC0 Quaternius base bake
|
||
([ADR-0017](../adr/0017-quaternius-npc-base.md)); this section and the MakeHuman notes below are the
|
||
pre-migration record. The MPFB/MakeHuman development files were removed from `tmp/` after the
|
||
2026-11-05 pivot.
|
||
|
||
### `assets/models/npc/npc_base.blend` (frozen fallback)
|
||
|
||
- Mesh `npc_base`: **652 verts / 642 tris**, five material slots in order
|
||
`pants, cloth, skin, hair, accessory`.
|
||
- Armature: **14 bones** — `Hips, Spine, Neck, Head, ShoulderL/R, ElbowL/R, HandL/R, HipL/R, KneeL/R`
|
||
(the required 13 core bones plus the `Hips` root).
|
||
- It is the compact fallback, not the prototype host; the overhaul must not absorb prototype
|
||
detail into it unless a separate contract decision approves it.
|
||
|
||
### 12 generated character GLBs
|
||
|
||
`audit` counts (glTF accessor; Blender `inspect` can differ ±1 on normal-split rounding):
|
||
|
||
| Character | Tris | Verts | | Character | Tris | Verts |
|
||
| --- | ---: | ---: | --- | --- | ---: | ---: |
|
||
| `npc_casual` | 5,034 | 4,607 | | `npc_firefighter` | 5,888 | 5,785 |
|
||
| `npc_jacket` | 5,432 | 5,225 | | `npc_guard` | 5,670 | 5,671 |
|
||
| `npc_coat` | 6,046 | 5,645 | | `npc_crowd_a` | 5,284 | 4,908 |
|
||
| `npc_vest` | 5,532 | 5,195 | | `npc_crowd_b` | 4,812 | 4,550 |
|
||
| `npc_dress` | 5,540 | 5,073 | | `npc_crowd_c` | 5,690 | 5,510 |
|
||
| `npc_hoodie` | 5,864 | 5,325 | | `npc_crowd_d` | 5,810 | 5,444 |
|
||
|
||
All well under the 16,000/16,000 cap; the largest is `npc_coat` at 38% of the tri budget, so
|
||
head/face/hand detail has ample headroom.
|
||
|
||
### Procedural builders (current geometry locations)
|
||
|
||
Coordinates are Blender metres, Z-up; face front is −Y / Godot +Z; `s` = ±1 side, `w` = build
|
||
width scale.
|
||
|
||
- **Head** — `_face` + `_shape_head_mesh`: `sphere(Head, 0.155, (0, 0, 1.635), seg=24, rings=16,
|
||
scale=(0.92, 1.0, 1.12))`. Forward-only sculpt (brow at z 1.695, cheek at z 1.612, jaw/chin at
|
||
z 1.565); recessed creases rejected because they bake as dark AO patches.
|
||
- **Eyes** — sclera `sphere(0.019, (0.052s, −0.134, 1.664), scale=(1.25, 0.62, 0.85))`;
|
||
iris `sphere(0.0105, (0.052s, −0.141, 1.664), scale=(1.0, 0.55, 1.0))`; upper lid at z +0.014,
|
||
lower lid at z −0.012; brow box at (0.052s, −0.143, 1.699).
|
||
- **Nose** — bridge `cyl(0.016→0.022, (0, −0.145, 1.652))`, tip `sphere(0.022, (0, −0.162, 1.615))`,
|
||
wings `sphere(0.012, (0.016s, −0.160, 1.606))`.
|
||
- **Mouth** — slot `sphere(0.028, (0, −0.158, 1.583), scale=(1.0, 0.28, 0.12))`, lower lip
|
||
`sphere(0.012, (0, −0.156, 1.578))`.
|
||
- **Ears** — `sphere(0.036, (0.143s, 0.008, 1.63), scale=(0.42, 1.0, 1.35))` + lobe + inner concha.
|
||
- **Hands** — `_hands_and_shoes`: palm `sphere(0.046, (0.275·s·w, −0.012, 0.762),
|
||
scale=(0.80, 0.62, 0.88))`; four fingers base (…, 0.778) → tips z 0.701–0.714; thumb
|
||
(…, 0.778 → 0.760 → 0.738). No finger bones; open palm + `HAND_ITEM_FORWARD` only.
|
||
|
||
### UV layout
|
||
|
||
- Five tint slots (`skin, hair, accessory, cloth, pants`) at 256² each; fixed detail slots
|
||
(`eye_white`, rubber, hivis, reflective, leather, metal) are the extra material slots.
|
||
- Authored texel density 64–122 px/m (below the background tier 128–256), fabric effective
|
||
287–440 px/m via the 4× repeat; runtime tint carries the look. This is a recorded note until a
|
||
portrait shows texture starvation.
|
||
|
||
### MakeHuman prototypes (historical, not shipped; research files removed from `tmp/`)
|
||
|
||
| File | Verts / tris | Bones | Slots | Note |
|
||
| --- | ---: | ---: | --- | --- |
|
||
| `makehuman_body.glb` | 19,320 / 6,444 | 0 | `skin_proto` | CC0 intake, unskinned, 1.82 m grounded |
|
||
| `makehuman_retargeted.glb` | 7,062 / 6,444 | 14 | 5 tint | arm-down + contract rig, deltoid warp remains |
|
||
| `makehuman_final.glb` | 6,651 / 6,407 | 14 | 8 (incl. `skin.001`, `eye_white`, `rubber`) | project head/hands/shoes swapped; duplicate `skin.001` |
|
||
|
||
The "Icosphere" the inspector lists alongside these GLBs is the glTF importer's
|
||
bone-display helper (a `pose_bone.custom_shape`), not part of the asset; `inspect_glb` already
|
||
excludes it from measurements. The real cleanup for `makehuman_final.glb` is the duplicate
|
||
`skin.001` material and the manual deltoid retopology.
|
||
|
||
## Overhaul acceptance criteria (measurable)
|
||
|
||
Defined 2026-11-04 for the local part pipeline. A prototype must pass every applicable item
|
||
before it is rolled out to the other variants; the bar is the stylized low-poly target, not
|
||
photorealism.
|
||
|
||
### Face (one-variant prototype first)
|
||
|
||
Capture `front` (yaw 0°), `three_quarter` (yaw 35°) and `profile` (yaw 90°) at **1.5 m** and
|
||
**1.85 m** (32° FOV), matching `capture_npc_baseline.gd` (a new 1.5 m portrait capture is added
|
||
for this). Pass conditions:
|
||
|
||
- **Eyes** — sclera + iris + both lids distinct at 1.85 m; at 1.5 m the iris is a readable dark
|
||
dot against the white sclera, not a floating oval; no socket ring or lid seam.
|
||
- **Mouth** — the mouth line and lower-lip volume read at both distances; no moustache-like
|
||
shadow from a floating dark slot.
|
||
- **Nose** — bridge, tip and ala planes read in three-quarter and profile; no nose-wing seam or
|
||
inverted normal at the base.
|
||
- **Ears** — concha depth + lobe read in profile; not a flat disc.
|
||
- **Jaw/cheek/chin** — front and profile show a jaw transition and cheek/chin planes; no faceted
|
||
plane seams, inverted normals or z-fighting.
|
||
- **Silhouette** — front/three-quarter/profile agree; face direction is unambiguous.
|
||
- **AO** — the 256² bake stays clean: no recessed crease baking into a dark patch.
|
||
|
||
### Hands (one-variant prototype first)
|
||
|
||
Capture an **open palm** and a **relaxed grip** at **0.8 m** (`--view=hand` plus a new grip
|
||
capture). Pass conditions:
|
||
|
||
- **Open palm** — four fingers and a thumb read as a hand, not a mitten; at least two of the four
|
||
finger gaps are readable.
|
||
- **Grip** — the palm plus curled fingers silhouette reads; no finger-through-palm clipping.
|
||
- **Gear contact** — phone, cup, clipboard, camera, flashlight, wand and radio contact the palm
|
||
at 0.8 m with `HAND_ITEM_FORWARD` unchanged; no floating or hand-through-item clipping.
|
||
|
||
### Contract (both, every prototype)
|
||
|
||
- 24,000 triangles / 24,000 glTF vertices per character; gear stays at 1,500.
|
||
- Optional extra bones are allowed; the required 13 core bones and their rest positions must not
|
||
move; the `npc_base` mesh node name and the five tint slots stay unchanged.
|
||
|
||
### Measurement gates
|
||
|
||
`blender_assets.py inspect`/`audit`/`fit`, `npc_proportions.py`, the 24-NPC budget, Godot + Python
|
||
tests, the 48-frame baseline and the 96-frame pose/context review all stay authoritative; the new
|
||
face/hands captures are the visual bar for this track.
|
||
|
||
### Working-set layout
|
||
|
||
Part sources and preview GLBs live under `tmp/npc-parts/<variant>/` (gitignored by the root
|
||
`tmp/` rule); only the assembled character GLBs, AO/maps, the manifest and the review sheets are
|
||
committed. No new committed `.blend` source tree is added without a docs/ADR decision — the
|
||
frozen `npc_base.blend` stays the only tracked `.blend`.
|
||
|
||
### Head prototype (in progress 2026-11-04)
|
||
|
||
`tools/npc_parts.py build --part head_proto` is the first connected-shell attempt: a seg-48
|
||
head sphere is displaced (not floating spheres) into eye sockets with an orbital rim, a nose
|
||
bridge/tip/ala, upper/lower lip volumes, and extruded ears with a concha recess; the eyeballs
|
||
stay separate. Local renders live under `tmp/npc-parts/npc_casual/head_proto_*.png`.
|
||
|
||
Iteration log (vision-judged, profile view):
|
||
|
||
1. Initial displacement too subtle — read as a "ball with props".
|
||
2. Nose + ear visible in profile but the cranium still read as a "ball with a nose".
|
||
3. Added face-plane flatten + jaw/chin taper; taper read, but "~90% sphere".
|
||
4. Stronger flatten + nose + chin; nose now reads, jawline still dissolves into the skull.
|
||
5. Fixed an ear-displacement sign bug (the ± side loop cancelled the helix); the ear now
|
||
protrudes (x ±0.155 vs the ±0.139 shell) but reads as a soft blob.
|
||
|
||
Current state (prototype built, ~3/5 vision read): the nose, mouth notch, chin bump and ear bulge
|
||
all read; the centre-profile measurement confirms the relief is objective — nose tip −0.192,
|
||
brow −0.150, chin −0.149, jaw step −0.11, forehead −0.126→−0.078. The remaining polish (a flatter
|
||
forehead, a defined ear helix/lobe, a jawline that steps to the neck) is folded into the §3.2
|
||
variant comparison; the neck is part of `_skin_body`, so the standalone head always lacks it.
|
||
|
||
### AI-driven head rework (2026-11-04)
|
||
|
||
The head was reworked against an AI-generated target concept (front + profile, saved under
|
||
`tmp/npc-parts/head_concept_*.png`) and judged with `describe_image.py` across several iterations.
|
||
The displacement-on-sphere approach oscillates between "decorated egg" (too subtle) and
|
||
"spiky wedges" (too aggressive) and settles at ~3/5 — the coarse s48 sphere cannot resolve the
|
||
nose/lip/chin relief cleanly.
|
||
|
||
The judge then compared the displacement head against the **decimated MakeHuman head**
|
||
(extracted from `makehuman_retargeted.glb`, 2,009 verts / 1,573 tris, saved as
|
||
the local `makehuman_head.glb`) and returned a decisive result: **ship the MakeHuman head** —
|
||
it keeps the nose/chin/jaw landmarks at a fraction of the cost, while the procedural head's
|
||
"clean ellipse outline instantly screams procedural sphere".
|
||
|
||
**Conclusion:** the next head asset is the MakeHuman head shape (1,573 tris) plus the project's
|
||
separate eye/mouth/brow pieces for readability, not the displacement shell. The displacement
|
||
`_head_proto` stays local as a rejected variant.
|
||
|
||
**Built (2026-11-04):** `_makehuman_head` in `blender_assets.py` imports the committed
|
||
`assets/models/blender_src/makehuman_head.glb` (CC0, attributed in `CREDITS.md`), collapses it to
|
||
one skin slot, bisects + caps the ragged neck, and adds the project sclera/iris/lids/brows/mouth
|
||
at the MakeHuman landmarks. `build_character(name, head="mh")` assembles it; the result passes
|
||
`inspect` (14 bones, skinned, no issues). The judge reads the face ~3.5/5; remaining polish is
|
||
welding the added eye/brow pieces into the head shell and a skull-conforming hair cap.
|
||
|
||
**Standalone head re-review (2026-11-04):** the separate
|
||
`assets/models/blender_src/makehuman_head.glb` prototype (12,641 tris / 7,690 split verts,
|
||
with a skin eyelid mask, recessed globes, flat iris/pupil discs and surface-anchored brows)
|
||
was **accepted by the owner** as a basic human head. The automated vision gate still returns
|
||
REJECT on the eye/lid region (globe depth, lid overlap, iris at three-quarter) as deferred
|
||
polish. The assembled `npc_casual --head mh` now adds a skull-conforming hair cap, a hidden
|
||
scalp trim and a narrowed neck/shoulder body: 14,753 tris / 12,823 seam-split verts /
|
||
14 bones / 9 materials, `inspect` clean. All 12 variants count 14,693–15,987 tris. See
|
||
[`makehuman-head-review.md`](makehuman-head-review.md).
|
||
|
||
### Head subdivision comparison (2026-11-04)
|
||
|
||
Three `head_proto` variants at UV-sphere segments 24/48/64 were rendered at 1.85 m and 1.5 m in
|
||
front/three-quarter/profile and judged side by side:
|
||
|
||
| Variant | Verts / tris | 1.85 m | 1.5 m | Verdict |
|
||
| --- | ---: | --- | --- | --- |
|
||
| s24 | 670 / 1,168 | lumpy, nose/lips melt into the chin | jagged silhouette, no mouth | reject |
|
||
| s48 | 1,558 / 2,944 | eyes/brows/nose/lips read cleanly | features resolve cleanly | **pick** |
|
||
| s64 | 2,470 / 4,768 | visually identical to s48 | indistinguishable from s48 | reject (wasted verts) |
|
||
|
||
The chosen s48 is the `head_proto` default; the rejected s24/s64 stay local under
|
||
`tmp/npc-parts/`. A before/after sign-off sheet (current `_face` vs s48, 1.85 m
|
||
front/three-quarter/profile) is
|
||
[`blender_previews/sheet_head_proto_before_after.jpg`](../blender_previews/sheet_head_proto_before_after.jpg).
|
||
|
||
## Foliage language P1 — resolved 2026-11-06
|
||
|
||
The ranked P1 was that shrubs read as rocks: `_shrub()` stacked spheres, so the silhouette was
|
||
smooth and the light response was a single facet per clump.
|
||
|
||
`_leaf_card()` replaces the stack: one low-poly inner mass for opacity, then 18–53 two-triangle
|
||
quads scattered over the canopy shell by a jittered Fibonacci distribution, each rotated onto the
|
||
canopy normal and tilted off it. No alpha texture, so the flat-colour library and the Compatibility
|
||
renderer are untouched.
|
||
|
||
| Prop | Before | After |
|
||
| --- | --- | --- |
|
||
| `planter_box` | 1,324 tris | 818 |
|
||
| `round_planter` | 1,240 tris | 808 |
|
||
| `street_tree` | 2,592 tris | 1,506 |
|
||
| Props total (20) | 13,808 | 11,784 |
|
||
|
||
## Scorecard skeleton
|
||
|
||
Verdicts use `qa-review` semantics: **P0** (blocker: wrong scale, broken normals, budget fail),
|
||
**P1** (major: silhouette/readability), **P2** (minor), **accept** (recorded limit). `—` means not
|
||
yet reviewed; this document is the place to fill it.
|
||
|
||
### Characters
|
||
|
||
| Character | Silhouette | Face @1.85 m | Hands/grip | Weights/pose | Cloth/hair | LOD | Verdict |
|
||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||
| *(pose captured 2026-11-04: see "Pose evidence" below)* | | | | | | | |
|
||
| `npc_casual` | accept (0.9.4) | accept (face prototype) | accept (open palm) | fixed (wrist/hand + hip/knee) | accept | accept (two-quad impostor) | pending owner |
|
||
| `npc_jacket` | accept | accept (face prototype) | accept | fixed (wrist/hand + hip/knee) | accept | accept | pending owner |
|
||
| `npc_coat` | accept | accept (face prototype) | accept | fixed (wrist/hand + hip/knee) | accept (hem 0.76 stride) | accept | pending owner |
|
||
| `npc_vest` | accept | accept (face prototype) | accept | fixed (wrist/hand + hip/knee) | accept | accept | pending owner |
|
||
| `npc_dress` | accept; performer props dominate | accept (face prototype) | accept | fixed (wrist/hand + hip/knee) | accept | accept | pending owner |
|
||
| `npc_hoodie` | accept (hooded criterion) | accept (lower face only) | accept | fixed (wrist/hand + hip/knee) | accept | accept | pending owner |
|
||
| `npc_firefighter` | accept | accept (face prototype) | accept | fixed (wrist/hand + hip/knee) | accept | accept | pending owner |
|
||
| `npc_guard` | accept | accept (face prototype) | accept | fixed (wrist/hand + hip/knee) | accept | accept | pending owner |
|
||
| `npc_crowd_a` | accept | accept (face prototype) | accept | fixed (wrist/hand + hip/knee) | accept | accept | crowd prototype |
|
||
| `npc_crowd_b` | accept | accept (face prototype) | accept | fixed (wrist/hand + hip/knee) | accept (beard) | accept | crowd prototype |
|
||
| `npc_crowd_c` | accept | accept (face prototype) | accept | fixed (wrist/hand + hip/knee) | accept (coat hem) | accept | crowd prototype |
|
||
| `npc_crowd_d` | accept | accept (face prototype) | accept | fixed (wrist/hand + hip/knee) | accept | accept | crowd prototype |
|
||
|
||
### Gear (18)
|
||
|
||
| Gear | Attach | Tris | Verdict |
|
||
| --- | --- | --- | --- |
|
||
| `cap` | head | 328 | — (fit measured, visual pending) |
|
||
| `hood` | head | 804 | — |
|
||
| `sunglasses` | head | 164 | accept (0.9.4 clearance) |
|
||
| `firefighter_helmet` | head | 672 | accept (0.9.4 + 3 cm runtime offset) |
|
||
| `backpack` | back | 380 | accept (0.9.4 strap refit) |
|
||
| `shoulder_bag` | back | 188 | — |
|
||
| `scba_tank` | back | 362 | — |
|
||
| `lanyard` | chest | 100 | accept (0.9.4 + 4 cm runtime offset) |
|
||
| `phone` | hand | 96 | accept (forward offset) |
|
||
| `cup` | hand | 188 | accept |
|
||
| `clipboard` | hand | 84 | accept |
|
||
| `umbrella` | hand | 134 | accept |
|
||
| `camera` | hand | 318 | accept |
|
||
| `protest_sign` | hand | 92 | accept (performer/protester read) |
|
||
| `flashlight` | hand | 168 | accept |
|
||
| `wand` | hand | 244 | accept |
|
||
| `radio` | hand | 220 | accept |
|
||
|
||
Gear total: 5,002 tris across 18 pieces (all far below the 1,500-per-piece cap).
|
||
|
||
`blender_assets.py fit` readings (2026-11-03): `shoulder_bag` is clear on both `npc_coat` and
|
||
`npc_dress` (the only back-gear clearance the fit test asserts); `sunglasses` sits inside the
|
||
head band at 0.64× head width. Three caveats worth recording, none of which changes a verdict:
|
||
`backpack` (npc_casual) and `scba_tank` (npc_firefighter) report `CLIP` because back gear is
|
||
measured against the **hip/skirt** radius, which is wider than the upper back where the bags
|
||
actually sit; `cap` and `firefighter_helmet` report `band=OUTSIDE` because they sit on the crown
|
||
above the 1.55–1.81 face/head band (expected); and the `lanyard` clip is corrected at runtime by
|
||
`GEAR_OFFSETS` (+4 cm forward). The unused `beanie` was retired on 2026-11-03 (removed from
|
||
`GEAR`, its builder, `npc_avatar.gd` and the manifest).
|
||
|
||
### Props (20 library + 6 generated fallbacks)
|
||
|
||
Library total 13,608 tris; `marquee` (2,804) and `street_tree` (2,592) are the two heaviest and are
|
||
the first candidates for a `vegetation-artist`/lookdev pass.
|
||
|
||
| Prop | Tris | Verdict |
|
||
| --- | --- | --- |
|
||
| `bollard` | 138 | accept (2026-11-03 flared base collar) |
|
||
| `bench` | 560 | — |
|
||
| `trash_bin` | 228 | — |
|
||
| `planter_box` | 1,324 | — |
|
||
| `newspaper_box` | 300 | accept (2026-11-03 recessed slot + door seam) |
|
||
| `fire_hydrant` | 610 | accept (2026-11-03 rebuild: bolted flange, bonnet cap nut, larger side + front outlets) |
|
||
| `street_lamp` | 428 | — |
|
||
| `street_tree` | 2,592 | P1 candidate (flat-shaded clumps) |
|
||
| `palm_tree` | 400 | P1 candidate (same foliage language) |
|
||
| `round_planter` | 1,240 | — |
|
||
| `a_frame_sign` | 212 | — |
|
||
| `wall_lantern` | 180 | — |
|
||
| `traffic_cone` | 210 | — |
|
||
| `crowd_barrier` | 388 | — |
|
||
| `stanchion` | 434 | — |
|
||
| `handrail` | 268 | — |
|
||
| `security_podium` | 184 | — |
|
||
| `double_door` | 380 | — |
|
||
| `marquee` | 2,804 | — (emissive check pending) |
|
||
| `sedan` | 928 | — |
|
||
|
||
Vision review 2026-11-03 (`tools/describe_image.py` on the prop gallery): `bench` and `trash_bin`
|
||
are ship-ready (minor chamfer polish only); `bollard` passes but leans on its reflective band
|
||
(add a base collar); `planter_box`/`round_planter` fail on the foliage — the `_shrub()` clumps
|
||
read as rocks, not plants (same foliage language gap, gated on the owner); `newspaper_box` is
|
||
borderline (the slot is a flat decal, wants a recessed inset + door seam); `fire_hydrant` is now
|
||
rebuilt (see row). No prop floats or sinks; shadow direction is consistent.
|
||
|
||
Generated fallbacks (`assets/models/generated/`): bench, bollard, fire_hydrant, newspaper_box,
|
||
planter_box, trash_bin. They are only used when the library prop is missing, so they are audited
|
||
for presence, not re-reviewed for art quality.
|
||
|
||
### Venues
|
||
|
||
| Venue | Measured (map-budget test) | Status |
|
||
| --- | --- | --- |
|
||
| Hollywood Boulevard | 58,884 tris / 409 meshes | shape-language review pending |
|
||
| The Joke Shop | 26,828 tris / 580 meshes | facade review landed: 6.0 m low hall, white signature band, tied-back curtains, west hotel tower; remaining gap = rounded west corner |
|
||
| TV Studio | — | shape-language review pending |
|
||
|
||
## Pose evidence (fixed 2026-11-04)
|
||
|
||
The weights/pose column can now be filled. The root cause was the harness: it never disabled the
|
||
avatar's `_physics_process`, so `_advance_fsm` + `_move_toward_target` reset `arrived`/velocity
|
||
every tick and `theater_pose()` reverted to idle. `capture_npc_pose.gd` now calls
|
||
`set_physics_process(false)`/`set_process(false)` after staging, and the pose sticks (sit vs idle
|
||
diff 5.63, walk vs idle 2.29, vision-judged as crouched). The earlier "moving the `Rig` child does
|
||
not render" probe was a red herring: it set the position in `_shoot()` in the same frame as
|
||
`root.get_texture().get_image()`.
|
||
|
||
`src/test/capture_npc_pose.gd` stages idle / walk-contact / walk-pass / seated for all twelve
|
||
variants (full-body + shoulder views).
|
||
|
||
| Check | Result |
|
||
| --- | --- |
|
||
| Pose reaches proxies | Seated pose logs `rig_y=-0.339`, `leg0=-1.357` |
|
||
| Pose reaches skeleton bones | `HipL`/`KneeL` bone rotations differ from rest; headless test `test_pose_reaches_the_skeleton_bones` locks it (suite 273/273) |
|
||
| Idle restores rest | Same test asserts the hip returns to its rest orientation |
|
||
| Skinning bound | One `Skeleton3D`, mesh bound to it (`same=true`); hiding `npc_base` removes the body |
|
||
| Bone rotations deform the mesh | Direct writes change rendered pixels: Spine 1,719 / HipL 1,052 / KneeL 810 / ElbowL 745 / Head 671 / ShoulderL 640 sampled pixels |
|
||
| Bone translation deforms the mesh | A +0.5 m Head offset moves the head in frame |
|
||
| Standalone window probes render the pose | Three variants (avatar under `root`, under a `Node3D` stage, created mid-`_process`) render the seated crouch (diff ~2,200-2,360 samples) |
|
||
| Offscreen capture renders the pose | A `SubViewport` + `RenderingServer.force_draw()` probe renders the crouch reliably (diff 602 at +2/+6/+12 frames), including a re-created second avatar |
|
||
| The harness renders the pose | **No** in its current form: same stage, same pose, rest frames. One ordering (`_frame_view()` immediately before `force_draw()`) produced posed walk frames once, then regressed to rest/garbage on the next run |
|
||
|
||
The rig, the animation math and the offscreen render path are all sound. A 2026-11-03 re-attempt
|
||
with the recipe (SubViewport + `force_draw()` + `force_update_all_bone_transforms()` + disabled
|
||
process/physics) still produced byte-identical rest frames, and a decisive probe narrowed the
|
||
failure further: teleporting the **avatar** changes the render (diff 10), but moving the **`Rig`
|
||
child** (the GLB model's parent) does not (diff 0). The skinned mesh therefore does not follow its
|
||
`Rig` Node3D transform, so the rig lowering never reaches the rendered model — the defect is
|
||
upstream of camera/`force_draw()` ordering. Next attempt must answer why the skinned model ignores
|
||
the `Rig` node, or stage poses via the known-good `capture_npc_review.gd` fallback.
|
||
|
||
Working recipe (verified by the probes; keep it when rebuilding):
|
||
|
||
1. Create a `SubViewport` (1280x720, `own_world_3d = true`, `UPDATE_ALWAYS`) in `_initialize`.
|
||
2. Build the studio (environment, key/fill light, ground, camera) and one avatar inside it.
|
||
3. Wait 2-3 frames so the skinned mesh binds.
|
||
4. Apply the pose with `_animate()` ticks, with the avatar's process/physics disabled.
|
||
5. Call `RenderingServer.force_draw()`, then `viewport.get_texture().get_image()`.
|
||
6. Free the avatar and repeat for the next pose; a re-created avatar also rendered correctly in a probe.
|
||
|
||
## Ranked candidate gaps
|
||
|
||
Ordered by expected player-visible gain per unit of work. None is approved for fixing yet; the
|
||
owner review decides.
|
||
|
||
1. **Face plane structure at portrait distance (P1, all eight).** The neutral sheet shows flat,
|
||
faceted cheek/brow planes and slit-like eyes at 1.85 m. `character-artist`/`retopology` guidance
|
||
asks for concentric eye loops and a 3-loop mouth. This is the largest readability gap and also
|
||
the most expensive (head rebuild + re-UV + re-AO per variant). Needs a targeted single-variant
|
||
prototype before committing to eight.
|
||
|
||
The eight library characters already carry the 0.9.4 face detail in
|
||
`tools/blender_assets.py._face()`: sculpted chin plate + cheekbones (`_shape_head_mesh`),
|
||
socketed sclera + thin upper lid, nose bridge/tip/wings, a rounded mouth and ear concha
|
||
(the plain sphere head in `tools/npc_pipeline.py` is the frozen `npc_base` fallback only).
|
||
So this gap is a readability/scale question on the existing sculpt — the head is
|
||
`seg=18/rings=12` with small offsets — not missing geometry. Concrete next step, if funded:
|
||
a single-variant `npc_casual` prototype that raises head subdivision and deepens the
|
||
cheek/brow offsets, then re-bakes AO and captures the portrait before touching the other seven.
|
||
|
||
**Prototype landed 2026-11-03/04:** `npc_casual` now has `seg=24/rings=16` (smoother), a subtle
|
||
brow ridge, a fuller lower lip and both eyelids (upper + lower); recessed creases (nasolabial,
|
||
temple, chin cleft) were rejected because they bake as dark AO patches. Vision-reviewed: the new
|
||
face reads cleaner and more realistic than the old (set-in eyes, brow break, lip volume, no
|
||
faceting); tris 4,482 → 5,038 (still under 16,000). The other seven variants keep the old face
|
||
until this prototype is approved.
|
||
2. **No pose/weight evidence (unknown).** Idle, walk, sit, block and turn deformation has never been
|
||
captured as a review set, so shoulder/hip/neck collapse is unmeasured. Cheap to add and it
|
||
gates any claim that the rig is acceptable.
|
||
3. **Foliage language (P1, `street_tree`, `palm_tree`, `planter_box`, `round_planter`).** Flat-shaded
|
||
clumps against an otherwise readable set, already deferred once. `vegetation-artist` carded
|
||
clusters would fix it inside the existing triangle budget.
|
||
4. **Texel density below the background tier (note).** 64–122 px/m authored vs the skill's 128–256
|
||
background tier. Fabric is already standard-tier at runtime. Decide: accept because the maps are
|
||
neutral tinted detail, or raise only `skin`/`hair` to 512² for portrait distance.
|
||
5. **`npc_firefighter` density floor (P2).** 64 px/m with the largest coat area; split the coat UV
|
||
shell or raise the map size before the next character pass, whichever the density decision says.
|
||
6. **Hands at conversation distance (accept, revisit only if face work opens the budget).** Open
|
||
palm with `HAND_ITEM_FORWARD`; no finger bones exist, so a curl rig is a contract decision, not
|
||
a modelling tidy-up.
|
||
7. **Gear never reviewed as silhouette language (P2).** 18 pieces pass fit/contract but have no
|
||
prop-artist verdict (the unused `beanie` was retired 2026-11-03).
|
||
8. **Venue shape language (P2).** Hollywood and the Joke Shop have budget data but no
|
||
`environment-artist`/`archviz` review; the TV Studio has no recorded map-budget number in this
|
||
document yet.
|
||
|
||
## qa-review sign-off (2026-11-04)
|
||
|
||
All eight library characters **ship with notes**. Face realism landed on all eight (`seg=24/rings=16`,
|
||
brow ridge, both eyelids, fuller lower lip), hands/grip and cloth/hair are accepted as recorded, gear
|
||
fit is measured with its caveats, and the props are reviewed (hydrant/bollard/newspaper_box rebuilt,
|
||
foliage two-toned). The only unresolved column is **weights/pose** (blocked on the capture harness),
|
||
and the full carded-cluster foliage pass is the one remaining owner-gated P1. No P0 remains.
|
||
|
||
## Decisions needed from the owner
|
||
|
||
- Accept the measured texel density as a stylized limitation, or fund a skin/hair resolution pass.
|
||
- Approve or reject a single-variant face/eye-loop prototype (and pick which variant to prototype
|
||
on) before any eight-character rebuild.
|
||
- Decide whether the pose/weight capture is required before the crowd-variant roster.
|
||
- Confirm foliage rebuild priority versus the map track (both touch the same street trees).
|
||
|
||
## Next actions
|
||
|
||
- Review the assembled [`makehuman-head-review.md`](makehuman-head-review.md) prototype (hair cap and full-body silhouette) before rebuilding the 12 shipped character GLBs.
|
||
|
||
|
||
Tracked in [`TODO.md`](../../TODO.md) `3D model re-evaluation`: the remaining `A. Baseline…` item is
|
||
this owner review; `B`/`C`/`D` carry the per-family work, and `E` adds the pose/weight,
|
||
collider-coverage and scorecard-coverage gates this document found missing.
|