Smooth the NPC neck and shoulder transition
Full build / Import + headless tests (push) Successful in 39s
Full build / Full desktop build + optional release (push) Skipped

This commit is contained in:
zwitschi committed 2026-11-02 19:03:38 +01:00
1 parent ab9c67d266
commit 15ab8c686d
29 files changed
+85 -53

No files matched your search

+2
View File
@@ -8,6 +8,8 @@ Semantic Versioning.
### Fixed
- Character necks are fuller and shoulders slope from the neck instead of
reading as a thin post on a hanger.
- Tourists no longer appear to carry two backpacks: the photo-pose camera bag is
hidden except while they are actually taking a picture.
- Backpack shoulder straps are now flattened bands that sit outside the chest
+5
View File
@@ -76,6 +76,11 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
stylized guide in `docs/npc-proportion-audit.md`; head ~0.190 (upper 0.22) and broad-build
shoulders ~0.370/hip ~0.225 are near the upper bounds, not outliers. No skeleton/mesh rescale was
needed; run the report after any body change.
- **NPC neck/shoulder transition (0.9.4, 2026-11-02):** the body graph uses a fuller neck base
(1.43 m, radius 0.074/0.072), a taller/thicker neck top and a shoulder root lowered from 1.39 to
1.37 m. A final triangulate + tiny-face delete in `build_character` removes zero-area triangles
that the new neck branch exposed in jacket/hoodie. All eight GLBs and AO maps were rebuilt;
`npc_proportions.py` still passes and `sheet_npc_baseline_compare-body.jpg` is the review image.
- **UI flow smoke (2026-11-02):** `smoke_ui_flows.gd` boots the real scene and
exercises inspection allow/deny, normal vs emergency radio, Help filtering,
wand allow/deny and review return-to-menu. Keep this smoke in the 0.9.6
+5 -2
View File
@@ -86,8 +86,11 @@ and equipment fit.
torso length, hip width, limb length, foot length) and correct the worst outliers.
(`tools/npc_proportions.py` + `docs/npc-proportion-audit.md`: all eight pass; no skeleton or
mesh rescue was needed. Head and broad-build silhouettes are nearest their upper bounds.)
- [ ] Improve neck, shoulder, torso and hip transitions so the silhouette reads as one body rather
than stacked primitives; preserve the existing broad/slim/average build variations.
- [x] Improve the neck and shoulder transition so the silhouette reads as one body rather than a
thin post on a hanger. (Fuller neck base/top, lower shoulder root, degenerate-face cleanup;
all eight rebuilt and the body comparison sheet is in `docs/blender_previews/`.)
- [ ] Improve the torso and hip transition; preserve the existing broad/slim/average build
variations.
- [ ] Add subtle body-shape variation between variants beyond uniform scaling (shoulder slope,
torso taper, hip width, limb thickness) while staying inside the triangle/vertex budget.
- [ ] Check the procedural fallback and `npc_base` silhouette against the library so the fallback
+21 -21
View File
@@ -28,12 +28,12 @@
"attach": "back",
"bounds_godot": {
"max": [
0.15,
0.297,
0.214
0.161,
0.296,
0.212
],
"min": [
-0.15,
-0.161,
-0.12,
-0.319
]
@@ -42,11 +42,11 @@
"materials": [
"pack_nylon",
"steel",
"plastic_black"
"denim_bag"
],
"path": "assets/models/blender/npc_gear/backpack.glb",
"triangles": 380,
"vertices": 406
"vertices": 408
},
"beanie": {
"attach": "head",
@@ -487,8 +487,8 @@
"accessory",
"hair"
],
"triangles": 3974,
"vertices": 3744
"triangles": 3984,
"vertices": 3793
},
"npc_coat": {
"bounds_godot": {
@@ -528,8 +528,8 @@
"accessory",
"hair"
],
"triangles": 5026,
"vertices": 4807
"triangles": 4978,
"vertices": 4776
},
"npc_dress": {
"bounds_godot": {
@@ -567,8 +567,8 @@
"accessory",
"hair"
],
"triangles": 4428,
"vertices": 4026
"triangles": 4400,
"vertices": 4075
},
"npc_firefighter": {
"bounds_godot": {
@@ -609,7 +609,7 @@
"hair"
],
"triangles": 4848,
"vertices": 4735
"vertices": 4831
},
"npc_guard": {
"bounds_godot": {
@@ -652,8 +652,8 @@
"accessory",
"hair"
],
"triangles": 4560,
"vertices": 4529
"triangles": 4558,
"vertices": 4652
},
"npc_hoodie": {
"bounds_godot": {
@@ -693,8 +693,8 @@
"accessory",
"hair"
],
"triangles": 4826,
"vertices": 4504
"triangles": 4816,
"vertices": 4425
},
"npc_jacket": {
"bounds_godot": {
@@ -734,8 +734,8 @@
"accessory",
"hair"
],
"triangles": 4394,
"vertices": 4175
"triangles": 4384,
"vertices": 4130
},
"npc_vest": {
"bounds_godot": {
@@ -775,8 +775,8 @@
"accessory",
"hair"
],
"triangles": 4474,
"vertices": 4340
"triangles": 4496,
"vertices": 4271
},
"palm_tree": {
"bounds_godot": {
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 34 KiB

+2 -2
View File
@@ -94,8 +94,8 @@ All eight use the `npc_base` contract, so each one can replace
`Materials.npc_slot()` tints them the same way it tints `npc_base`.
- budget ≤ 6000 triangles and ≤ 5000 vertices (glTF vertex count, after seam
splits; `CHAR_MAX_VERTS` in `tools/blender_assets.py`, raised from 4000 for
the 2026-11-01 face/hand pass). The current range is 3,974–5,026 triangles
and 3,744–4,807 vertices.
the 2026-11-01 face/hand pass). The current range is 3,984–4,978 triangles
and 3,793–4,831 vertices.
Construction: the torso and limbs are one continuous Skin-modifier mesh,
subdivided twice, decimated to 26 % with X symmetry, and cut at the waist, cuffs,
+1
View File
@@ -45,6 +45,7 @@ Committed contact sheets and local Blender renders for the character, gear and p
| <img src="sheet_npc.jpg" width="280" alt="sheet_npc.jpg"> | [`sheet_npc.jpg`](sheet_npc.jpg) |
| <img src="sheet_npc_baseline.jpg" width="280" alt="sheet_npc_baseline.jpg"> | [`sheet_npc_baseline.jpg`](sheet_npc_baseline.jpg) |
| <img src="sheet_npc_baseline_compare-p1.jpg" width="280" alt="sheet_npc_baseline_compare-p1.jpg"> | [`sheet_npc_baseline_compare-p1.jpg`](sheet_npc_baseline_compare-p1.jpg) |
| <img src="sheet_npc_baseline_compare-body.jpg" width="280" alt="sheet_npc_baseline_compare-body.jpg"> | [`sheet_npc_baseline_compare-body.jpg`](sheet_npc_baseline_compare-body.jpg) |
| <img src="sheet_npc_gear.jpg" width="280" alt="sheet_npc_gear.jpg"> | [`sheet_npc_gear.jpg`](sheet_npc_gear.jpg) |
| <img src="sheet_props.jpg" width="280" alt="sheet_props.jpg"> | [`sheet_props.jpg`](sheet_props.jpg) |
| <img src="shoulder_bag.png" width="280" alt="shoulder_bag.png"> | [`shoulder_bag.png`](shoulder_bag.png) |
Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

+1
View File
@@ -39,6 +39,7 @@ Implemented so far:
- Aim-free selection pass (v0.3.0 work, [ADR-0011](adr/0011-aim-free-selection-and-precision-walk.md)): the focus cone ranks candidates by angle then distance, `Tab` and the mouse wheel cycle nearby people nearest-first and pin the selection, right click clears focus with a short re-acquire grace, the HUD reports how many people are in the ring, and holding `Shift` walks at half speed for close work
- Visual alignment: start/venue menu, pause, settings, shift review, radio, wand, inspection, timeline, HUD target chip/escalation bar, and radial menu now use or match their AI references in `assets/images/ui/`. The runtime art loader (`src/game/ui/art.gd`) falls back cleanly when a file is missing. The wand panel lists the guest's carried items, and multiline HUD/wand strings render real line breaks.
- NPC neck/shoulder transition (0.9.4 work): the body graph now uses a fuller neck base, a slightly tapered neck top and a lower shoulder root, giving the head/neck/shoulder line a trapezius slope instead of a thin post. The full character library was rebuilt with a final degenerate-face cleanup; the baseline comparison is `sheet_npc_baseline_compare-body.jpg`.
- NPC proportion audit (0.9.4 work): `tools/npc_proportions.py` measures all eight GLBs against a stylized height/head/shoulder/hip/torso/leg/arm/foot guide. All variants pass; the head and broad-build silhouettes are the nearest upper bounds. Results are in [`npc-proportion-audit.md`](npc-proportion-audit.md).
- NPC P1 portrait fixes (0.9.4 work): tourists now hide their photo-pose camera bag outside `photo`/`film` poses, removing the duplicate-backpack silhouette; the backpack gear was rebuilt with flattened, forward chest straps and a fresh AO map. The before/after sheet is [`blender_previews/sheet_npc_baseline_compare-p1.jpg`](blender_previews/sheet_npc_baseline_compare-p1.jpg); the face and remaining clothing/hand work stay open.
- NPC visual baseline (0.9.4 work): `src/test/capture_npc_baseline.gd` captures all eight library characters from front, three-quarter, profile, back, full-body and crowd views in a neutral studio (48 frames). The criteria, measurements and ranked gaps are in [`npc-visual-review.md`](npc-visual-review.md); the committed sheet is [`blender_previews/sheet_npc_baseline.jpg`](blender_previews/sheet_npc_baseline.jpg). No model changes were made in this baseline step.
+9 -9
View File
@@ -48,14 +48,14 @@ than the slim build.
| Character | Height | Head | Aspect | Shoulder | Hip | Torso | Leg | Arm | Foot | Result |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- |
| `npc_casual` | 1.824 m | 0.190 | 0.907 | 0.320 | 0.209 | 0.307 | 0.449 | 0.329 | 0.144 | PASS |
| `npc_jacket` | 1.824 m | 0.190 | 0.907 | 0.325 | 0.209 | 0.307 | 0.449 | 0.329 | 0.144 | PASS |
| `npc_coat` | 1.824 m | 0.190 | 0.907 | 0.329 | 0.209 | 0.307 | 0.449 | 0.329 | 0.144 | PASS |
| `npc_vest` | 1.819 m | 0.191 | 0.907 | 0.321 | 0.210 | 0.308 | 0.451 | 0.330 | 0.144 | PASS |
| `npc_dress` | 1.825 m | 0.190 | 0.907 | 0.287 | 0.191 | 0.307 | 0.449 | 0.329 | 0.144 | PASS |
| `npc_hoodie` | 1.856 m | 0.187 | 0.907 | 0.320 | 0.205 | 0.302 | 0.442 | 0.323 | 0.141 | PASS |
| `npc_firefighter` | 1.819 m | 0.191 | 0.907 | 0.356 | 0.225 | 0.308 | 0.451 | 0.330 | 0.148 | PASS |
| `npc_guard` | 1.819 m | 0.191 | 0.907 | 0.370 | 0.219 | 0.308 | 0.451 | 0.330 | 0.144 | PASS |
| `npc_casual` | 1.824 m | 0.190 | 0.907 | 0.321 | 0.209 | 0.307 | 0.449 | 0.329 | 0.144 | PASS |
| `npc_jacket` | 1.824 m | 0.190 | 0.907 | 0.333 | 0.209 | 0.307 | 0.449 | 0.329 | 0.144 | PASS |
| `npc_coat` | 1.824 m | 0.190 | 0.907 | 0.334 | 0.209 | 0.307 | 0.449 | 0.329 | 0.144 | PASS |
| `npc_vest` | 1.819 m | 0.191 | 0.907 | 0.322 | 0.210 | 0.308 | 0.451 | 0.330 | 0.144 | PASS |
| `npc_dress` | 1.825 m | 0.190 | 0.907 | 0.289 | 0.191 | 0.307 | 0.449 | 0.329 | 0.144 | PASS |
| `npc_hoodie` | 1.856 m | 0.187 | 0.907 | 0.328 | 0.205 | 0.302 | 0.442 | 0.323 | 0.141 | PASS |
| `npc_firefighter` | 1.819 m | 0.191 | 0.907 | 0.358 | 0.225 | 0.308 | 0.451 | 0.330 | 0.148 | PASS |
| `npc_guard` | 1.819 m | 0.191 | 0.907 | 0.369 | 0.219 | 0.308 | 0.451 | 0.330 | 0.144 | PASS |
No variant is outside a band, so the body pass does not need a global skeleton
or mesh rescale. The values closest to a boundary are:
@@ -63,7 +63,7 @@ or mesh rescale. The values closest to a boundary are:
- **head height 0.190** against the 0.22 upper bound, and **head aspect 0.907**
against 1.00 — the head is deliberately large. Any head-size change must be
coordinated with the face/headgear pass and its gear-fit measurements.
- **guard shoulder 0.370** and **firefighter hip 0.225** against the broad-build
- **guard shoulder 0.369** and **firefighter hip 0.225** against the broad-build
upper bounds — these are the intended uniform/turnout silhouettes, not
outliers.
- **leg length 0.449** and **torso 0.307** sit inside the guide for all variants;
+11 -11
View File
@@ -98,17 +98,17 @@ From `python3 tools/blender_assets.py fit` and the manifest:
| Character | Tris | Verts | Head width | Clothing radius | Notable gear clearances |
| --- | ---: | ---: | ---: | ---: | --- |
| `npc_casual` | 3,974 | 3,744 | 0.315 m | 0.191 m (hip) | backpack x −0.041, radial −0.162 |
| `npc_jacket` | 4,394 | 4,175 | 0.315 m | 0.191 m (hip) | lanyard x −0.116, radial +0.023 (runtime +4 cm forward) |
| `npc_coat` | 5,026 | 4,807 | 0.315 m | 0.225 m (skirt) | sunglasses width ratio 0.641; bag x +0.037, radial −0.130 |
| `npc_vest` | 4,474 | 4,340 | 0.315 m | 0.191 m (hip) | cap width ratio 1.067 |
| `npc_dress` | 4,428 | 4,026 | 0.315 m | 0.260 m (skirt) | bag x +0.002, radial −0.095 |
| `npc_hoodie` | 4,826 | 4,504 | 0.315 m | 0.190 m (hip) | — |
| `npc_firefighter` | 4,848 | 4,735 | 0.315 m | 0.255 m (skirt) | helmet width ratio 1.651; SCBA x −0.105, radial −0.076 |
| `npc_guard` | 4,560 | 4,529 | 0.315 m | 0.194 m (hip) | cap width ratio 1.067 |
| `npc_casual` | 3,984 | 3,793 | 0.315 m | 0.191 m (hip) | backpack x −0.041, radial −0.162 |
| `npc_jacket` | 4,384 | 4,130 | 0.315 m | 0.191 m (hip) | lanyard x −0.116, radial +0.023 (runtime +4 cm forward) |
| `npc_coat` | 4,978 | 4,776 | 0.315 m | 0.225 m (skirt) | sunglasses width ratio 0.641; bag x +0.037, radial −0.130 |
| `npc_vest` | 4,496 | 4,271 | 0.315 m | 0.191 m (hip) | cap width ratio 1.067 |
| `npc_dress` | 4,400 | 4,075 | 0.315 m | 0.260 m (skirt) | bag x +0.002, radial −0.095 |
| `npc_hoodie` | 4,816 | 4,425 | 0.315 m | 0.190 m (hip) | — |
| `npc_firefighter` | 4,848 | 4,831 | 0.315 m | 0.255 m (skirt) | helmet width ratio 1.651; SCBA x −0.105, radial −0.076 |
| `npc_guard` | 4,558 | 4,652 | 0.315 m | 0.194 m (hip) | cap width ratio 1.067 |
The widest character is `npc_coat` at 5,026 triangles and 4,807 vertices, so
the current headroom is 974 triangles and 193 vertices per character. Any face
The widest character is `npc_firefighter` at 4,848 triangles and 4,831 vertices, so
the current headroom is 1,152 triangles and 169 vertices per character. Any face
or hand refinement must come out of that budget or replace existing detail. The
refit backpack measures 380 triangles / 406 vertices; the character and gear
table above remains valid because the character meshes did not change.
@@ -183,7 +183,7 @@ python3 tools/npc_baseline.py compare \
The comparison sheet places the before/after pair for each view side by side.
The first run is committed as
[`sheet_npc_baseline_compare-p1.jpg`](blender_previews/sheet_npc_baseline_compare-p1.jpg);
[`sheet_npc_baseline_compare-p1.jpg` and `sheet_npc_baseline_compare-body.jpg`](blender_previews/sheet_npc_baseline_compare-p1.jpg);
keep the baseline sheet as the before artifact and add a new numbered/after
sheet rather than replacing it.
+28 -8
View File
@@ -535,15 +535,18 @@ def _skin_body(spec):
((0, 0, 0.96), 0.150 * w * bulk, 0.105 * d * bulk), # 1 waist
((0, 0.0, 1.18), 0.170 * w * bulk, 0.115 * d * bulk), # 2 chest
((0, 0.0, 1.355), 0.172 * w * bulk, 0.108 * d * bulk), # 3 upper chest
((0, 0, 1.44), 0.058, 0.058), # 4 neck base
((0, -0.005, 1.53), 0.052, 0.052), # 5 neck top
# A slightly fuller neck base and a lower shoulder root give the
# head/neck/shoulder line a trapezius slope instead of a thin post on
# a hanger. The head and collar still sit above this band.
((0, 0, 1.43), 0.074, 0.072), # 4 neck base
((0, -0.005, 1.53), 0.060, 0.060), # 5 neck top
]
edges = [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]
for s in (-1.0, 1.0):
base = len(nodes)
nodes += [
((0.20 * s * w, 0, 1.39), 0.075 * t * sleeve, 0.07 * t * sleeve), # shoulder
((0.27 * s * w, 0, 1.24), 0.058 * t * sleeve, 0.058 * t * sleeve), # upper arm
((0.20 * s * w, 0, 1.37), 0.080 * t * sleeve, 0.074 * t * sleeve), # shoulder
((0.27 * s * w, 0, 1.23), 0.058 * t * sleeve, 0.058 * t * sleeve), # upper arm
((0.275 * s * w, 0.005, 1.06), 0.046 * t * sleeve, 0.048 * t * sleeve), # elbow
((0.275 * s * w, -0.01, 0.80), 0.034 * t, 0.030 * t), # wrist
]
@@ -929,6 +932,23 @@ def build_character(name):
rest.append(cl)
mesh = join(rest, "npc_base")
# The fuller neck/shoulder skin graph can leave a few collinear triangles
# after the decimate step. Remove them before UV/export; Godot warns about
# zero-area faces during LOD generation and `audit` rejects them.
bm = bmesh.new()
bm.from_mesh(mesh.data)
bmesh.ops.dissolve_degenerate(bm, dist=2e-4, edges=bm.edges)
# Skin-modifier branch cleanup can still leave a handful of zero-area
# faces; Godot's LOD generator treats them as malformed. Triangulate first
# because a degenerate quad can become a zero-area triangle on export, then
# drop the tiny faces explicitly.
bmesh.ops.triangulate(bm, faces=bm.faces[:], quad_method="BEAUTY",
ngon_method="BEAUTY")
bad = [face for face in bm.faces if face.calc_area() < 1e-10]
if bad:
bmesh.ops.delete(bm, geom=bad, context="FACES_ONLY")
bm.to_mesh(mesh.data)
bm.free()
# Every contract slot must exist even if a variant does not paint with it.
present = {m.name for m in mesh.data.materials if m}
for slot in SLOTS:
@@ -1019,10 +1039,10 @@ def build_gear(name):
# torso front (Godot z≈0.21). The old chest/waist points sat at
# z≈0.13 inside the body, so only thin tube fragments read as two
# rigid black bars from the front.
p.append(tube("Strap", [(0.09 * s, 0.13, 0.27), (0.10 * s, -0.02, 0.29),
(0.105 * s, -0.06, 0.25),
(0.11 * s, -0.19, 0.18), (0.12 * s, -0.19, -0.08)],
0.018, M("plastic_black"), seg=8, width_scale=1.35,
p.append(tube("Strap", [(0.09 * s, 0.13, 0.27), (0.11 * s, -0.02, 0.29),
(0.12 * s, -0.06, 0.25),
(0.14 * s, -0.19, 0.18), (0.145 * s, -0.19, -0.08)],
0.016, M("denim_bag"), seg=8, width_scale=1.35,
thickness_scale=0.42))
elif name == "shoulder_bag":
# Keep the bag's inner face outside the widest skirt flare (dress = 0.26):