# 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-visual-review.md`](npc-visual-review.md) (accepted 0.9.4 limits and before/after sheets), [`npc-proportion-audit.md`](npc-proportion-audit.md) (proportion guide), [`blender-assets.md`](blender-assets.md) (asset contract), [`asset-tooling.md`](asset-tooling.md) (commands). ## 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. The skills assume Blender MCP, which this project does not use: everything below came from the headless `tools/blender_assets.py` / `tools/npc_pipeline.py` pipelines plus the Godot capture harnesses. ## 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, 131,616 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` | 4,482 / 4,260 | 1.824 / +0.000 | 7 | 106.8–122.2 | 429–440 | Contract pass; visual accepted 0.9.4 | | `npc_jacket` | 4,872 / 4,662 | 1.824 / +0.000 | 7 | 100.3–113.7 | 409–413 | Contract pass; visual accepted 0.9.4 | | `npc_coat` | 5,474 / 5,265 | 1.824 / +0.000 | 7 | 75.3–81.4 | 307–310 | Contract pass; visual accepted 0.9.4 | | `npc_vest` | 4,980 / 4,879 | 1.819 / +0.000 | 9 | 85.5–99.3 | 362–366 | Contract pass; visual accepted 0.9.4 | | `npc_dress` | 4,900 / 4,719 | 1.825 / +0.000 | 7 | 81.9–89.0 | 338–339 | Contract pass; visual accepted 0.9.4 | | `npc_hoodie` | 5,304 / 4,950 | 1.856 / +0.000 | 7 | 89.5–98.6 | 359–364 | Contract pass; visual accepted 0.9.4 | | `npc_firefighter` | 5,344 / 5,494 | 1.819 / +0.000 | 9 | 64.0–75.1 | 287–290 | Contract pass; lowest density | | `npc_guard` | 5,046 / 5,096 | 1.819 / +0.000 | 11 | 90.0–100.8 | 374–374 | Contract pass; visual accepted 0.9.4 | | `npc_base` (fallback) | 1,204 / 2,488 | 1.836 / +0.000 | 5 | 70.1–72.9 | 4× on fabric | Contract pass; far below its 6,000/4,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 8,000/8,000 with the farthest at 5,474 tris (68% of budget) 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. ## 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 evidence blocked: see "Pose evidence is blocked" below)* | | | | | | | | | `npc_casual` | accept (0.9.4) | P1 (faceted planes) | accept (open palm) | blocked | accept | accept (two-quad impostor) | pending owner | | `npc_jacket` | accept | P1 | accept | blocked | accept | accept | pending owner | | `npc_coat` | accept | P1 | accept | blocked | accept (hem 0.76 stride) | accept | pending owner | | `npc_vest` | accept | P1 | accept | blocked | accept | accept | pending owner | | `npc_dress` | accept; performer props dominate | P1 | accept | blocked | accept | accept | pending owner | | `npc_hoodie` | accept (hooded criterion) | accept (lower face only) | accept | blocked | accept | accept | pending owner | | `npc_firefighter` | accept | P1 | accept | blocked | accept | accept | pending owner | | `npc_guard` | accept | P1 | accept | blocked | accept | accept | pending owner | ### Gear (18) | Gear | Attach | Tris | Verdict | | --- | --- | --- | --- | | `cap` | head | 328 | — (fit measured, visual pending) | | `beanie` | head | 460 | — (unused by any character) | | `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). `beanie` is unused and remains a retire/reassign candidate. ### 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 | — | | `bench` | 560 | — | | `trash_bin` | 228 | — | | `planter_box` | 1,324 | — | | `newspaper_box` | 244 | — | | `fire_hydrant` | 466 | — | | `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 | — | 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 | 22,680 tris / 454 meshes | shape-language review pending | | TV Studio | — | shape-language review pending | ## Pose evidence is blocked (2026-11-03) The weights/pose column cannot be filled yet. The pose logic is verified; the *capture* is not. `src/test/capture_npc_pose.gd` stages idle / walk-contact / walk-pass / seated for all eight variants (full-body + shoulder views) and its output does not match the state it reports. | 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. 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; `beanie` is unused and could be retired or reassigned. 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. ## 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 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.