# NPC visual baseline and acceptance criteria Status: baseline captured 2026-11-02. No character GLB, texture, gear or runtime mesh was changed in this step. The baseline is the reference for the 0.9.4 NPC visual optimization. ## 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). 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 6,000 triangles and 5,000 glTF vertices per character. 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, beanies, 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.93 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. ## Baseline measurements From `python3 tools/blender_assets.py fit` and the manifest: | Character | Tris | Verts | Head width | Clothing radius | Notable gear clearances | | --- | ---: | ---: | ---: | ---: | --- | | `npc_casual` | 3,974 | 3,744 | 0.315 m | 0.191 m (hip) | backpack x −0.041, radial −0.162 | | `npc_jacket` | 4,394 | 4,175 | 0.315 m | 0.191 m (hip) | lanyard x −0.116, radial +0.023 (runtime +4 cm forward) | | `npc_coat` | 5,026 | 4,807 | 0.315 m | 0.225 m (skirt) | sunglasses width ratio 0.641; bag x +0.037, radial −0.130 | | `npc_vest` | 4,474 | 4,340 | 0.315 m | 0.191 m (hip) | cap width ratio 1.067 | | `npc_dress` | 4,428 | 4,026 | 0.315 m | 0.260 m (skirt) | bag x +0.002, radial −0.095 | | `npc_hoodie` | 4,826 | 4,504 | 0.315 m | 0.190 m (hip) | — | | `npc_firefighter` | 4,848 | 4,735 | 0.315 m | 0.255 m (skirt) | helmet width ratio 1.651; SCBA x −0.105, radial −0.076 | | `npc_guard` | 4,560 | 4,529 | 0.315 m | 0.194 m (hip) | cap width ratio 1.067 | The widest character is `npc_coat` at 5,026 triangles and 4,807 vertices, so the current headroom is 974 triangles and 193 vertices per character. Any face or hand refinement must come out of that budget or replace existing detail. ## Baseline findings, ranked ### P1 — fix in the first model pass 1. **The tourist carries two bag silhouettes.** `_add_props()` creates 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 attaches the library backpack. From the three-quarter/back view the two read as separate bags. The photo prop should be hidden outside the photo pose, merged into the backpack, or moved to a clearly held camera. 2. **The face is the largest portrait gap.** The eyes, nose and lips are readable but sit on a shallow, faceted head: the eye whites protrude, the eyelid/cheek transition is missing, and the mouth is a dark slot. This is intentionally stylized, but the head volume and eye socket are the first things a player sees at conversation range. 3. **Backpack straps read as rigid black bars.** The back gear's straps cross the chest as thin, straight geometry with visible gaps from the shoulders, not as fabric sitting on the torso. A player sees this on the most common tourist silhouette in every crowd. ### P2 — fix after the first P1 pass 4. **Hands and grips are not proven at conversation distance.** The full-body frame shows small block hands; the camera, phone and clipboard need a close-up grip check before the hand mesh is accepted. 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 are simple blocks.** They are grounded, but the toe/heel silhouette is minimal; the responder and firefighter footwear should remain distinct after any refinement. 7. **Headgear scale needs a visual, not just numeric, check.** The cap and helmet width ratios are 1.067 and 1.651; those are expected for headgear, but the brim/ear clearance and the helmet's relationship to the head still need a face-on check. 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 or deferred for this baseline - 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. - Hem-through-leg clipping during a walk cannot be judged from a rest pose; it stays in the body/clothing workstream. - The 8 m `crowd` view is still LOD 0. The separate LOD/impostor check remains required; this baseline does not claim to validate it. ## 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. If the change is accepted, copy the after sheet to a new committed name (`sheet_npc_baseline_after.jpg`) and link both from the preview index. The baseline is not replaced. ## Next implementation order 1. Remove the duplicate tourist bag silhouette and refit the backpack straps. 2. Refine the head/eye socket/jaw volume without changing the face direction or the five tint slots. 3. Add a conversation-distance hand/grip capture and fix hand geometry before judging hand items. 4. Audit hems and footwear in walk/sit/block poses. 5. Re-run the 48-frame baseline, the 96-frame final-context review, the 24-NPC budget and `blender_assets.py audit/fit`; publish the before/after sheet.