270 lines
17 KiB
Markdown
270 lines
17 KiB
Markdown
# 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 | accept (2026-11-03 flared base collar) |
|
||
| `bench` | 560 | — |
|
||
| `trash_bin` | 228 | — |
|
||
| `planter_box` | 1,324 | — |
|
||
| `newspaper_box` | 300 | accept (2026-11-03 recessed slot + door seam) |
|
||
| `fire_hydrant` | 610 | accept (2026-11-03 rebuild: bolted flange, bonnet cap nut, larger side + front outlets) |
|
||
| `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 | — |
|
||
|
||
Vision review 2026-11-03 (`tools/describe_image.py` on the prop gallery): `bench` and `trash_bin`
|
||
are ship-ready (minor chamfer polish only); `bollard` passes but leans on its reflective band
|
||
(add a base collar); `planter_box`/`round_planter` fail on the foliage — the `_shrub()` clumps
|
||
read as rocks, not plants (same foliage language gap, gated on the owner); `newspaper_box` is
|
||
borderline (the slot is a flat decal, wants a recessed inset + door seam); `fire_hydrant` is now
|
||
rebuilt (see row). No prop floats or sinks; shadow direction is consistent.
|
||
|
||
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 | 25,828 tris / 518 meshes | facade review landed (white signature band + tied-back curtains); shape-language gaps ranked |
|
||
| 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.
|