Files
security-game/docs/npc-visual-review.md
T
zwitschi 4bc6390e9f
Full build / Import + headless tests (push) Successful in 46s
Full build / Full desktop build + optional release (push) Successful in 4m54s
Release 0.9.5 texture and material refinement
2026-11-02 21:04:48 +01:00

14 KiB
Raw Permalink Blame History

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, sheet_npc_baseline_compare-body.jpg, the latest sheet_npc_baseline_compare-torso.jpg, the face-detail pass sheet_npc_baseline_compare-face-detail.jpg, the conversation-distance hand pass sheet_npc_hands_compare.jpg, and the forward-grip pass sheet_npc_hands_grip_compare.jpg.

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:

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. For the P2 hand/grip check there is an optional eighth view, captured separately so the 48-frame baseline stays stable:

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. 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 8,000 triangles and 8,000 glTF vertices per library character. The frozen npc_base fallback keeps 6,000 triangles / 4,000 vertices. 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.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

The separate npc-proportion-audit.md measures height, head size, shoulder/hip width, torso, leg, arm and foot ratios for every variant with tools/npc_proportions.py. All eight pass the stylized guide; the head and broad-build shoulders sit nearest their upper bounds. No skeleton or mesh proportion change was needed in this pass.

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 (2,526 below the 8,000 cap); npc_firefighter is closest on vertices at 5,494 (2,506 below the 8,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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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:

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, sheet_npc_baseline_compare-body.jpg and sheet_npc_baseline_compare-face.jpg. The transition pass is sheet_npc_baseline_compare-torso.jpg. The face-detail pass is sheet_npc_baseline_compare-face-detail.jpg. The hand pass is sheet_npc_hands_compare.jpg; the forward-grip pass is sheet_npc_hands_grip_compare.jpg. The 0.9.5 texture pass adds 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.