# NPC visual baseline and acceptance criteria 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). 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. 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). ## What was captured `src/test/capture_npc_baseline.gd` stages all eight library characters in a neutral studio and captures six views per character: | View | Camera | Purpose | | --- | --- | --- | | `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 | 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: ```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 ``` 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: ```bash godot --path . --script res://src/test/capture_npc_baseline.gd -- \ --out=res://tmp/npc-hands --view=hand ``` 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.