17 KiB
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. 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 (accepted 0.9.4 limits and
before/after sheets), npc-proportion-audit.md (proportion guide),
blender-assets.md (asset contract), 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_casualis 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,accessoryand 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 |
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 | — |
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):
- Create a
SubViewport(1280x720,own_world_3d = true,UPDATE_ALWAYS) in_initialize. - Build the studio (environment, key/fill light, ground, camera) and one avatar inside it.
- Wait 2-3 frames so the skinned mesh binds.
- Apply the pose with
_animate()ticks, with the avatar's process/physics disabled. - Call
RenderingServer.force_draw(), thenviewport.get_texture().get_image(). - 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.
-
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/retopologyguidance 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 intools/npc_pipeline.pyis the frozennpc_basefallback only). So this gap is a readability/scale question on the existing sculpt — the head isseg=18/rings=12with small offsets — not missing geometry. Concrete next step, if funded: a single-variantnpc_casualprototype that raises head subdivision and deepens the cheek/brow offsets, then re-bakes AO and captures the portrait before touching the other seven. -
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.
-
Foliage language (P1,
street_tree,palm_tree,planter_box,round_planter). Flat-shaded clumps against an otherwise readable set, already deferred once.vegetation-artistcarded clusters would fix it inside the existing triangle budget. -
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/hairto 512² for portrait distance. -
npc_firefighterdensity 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. -
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. -
Gear never reviewed as silhouette language (P2). 18 pieces pass fit/contract but have no prop-artist verdict;
beanieis unused and could be retired or reassigned. -
Venue shape language (P2). Hollywood and the Joke Shop have budget data but no
environment-artist/archvizreview; 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 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.