Turn the NPC visual review into the 0.9.9 roster signoff page with committed sheets
Full build / Import + headless tests (push) Successful in 44s
Full build / Full desktop build + optional release (push) Skipped

This commit is contained in:
zwitschi committed 2026-11-06 14:05:49 +01:00
1 parent b3acf370fc
commit c8875a3dfa
7 files changed
+128 -310

No files matched your search

+15
View File
@@ -123,6 +123,12 @@ canonical implemented-features list in `docs/development-status.md`.
offsets from the bone rest, so the correct swing around a world axis is
`rest * Quaternion(rest.inverse() * axis, angle)`. For the old 180° rest this reduces to the
legacy `rest * Quaternion(axis, angle)`, so the 13-core fallback is unchanged (commit `8b1bf73`).
- **Full roster swap (2026-11-05, owner):** at the `0.9.9` face gate the owner chose option (a),
full swap. All twelve public character names are baked from the three CC0 Quaternius sources
(`casual`, `hoodie`, `worker`); variety comes from the five tint slots, the outfit rules and the
gear library. The procedural library + MakeHuman head stay in the tree as the tested fallback
(`build --source procedural`), not in the shipped roster. Silhouette variety is the known
regression — three meshes for twelve roles — and the modular part library is the `0.9.10` fix.
- **One character source, one fallback:** no new NPC source without an ADR. MPFB body work stays
closed, the Stage-1 proportion warp is not reopened, and the procedural `npc_base` + MakeHuman
head stay tested until the Quaternius base passes its gate.
@@ -275,6 +281,15 @@ patches (`0.9.9`–`0.9.12`) with `0.10.0` as the public-readiness minor. The ga
three release roadmaps now live in `PLAN.md`; `TODO.md` is the executable task list. ADR-0017
Stage 1 (proportion warp) is dead; ADR-0018 option 2 (62-joint runtime) is accepted.
**Full roster (0.9.9, commits `a6ccd86`, `04d211b`, `b3acf37`):** all twelve characters run on
the Quaternius bases (4,458–6,424 tris, 62 joints, 73–74 px/m texel density), built by the
shipped `tools/blender_assets.py` two-pass pipeline (`_quaternius_tpose_glb` →
`_bake_quaternius_rest`). Gates: `audit` 12/12, `inspect` clean, `fit` OK, 24-NPC budget
160,608 tris / 0.84 ms, Godot 278/278, Python 167/167. The owner visual signoff page is
`docs/npc-visual-review.md`. Release packaging was also fixed the same day: excluding
`tmp/`, `build/`, `docs/`, `tools/`, `src/test/` and `.agents/` from the export presets cut the
Linux binary from 426 MB to 273 MB, and `tools/build_releases.sh` now writes a SHA256SUMS file.
**Quaternius pilot (2026-11-05, commits `cd285b3`, `fb3e75a`, `90f189a`):** `npc_casual` ships as
the Quaternius base. It renders arm-down, carries the five tint slots, plays the Idle/Walk/Sit pose
code on the 62-joint rig, and the procedural rigs are visually unchanged. Verified: `inspect`
+17 -6
View File
@@ -820,15 +820,21 @@ blocked rig can no longer block the whole release.
`npc_casual`, front / three-quarter / profile. ADR-0018 removed the proportion warp, so
source proportions are accepted by definition (head 13.4 % of height, measured by
`npc_proportions`).
- [x] Owner review page with the visual signoff sheets and checklist:
[`docs/npc-visual-review.md`](docs/npc-visual-review.md) (the procedural-era review text
that used to live there is retired; its baseline sheets stay in `blender_previews/`).
- [x] Face-fidelity comparison sheet: [`docs/screens/npc-face-gate/`](../../docs/screens/npc-face-gate/README.md)
captures the Quaternius base, the procedural + MakeHuman head and a procedural crowd variant
at front / three-quarter / profile / 8 m. The page records the three options (full swap,
crowd only, add head detail) and recommends the full swap.
- [ ] **Owner decision:** pick (a) full swap, (b) crowd only or (c) add head detail from
`docs/screens/npc-face-gate/README.md`, then apply it within 24,000 tris / 24,000 glTF
vertices and re-run the gate.
- [ ] Record the decision and its consequence in `MEMORY.md`, `docs/npc-model-sourcing.md` and
(if it changes the contract) a new ADR. Do not silently lower character fidelity.
- [x] **Owner decision:** option (a) full swap (2026-11-05). Applied: all twelve characters are
baked from the three Quaternius sources inside 24,000 tris / 24,000 glTF vertices
(`a6ccd86`), and the gate was re-run.
- [x] Record the decision and its consequence in `MEMORY.md`,
`docs/npc-model-sourcing.md#shipped-roster-099-owner-chose-the-full-swap` and the
follow-up decision section of
[`docs/adr/0018-quaternius-stage2-runtime-rig.md`](docs/adr/0018-quaternius-stage2-runtime-rig.md).
- [ ] Owner visual signoff on the shipped roster: [`docs/npc-visual-review.md`](docs/npc-visual-review.md).
### Pilot rollout
@@ -879,7 +885,12 @@ regressing gear fit, budget or readability.
import) before judging any face or body change.
- [ ] Run `inspect`/`audit`/`fit`/`texels` and `npc_proportions.py report`; record the
12-variant measurement table.
- [ ] Capture the 12 × 6 baseline and get owner approval before replacing the committed GLBs.
- [-] Capture the 12 × 6 baseline and get owner approval before replacing the committed GLBs.
**Owner review page: [`docs/npc-visual-review.md`](docs/npc-visual-review.md)** — the
full-body roster, front/profile per role, the idle/sit/walk pose sheet and the 8 m crowd
strip, with an eight-row signoff checklist and the six issues found while capturing. The
GLBs are already committed (0.9.9); this signoff either accepts them or turns each rejected
row into a fix task.
- [ ] Re-run the 24-NPC budget smoke and the Web/Compatibility capture; record frame time,
mesh/triangle counts and Web load before/after.
- [ ] Retire the procedural variants that are no longer used, keeping one tested fallback; update
Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

