Files
security-game/docs/3d-model-review.md
T
zwitschi a677a95d13
Full build / Import + headless tests (push) Successful in 40s
Full build / Full desktop build + optional release (push) Skipped
Narrow the pose-harness root cause: skinned mesh ignores its Rig parent
2026-11-03 19:26:07 +01:00

245 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).
### 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.
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.