+96 -304
View File
@@ -1,317 +1,109 @@
# NPC visual baseline and acceptance criteria
# NPC roster visual review — 0.9.9 signoff
Status: **accepted 2026-11-02 for the 0.9.4 stylized target.** The P1 fixes
(tourist photo-prop visibility, backpack straps), neck/shoulder and torso/hip
transitions, face depth/detail, hand and gear placement, headgear clearance and
shoe/boot silhouettes are applied. The baseline remains the comparison reference;
the accepted limitations are listed below. Comparison sheets:
[`sheet_npc_baseline_compare-p1.jpg`](blender_previews/sheet_npc_baseline_compare-p1.jpg),
[`sheet_npc_baseline_compare-body.jpg`](blender_previews/sheet_npc_baseline_compare-body.jpg),
the latest [`sheet_npc_baseline_compare-torso.jpg`](blender_previews/sheet_npc_baseline_compare-torso.jpg),
the face-detail pass [`sheet_npc_baseline_compare-face-detail.jpg`](blender_previews/sheet_npc_baseline_compare-face-detail.jpg),
the conversation-distance hand pass
[`sheet_npc_hands_compare.jpg`](blender_previews/sheet_npc_hands_compare.jpg), and the
forward-grip pass [`sheet_npc_hands_grip_compare.jpg`](blender_previews/sheet_npc_hands_grip_compare.jpg).
**Status: awaiting owner visual signoff.** This page is the single review point for the
Quaternius roster swap that shipped in 0.9.9
([ADR-0017](adr/0017-quaternius-npc-base.md), [ADR-0018](adr/0018-quaternius-stage2-runtime-rig.md)).
Every character name the game ships is captured here at gameplay distance, in three studio views,
and in the three animation poses the runtime drives.
The 2026-11-03 skill-guided re-evaluation, its measurements (texel density, budgets, imported
bounds) and the open scorecard live in [`3d-model-review.md`](3d-model-review.md). It does not
change the accepted limits below.
Until this page is signed off, the roster swap is reversible: the procedural library and the
MakeHuman head are still in the tree and still pass `audit`, `inspect`, `npc_proportions` and the
tests (`build --source procedural`).
A single-variant face prototype (`npc_casual`) landed 2026-11-03/04: `seg=24/rings=16` head,
subtle brow ridge, fuller lower lip and both eyelids. It reads cleaner than the 0.9.4 face and is
a candidate for the remaining seven once the owner approves (see the review doc).
## 1. Full-body roster (12 roles)
## What was captured
One tile per shipped character, neutral studio, camera at 4.2 m.
`src/test/capture_npc_baseline.gd` stages all eight library characters in a
neutral studio and captures six views per character:
![NPC roster full body](blender_previews/sheet_npc_roster_0.9.9.jpg)
| View | Camera | Purpose |
## 2. Front and profile per role
Front row set, then profile. Use this to judge the silhouette, the head size and the gear placement
without perspective hiding anything.
![NPC roster front and profile](blender_previews/sheet_npc_roster_views_0.9.9.jpg)
## 3. Animation poses (idle / sit / walk contact)
Six representative roles × three poses, driven by the real `_animate()` path with the FSM frozen.
Check: arms hang instead of holding an A-pose, the seated pose puts the forearms on the thighs and
keeps the shins down, the walk contact keeps limbs in contralateral opposition, and no limb
pinches or detaches at the shoulder, elbow, hip or knee.
![NPC roster poses](blender_previews/sheet_npc_roster_poses_0.9.9.jpg)
## 4. Crowd distance (8 m)
The distance the player actually sees a passer-by from the sidewalk.
![NPC roster at crowd distance](blender_previews/sheet_npc_roster_crowd_0.9.9.jpg)
## Signoff checklist
Tick each row. Anything you reject is a concrete task rather than an opinion, so it can go
straight into `TODO.md`.
| # | Question | Verdict |
| --- | --- | --- |
| `front` | 1.85 m, 32° FOV, head/upper body | face, hair, collar, shoulders |
| `three_quarter` | 1.85 m, yaw 35° | cheek/jaw, ear, profile transition |
| `profile` | 1.85 m, yaw 90° | head depth, nose, ear, chest silhouette |
| `back` | 1.85 m, yaw 180° | hair cap, neck, coat/backpack fit |
| `full_body` | 4.2 m, 38° FOV | proportions, hands, shoes, hem |
| `crowd` | 8.0 m, 45° FOV | silhouette read at normal crowd distance |
| 1 | Do the twelve characters read as distinct people at 8 m? | ☐ yes ☐ no |
| 2 | Is the head size right for the body (no bobblehead, no pinhead)? | ☐ yes ☐ no |
| 3 | Do the faces hold up at portrait distance? | ☐ yes ☐ no |
| 4 | Do the hands and the hand-held gear (camera, cup, clipboard, sign) read correctly? | ☐ yes ☐ no |
| 5 | Does the head gear (cap, hood, sunglasses, firefighter helmet) sit on the skull? | ☐ yes ☐ no |
| 6 | Are the role outfits believable for a Hollywood-boulevard shift (tourist, delivery, guard, responder)? | ☐ yes ☐ no |
| 7 | Are the seeded tint palettes acceptable, or too pastel/saturated? | ☐ yes ☐ no |
| 8 | Do the seated and walking poses hold up? | ☐ yes ☐ no |
The stage is deliberately separate from the venue review harness: a mid-grey
solid background, ground plane, one key light and one cool fill light, no HUD,
no crowd, no flashlight, no phase lighting and no animation. Front is Godot +Z,
matching `NpcAvatar.yaw_towards()`. The camera orbits at the recorded yaw rather
than rotating the avatar, so the pose and foot contact stay fixed between views.
The full command is:
**Signoff:** ☐ approved as shipped ☐ approved with the follow-ups below ☐ rejected
```bash
godot --path . --script res://src/test/capture_npc_baseline.gd -- \
--out=res://tmp/npc-baseline
python3 tools/npc_baseline.py sheet --dir tmp/npc-baseline
## Issues visible in the captures
Found while building this page; none blocks the swap, all are queued in `TODO.md`:
1. **`npc_dress` wears the performer cone hat plus the protest sign** because the performer props
are procedural and scaled for the old body. The dress itself has no geometry anymore — the role
is the `casual` base with a dress tint.
2. **`npc_coat` and `npc_hoodie`** are the hoodie base with a coat/hood tint; the raised hood and
the long coat silhouette are gone.
3. **The `npc_vest` clipboard** reads oversized at 0.23 × 0.31 m on the shorter Quaternius arm.
4. **The firefighter helmet brim** is 1.55× the head width — realistic for the helmet, but it reads
as a wide-brim hat next to the small head.
5. **Skin and cloth read pale** on the `hoodie` and `worker` bases; the seeded palette was tuned
against the procedural library's darker cloth.
6. **Roster variety is three meshes for twelve roles.** Tint and gear carry the difference; the
modular part library is the fix.
## Measured facts for this build
| Metric | Value |
| --- | --- |
| Characters | 12, all on the Quaternius 62-joint rig |
| Triangles per character | 4,458 (`worker` minus vest) … 6,424 (`casual`) |
| glTF vertices per character | 9,370 … 13,440 |
| Budget | 24,000 tris / 24,000 verts per character |
| Tint slots | `skin`, `cloth`, `pants`, `hair`, `accessory`, plus fixed detail |
| Texel density | 73–74 px/m on the 256² maps |
| Height | 1.817–1.881 m |
| 24-NPC frame budget | 160,608 tris, 0.84 ms average frame (budget 50 ms) |
| Gates | `inspect` clean, `audit` 12/12, `fit` OK, Godot 278/278, Python 167/167 |
## Reproducing the captures
```sh
godot --path . --script res://src/test/capture_npc_baseline.gd -- --out=res://tmp/npc-signoff
godot --path . --script res://src/test/capture_npc_pose.gd -- --out=res://tmp/npc-signoff-pose \
--pose=idle # repeat for sit and walk_contact
```
The 48 raw PNGs are local QA under `tmp/` (gitignored). The committed contact
sheet is [`blender_previews/sheet_npc_baseline.jpg`](blender_previews/sheet_npc_baseline.jpg).
For the P2 hand/grip check there is an optional eighth view, captured separately
so the 48-frame baseline stays stable:
## Superseded
```bash
godot --path . --script res://src/test/capture_npc_baseline.gd -- \
--out=res://tmp/npc-hands --view=hand
```
This page previously carried the **0.9.4 procedural library** baseline (eight hand-built
characters, the face/hand/neck fixes, the 13-core proportion audit). That mesh no longer ships, so
the text is retired; the comparison sheets stay in `blender_previews/` as history:
`sheet_npc_baseline_compare-p1.jpg`, `-body.jpg`, `-torso.jpg`, `-face-detail.jpg`,
`sheet_npc_hands_compare.jpg`, `sheet_npc_hands_grip_compare.jpg`.
That writes eight `*__hand.png` frames at 0.80 m from the right hand; the
before/after review image is
[`sheet_npc_hands_compare.jpg`](blender_previews/sheet_npc_hands_compare.jpg).
The capture is deterministic apart from engine rasterisation; re-running into
the same directory keeps the earlier files and writes `-vN` copies rather than
overwriting them.
## Acceptance criteria
The criteria below are for the stylized target, not photorealism. A change must
pass every applicable item before a library rebuild is accepted.
1. **Asset contract:** `npc_base` mesh node, the required bones, five tint slots,
front +Z, feet within 3 cm of y=0, 1.6–2.1 m height, at most 24,000 triangles
and 24,000 glTF vertices per character. The required 13 core bones stay fixed;
optional extra bones are allowed. Gear remains at most 1,500 triangles.
2. **Proportion and silhouette:** head, neck, shoulders, torso, hips, limbs and
feet read as one body; front/three-quarter/profile silhouettes agree with the
rest of the eight-character library; broad/slim/average build variation is
preserved.
3. **Face:** at 1.85 m the eyes, iris, brows, nose, mouth and ears are distinct;
the head volume has a readable jaw/cheek/chin transition; no face-plane seams,
inverted normals or z-fighting. The face direction must remain unambiguous.
4. **Hair and headgear:** hair covers the scalp without gaps or scalp-through
seams; fringes, buns and beards keep their silhouette. Caps, hoods,
sunglasses and the firefighter helmet clear the brow, eyes and ears. Headgear
may sit outside the 1.55–1.81 m face band, but its brim must not cover the
eyes.
5. **Hands and footwear:** at conversation distance the fingers and thumb read
as a hand, not a mitten; hand items contact the palm rather than floating;
shoes and boots are grounded with a readable toe/heel silhouette and preserve
the responder/firefighter footwear language.
6. **Clothing:** collars, cuffs, plackets, waistbands, pockets, badges and
reflective bands transition cleanly; no z-fighting or floating detail; coat,
dress and skirt hems do not show legs through the fabric during idle, walk,
block, sit or turn; the five tint slots and fixed detail colours remain
unchanged.
7. **Gear:** head gear clears the refined face/hair; the lanyard badge remains
readable in front of the jacket; backpack, shoulder bag and SCBA tank straps
lie outside the torso and do not read as a second bag; hand items use the
authored grip points.
8. **Crowd and performance:** the silhouette remains readable at the 8 m crowd
frame and the LOD/impostor tiers match the full mesh; the 24-NPC budget stays
within the existing 6.94 ms baseline (no regression beyond the measured
variance), and no new per-frame material or mesh churn is introduced.
Asset-space `fit` values are measurements of the untouched GLBs, not a pass/fail
result. A negative `x_clearance` means the gear's narrowest face is inside the
body reference; a negative `radial_clearance` means it extends beyond the body
silhouette (normal for a backpack, suspicious when it is a strap). Runtime
`NpcAvatar.GEAR_OFFSETS` is applied after attachment and is not part of the GLB
measurement.
## Proportion audit
Status: audited 2026-11-02. All eight library variants are inside the stylized
proportion guide; no geometry correction was required in this pass. The report
is repeatable from the exported GLBs:
```bash
python3 tools/npc_proportions.py report
python3 tools/npc_proportions.py report --json
```
### Method
`tools/npc_proportions.py` reads the GLB meshes and rest-pose joint hierarchy:
- height from the full mesh bounds;
- head height, width and aspect from `Head`/`skin` vertices (hair excluded so
long hairstyles do not change the head measurement);
- shoulder silhouette from vertices in the upper-chest band around the
shoulder joint;
- hip width from `HipL`/`HipR` pants vertices;
- torso length from the Spine joint to the shoulder joint;
- leg length from the Spine joint to the lowest vertex;
- arm length from the shoulder joint to the Hand joint;
- foot length from the forward extent of the KneeL/KneeR shoe vertices.
All ratios are divided by total standing height except `head_aspect`
(width / head height).
### Guide
These are stylized-but-credible bands for the game's low-poly adults, not
photoreal anatomy. The broad build is allowed a larger shoulder/hip silhouette
than the slim build.
| Ratio | Target | Notes |
| --- | ---: | --- |
| `head_height` | 0.16–0.22 | deliberately larger than a realistic 0.13–0.15 |
| `head_aspect` | 0.70–1.00 | width / height; rounded heads are acceptable |
| `shoulder_width` | 0.28–0.38 | silhouette includes sleeves and build scaling |
| `hip_width` | 0.17–0.23 | pants-only width, not skirt/coat flare |
| `torso_length` | 0.28–0.38 | Spine joint to shoulder |
| `leg_length` | 0.40–0.48 | Spine joint to ground |
| `arm_length` | 0.28–0.38 | shoulder to hand |
| `foot_length` | 0.12–0.18 | shoe forward extent |
### Result
| Character | Height | Head | Aspect | Shoulder | Hip | Torso | Leg | Arm | Foot | Result |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- |
| `npc_casual` | 1.824 m | 0.190 | 0.907 | 0.321 | 0.209 | 0.307 | 0.449 | 0.329 | 0.144 | PASS |
| `npc_jacket` | 1.824 m | 0.190 | 0.907 | 0.333 | 0.209 | 0.307 | 0.449 | 0.329 | 0.144 | PASS |
| `npc_coat` | 1.824 m | 0.190 | 0.907 | 0.328 | 0.209 | 0.307 | 0.449 | 0.329 | 0.144 | PASS |
| `npc_vest` | 1.819 m | 0.191 | 0.907 | 0.322 | 0.209 | 0.308 | 0.451 | 0.330 | 0.144 | PASS |
| `npc_dress` | 1.825 m | 0.190 | 0.907 | 0.289 | 0.191 | 0.307 | 0.449 | 0.329 | 0.144 | PASS |
| `npc_hoodie` | 1.856 m | 0.187 | 0.907 | 0.328 | 0.206 | 0.302 | 0.442 | 0.323 | 0.141 | PASS |
| `npc_firefighter` | 1.819 m | 0.191 | 0.907 | 0.358 | 0.223 | 0.308 | 0.451 | 0.330 | 0.148 | PASS |
| `npc_guard` | 1.819 m | 0.191 | 0.907 | 0.369 | 0.219 | 0.308 | 0.451 | 0.330 | 0.144 | PASS |
No variant is outside a band, so the body pass does not need a global skeleton
or mesh rescale. The values closest to a boundary are:
- **head height 0.190** against the 0.22 upper bound, and **head aspect 0.907**
against 1.00 — the head is deliberately large. Any head-size change must be
coordinated with the face/headgear pass and its gear-fit measurements.
- **guard shoulder 0.369** and **firefighter hip 0.223** against the broad-build
upper bounds — these are the intended uniform/turnout silhouettes, not
outliers.
- **leg length 0.449** and **torso 0.307** sit inside the guide for all variants;
the earlier short-leg impression came from the hip joint position, not the
visible pelvis line.
### Follow-up
The proportion audit does not replace the visual criteria in
[`npc-visual-review.md`](npc-visual-review.md): neck/shoulder/hip transitions,
face depth, hand grips, clothing hems and shoes still need the appearance pass.
Re-run the audit after any body mesh or skeleton change and record the new
ratios here.
## Baseline measurements
From `python3 tools/blender_assets.py fit` and the manifest:
| Character | Tris | Verts | Head width | Clothing radius | Notable gear clearances |
| --- | ---: | ---: | ---: | ---: | --- |
| `npc_casual` | 4,482 | 4,260 | 0.315 m | 0.191 m (hip) | backpack x −0.041, radial −0.162 |
| `npc_jacket` | 4,872 | 4,661 | 0.315 m | 0.191 m (hip) | lanyard x −0.116, radial +0.023 (runtime +4 cm forward) |
| `npc_coat` | 5,474 | 5,264 | 0.315 m | 0.225 m (skirt) | sunglasses width ratio 0.641; bag x +0.037, radial −0.130 |
| `npc_vest` | 4,980 | 4,879 | 0.315 m | 0.191 m (hip) | cap width ratio 1.067 |
| `npc_dress` | 4,900 | 4,719 | 0.315 m | 0.260 m (skirt) | bag x +0.002, radial −0.095 |
| `npc_hoodie` | 5,304 | 4,950 | 0.315 m | 0.190 m (hip) | — |
| `npc_firefighter` | 5,344 | 5,494 | 0.315 m | 0.255 m (skirt) | helmet width ratio 1.651; SCBA x −0.105, radial −0.076 |
| `npc_guard` | 5,046 | 5,096 | 0.315 m | 0.194 m (hip) | cap width ratio 1.067 |
`npc_coat` is widest at 5,474 triangles (well below the 16,000 cap);
`npc_firefighter` is closest on vertices at 5,494 (well below the 16,000 cap).
The refit backpack measures 380 triangles / 406 vertices.
## Baseline findings, ranked
### P1 — fix in the first model pass
1. **The tourist carried two bag silhouettes. Fixed 2026-11-02.** `_add_props()`
created a 0.30 × 0.36 × 0.14 box at `(-0.34, 1.15, -0.18)` for the tourist
photo pose while `_add_accessory("backpack")` also attached the library
backpack. From the three-quarter/back view the two read as separate bags. The
photo prop is now hidden outside `photo`/`film` poses
(`photo_prop_visible_for_pose()`), so it only appears when the character is
actually taking a picture; the after frame confirms one bag at rest.
2. **Face depth and detail. Fixed 2026-11-02.** The sclera was recessed into the
socket and wrapped with a thin upper lid; the separate jaw/cheek spheres were
replaced by a chin plate and cheekbones sculpted into the head mesh. The nose
now has ala wings and a smaller bridge/tip, the mouth is one rounded slot
instead of a flat bar, and the ears have a darker concha. That removes the
dark intersection rings and the moustache-like read at portrait distance; the
low-poly brows remain stylized.
3. **Backpack straps read as rigid black bars. Refit 2026-11-02.** The straps
were thin round tubes whose chest/waist points sat inside the torso, so only
fragments showed from the front. They are now flattened bands whose front path
sits outside the chest; the after sheet shows a continuous strap instead of
two detached rods. The backpack asset is now 380 triangles / 406 vertices
(still far below the 1,500-triangle gear budget).
### P2 — fix after the first P1 pass
4. **Hands: geometry and forward offset fixed 2026-11-02; finger curl still
open.** The palm is wider and shallower, and each finger is now a
two-segment bent tube with a rounded tip and a separate bent thumb. Hand gear
is authored around the wrist proxy, so the runtime now moves every hand item
`HAND_ITEM_FORWARD` (4 cm) into the open palm; the phone, flashlight, camera,
cup and clipboard are no longer buried inside the hand. The fingers are still
open, so a true wrap/thumb contact and the umbrella/sign grips remain to be
judged.
5. **Clothing detail is tile-driven and transitional seams are visible.** The
fabric pattern repeats strongly on the casual, coat and dress variants, and
collar/cuff/waist edges are abrupt. This is more visible than the shoe/hem
details in the current portraits.
6. **Shoes: fixed 2026-11-02.** Casual shoes gained rounded toe and heel caps;
responder/firefighter boots gained matching toe/heel volumes while keeping the
taller black rubber boot language. Both stay above the sole contact plane, and
the full-body comparison is `sheet_npc_baseline_compare-shoes.jpg`.
7. **Headgear scale and clearance. Checked 2026-11-02.** The cap and helmet
width ratios are 1.067 and 1.651; those are expected for headgear. Final-context
portraits confirm the cap, firefighter helmet, hood and sunglasses clear the
refined brow/eyes, and the helmet brim still reads as deliberate turnout gear.
No gear rebuild was needed.
8. **Performer props dominate the silhouette.** The red cone hat and the
primitive sign are intentionally large stage props, but they should be
checked during the pose pass so they do not read as head/body geometry.
### Accepted limits for the 0.9.4 release
- The hooded variants intentionally cover part of the face; face readability is
judged on the visible lower face and silhouette, not on hidden eyes.
- The low-poly style is the target. The acceptance bar is readability and clean
construction, not photoreal skin or hair.
- Held items use the forward-offset open palm (`HAND_ITEM_FORWARD`) rather than a
finger-curl rig. There are no finger bones in the contract; phone, cup,
clipboard, camera, flashlight, wand and radio were judged in the hand capture.
- Fabric folds are not separate geometry; the current torso, elbow and knee
shaping is the accepted stylized construction.
- The >26 m impostor remains a two-quad billboard; the LOD tier/hysteresis and
near/far swap are covered by `test_avatar_mesh_budget_and_lod_swap`.
- NPC fabric textures are 256² with a 4× UV repeat; cloth/pants skip the baked
per-character AO to avoid large portrait blotches. Skin, hair and gear keep AO.
- Evening readability is represented by the day/night/LATE final-context set;
LATE is the venue night variant.
## Before/after workflow
Keep the baseline directory, rebuild the same capture into a new directory after
the model changes, then produce a comparison sheet. Do not overwrite the
committed baseline image:
```bash
godot --path . --script res://src/test/capture_npc_baseline.gd -- \
--out=res://tmp/npc-baseline-after
python3 tools/npc_baseline.py compare \
--before tmp/npc-baseline --after tmp/npc-baseline-after \
--out tmp/npc-baseline-compare.jpg
```
The comparison sheet places the before/after pair for each view side by side.
The committed runs are
[`sheet_npc_baseline_compare-p1.jpg`](blender_previews/sheet_npc_baseline_compare-p1.jpg),
[`sheet_npc_baseline_compare-body.jpg`](blender_previews/sheet_npc_baseline_compare-body.jpg)
and [`sheet_npc_baseline_compare-face.jpg`](blender_previews/sheet_npc_baseline_compare-face.jpg).
The transition pass is [`sheet_npc_baseline_compare-torso.jpg`](blender_previews/sheet_npc_baseline_compare-torso.jpg).
The face-detail pass is [`sheet_npc_baseline_compare-face-detail.jpg`](blender_previews/sheet_npc_baseline_compare-face-detail.jpg).
The hand pass is [`sheet_npc_hands_compare.jpg`](blender_previews/sheet_npc_hands_compare.jpg);
the forward-grip pass is
[`sheet_npc_hands_grip_compare.jpg`](blender_previews/sheet_npc_hands_grip_compare.jpg).
The 0.9.5 texture pass adds
[`sheet_texture_refinement_0.9.5.jpg`](blender_previews/sheet_texture_refinement_0.9.5.jpg),
which pairs the close-range surface and cloth comparisons. Keep the baseline
sheet as the before artifact and add a new comparison sheet rather than
replacing it.
## Next implementation order
0.9.4 and the 0.9.5 texture pass are closed. The next visual work belongs to the
0.10.0 acceptance review; keep the NPC baseline/final-context commands as the
regression gate for any future character edit.
The current source evaluation and per-character build table live 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);
the face options that led to the full swap are in
[`screens/npc-face-gate/`](screens/npc-face-gate/README.md).