Refine the NPC nose, mouth and ears
Full build / Import + headless tests (push) Successful in 35s
Full build / Full desktop build + optional release (push) Skipped

This commit is contained in:
zwitschi committed 2026-11-02 19:29:19 +01:00
1 parent 7dc3d3c873
commit 12e70357c5
26 files changed
+80 -62

No files matched your search

+2
View File
@@ -19,6 +19,8 @@ Semantic Versioning.
the hips, so shirts and jackets sit less like straight tubes.
- Character hands now have a wider palm, bent fingers with rounded tips and a
separate thumb instead of flat bars.
- Character noses gained ala wings and a smaller bridge/tip, the mouth is a
rounded slot, and the ears have a darker inner concha.
### Fixed
+7 -2
View File
@@ -96,11 +96,16 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
0.150→0.146, chest 0.170→0.168, upper chest 0.172→0.170. This is a subtle shirt taper, not a
skeleton change; all proportions pass and `sheet_npc_baseline_compare-torso.jpg` records it.
Max character is 4,754 tris / 4,584 verts.
- **NPC face-detail pass (0.9.4, 2026-11-02):** Nose tip radius reduced to 0.022 with ala wings,
the two dark lip boxes became one shallow `Mouth` sphere (two blobs read as a moustache), and the
ears gained a dark `EarInner` concha. All eight rebuilt; current counts are 4,226–5,218 tris /
4,119–5,240 verts under the 8,000/8,000 cap. Sheet:
`sheet_npc_baseline_compare-face-detail.jpg`.
- **NPC hand pass (0.9.4, 2026-11-02):** palm radius 0.046 at z .742, scale (.80,.62,.88);
fingers are `tube` paths from z .758 to .681–.694 with tip spheres, thumb is a bent tube. The
optional hand capture is `capture_npc_baseline.gd -- --view=hand` (8 frames, baseline stays 48).
`npc_firefighter` is at 4,948 verts; the library cap is now 8,000 (see the character-mesh-cap
decision above), so the remaining face/grip/clothing detail can use real headroom. Grip offsets
`npc_firefighter` is at 5,240 verts after the face-detail pass; the library cap is 8,000 (see
the character-mesh-cap decision above), so grip/clothing detail still has headroom. Grip offsets
remain open.
- **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,
+3 -2
View File
@@ -110,8 +110,9 @@ and equipment fit.
sculpted into the head mesh; no separate jaw/cheek spheres or AO intersection rings.)
- [x] Improve brow/eye-socket depth and eyelid/iris readability without turning the low-poly face
into a noisy mesh. (Recessed sclera, thin upper lid; `sheet_npc_baseline_compare-face.jpg`.)
- [ ] Refine the nose bridge/tip, lips and ear shape; keep the five tint slots and face direction
unchanged.
- [x] Refine the nose bridge/tip, lips and ear shape; keep the five tint slots and face direction
unchanged. (Ala wings, smaller tip, one rounded mouth slot, darker ear concha; all eight
rebuilt. `sheet_npc_baseline_compare-face-detail.jpg`.)
- [ ] Audit hair caps, fringes, side locks, buns and beard silhouettes for gaps, intersections and
obvious scalp seams; keep hair color in the tintable slot.
- [ ] Verify hats/hoods/sunglasses/helmets still clear the refined brow, ears and hair; update gear
+30 -30
View File
@@ -453,7 +453,7 @@
"max": [
0.321,
1.824,
0.185
0.181
],
"min": [
-0.321,
@@ -467,8 +467,8 @@
"skin",
"cloth",
"pants",
"eye_white",
"accessory",
"eye_white",
"hair",
"rubber"
],
@@ -487,15 +487,15 @@
"accessory",
"hair"
],
"triangles": 4042,
"vertices": 3894
"triangles": 4226,
"vertices": 4119
},
"npc_coat": {
"bounds_godot": {
"max": [
0.332,
1.824,
0.185
0.181
],
"min": [
-0.332,
@@ -509,8 +509,8 @@
"skin",
"cloth",
"pants",
"eye_white",
"accessory",
"eye_white",
"hair",
"rubber"
],
@@ -528,8 +528,8 @@
"accessory",
"hair"
],
"triangles": 5034,
"vertices": 4814
"triangles": 5218,
"vertices": 4975
},
"npc_dress": {
"bounds_godot": {
@@ -550,8 +550,8 @@
"skin",
"cloth",
"pants",
"eye_white",
"accessory",
"eye_white",
"hair",
"rubber"
],
@@ -567,8 +567,8 @@
"accessory",
"hair"
],
"triangles": 4460,
"vertices": 4191
"triangles": 4644,
"vertices": 4439
},
"npc_firefighter": {
"bounds_godot": {
@@ -589,8 +589,8 @@
"skin",
"cloth",
"pants",
"eye_white",
"accessory",
"eye_white",
"hair",
"hivis",
"reflective",
@@ -608,15 +608,15 @@
"accessory",
"hair"
],
"triangles": 4904,
"vertices": 4948
"triangles": 5088,
"vertices": 5240
},
"npc_guard": {
"bounds_godot": {
"max": [
0.361,
1.819,
0.185
0.181
],
"min": [
-0.361,
@@ -630,8 +630,8 @@
"skin",
"cloth",
"pants",
"eye_white",
"accessory",
"eye_white",
"hair",
"rubber",
"leather",
@@ -652,15 +652,15 @@
"accessory",
"hair"
],
"triangles": 4606,
"vertices": 4679
"triangles": 4790,
"vertices": 4865
},
"npc_hoodie": {
"bounds_godot": {
"max": [
0.326,
1.856,
0.185
0.181
],
"min": [
-0.326,
@@ -674,8 +674,8 @@
"skin",
"cloth",
"pants",
"eye_white",
"accessory",
"eye_white",
"hair",
"rubber"
],
@@ -693,15 +693,15 @@
"accessory",
"hair"
],
"triangles": 4864,
"vertices": 4506
"triangles": 5048,
"vertices": 4712
},
"npc_jacket": {
"bounds_godot": {
"max": [
0.332,
1.824,
0.185
0.181
],
"min": [
-0.332,
@@ -715,8 +715,8 @@
"skin",
"cloth",
"pants",
"eye_white",
"accessory",
"eye_white",
"hair",
"rubber"
],
@@ -734,15 +734,15 @@
"accessory",
"hair"
],
"triangles": 4432,
"vertices": 4265
"triangles": 4616,
"vertices": 4447
},
"npc_vest": {
"bounds_godot": {
"max": [
0.321,
1.819,
0.185
0.181
],
"min": [
-0.321,
@@ -756,8 +756,8 @@
"skin",
"cloth",
"pants",
"eye_white",
"accessory",
"eye_white",
"hair",
"rubber",
"hivis",
@@ -775,8 +775,8 @@
"accessory",
"hair"
],
"triangles": 4540,
"vertices": 4503
"triangles": 4724,
"vertices": 4713
},
"palm_tree": {
"bounds_godot": {
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

+2 -2
View File
@@ -96,8 +96,8 @@ All eight use the `npc_base` contract, so each one can replace
variants (vertex count after seam splits; `CHAR_MAX_TRIS`/`CHAR_MAX_VERTS` in
`tools/blender_assets.py`, raised from 6,000/5,000 on 2026-11-02). The frozen
`npc_base` fallback keeps its original 6,000/4,000 contract. The current range
is 4,042–5,034 triangles and 3,894–4,948 vertices, leaving room for the
remaining face, grip and clothing detail.
is 4,226–5,218 triangles and 4,119–5,240 vertices, leaving room for the
remaining grip and clothing detail.
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
@@ -48,6 +48,7 @@ Committed contact sheets and local Blender renders for the character, gear and p
| <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_baseline_compare-face.jpg" width="280" alt="sheet_npc_baseline_compare-face.jpg"> | [`sheet_npc_baseline_compare-face.jpg`](sheet_npc_baseline_compare-face.jpg) |
| <img src="sheet_npc_baseline_compare-torso.jpg" width="280" alt="sheet_npc_baseline_compare-torso.jpg"> | [`sheet_npc_baseline_compare-torso.jpg`](sheet_npc_baseline_compare-torso.jpg) |
| <img src="sheet_npc_baseline_compare-face-detail.jpg" width="280" alt="sheet_npc_baseline_compare-face-detail.jpg"> | [`sheet_npc_baseline_compare-face-detail.jpg`](sheet_npc_baseline_compare-face-detail.jpg) |
| <img src="sheet_npc_hands_compare.jpg" width="280" alt="sheet_npc_hands_compare.jpg"> | [`sheet_npc_hands_compare.jpg`](sheet_npc_hands_compare.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) |
Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

+2 -1
View File
@@ -42,7 +42,8 @@ Implemented so far:
- 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 face-depth pass (0.9.4 work): sclera are recessed and capped by a thin upper lid, and the separate jaw/cheek spheres are replaced by a chin plate and cheekbones sculpted into the head mesh. The full library was rebuilt; the comparison is `sheet_npc_baseline_compare-face.jpg`.
- NPC torso/hip transition (0.9.4 work): the pelvis and chest are slightly wider than the waist, giving shirts and jackets a readable taper into the hips without changing the build multipliers or the skeleton. The comparison is `sheet_npc_baseline_compare-torso.jpg`.
- NPC mesh budget (0.9.4 work): the eight library variants now have an 8,000-triangle / 8,000-glTF-vertex cap (up from 6,000/5,000); the `npc_base` fallback remains 6,000/4,000. Current characters measure 4,042–5,034 triangles and 3,894–4,948 vertices.
- NPC mesh budget (0.9.4 work): the eight library variants now have an 8,000-triangle / 8,000-glTF-vertex cap (up from 6,000/5,000); the `npc_base` fallback remains 6,000/4,000. Current characters measure 4,226–5,218 triangles and 4,119–5,240 vertices.
- NPC face-detail pass (0.9.4 work): ala wings and a smaller nose bridge/tip, one rounded mouth slot, and a darker ear concha; all eight rebuilt. The comparison is `sheet_npc_baseline_compare-face-detail.jpg`.
- NPC hand pass (0.9.4 work): wider, shallower palms; four two-segment bent fingers with rounded tips and a separate bent thumb; optional `--view=hand` capture at 0.80 m. The hand comparison is `sheet_npc_hands_compare.jpg`; item grip offsets remain open.
- 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.
+20 -19
View File
@@ -8,6 +8,7 @@ Comparison sheets:
[`sheet_npc_baseline_compare-p1.jpg`](blender_previews/sheet_npc_baseline_compare-p1.jpg),
[`sheet_npc_baseline_compare-body.jpg`](blender_previews/sheet_npc_baseline_compare-body.jpg)
the latest [`sheet_npc_baseline_compare-torso.jpg`](blender_previews/sheet_npc_baseline_compare-torso.jpg),
the face-detail pass [`sheet_npc_baseline_compare-face-detail.jpg`](blender_previews/sheet_npc_baseline_compare-face-detail.jpg),
and the conversation-distance hand pass
[`sheet_npc_hands_compare.jpg`](blender_previews/sheet_npc_hands_compare.jpg).
@@ -115,17 +116,17 @@ From `python3 tools/blender_assets.py fit` and the manifest:
| Character | Tris | Verts | Head width | Clothing radius | Notable gear clearances |
| --- | ---: | ---: | ---: | ---: | --- |
| `npc_casual` | 4,042 | 3,894 | 0.315 m | 0.191 m (hip) | backpack x −0.041, radial −0.162 |
| `npc_jacket` | 4,432 | 4,265 | 0.315 m | 0.191 m (hip) | lanyard x −0.116, radial +0.023 (runtime +4 cm forward) |
| `npc_coat` | 5,034 | 4,814 | 0.315 m | 0.225 m (skirt) | sunglasses width ratio 0.641; bag x +0.037, radial −0.130 |
| `npc_vest` | 4,540 | 4,503 | 0.315 m | 0.191 m (hip) | cap width ratio 1.067 |
| `npc_dress` | 4,460 | 4,191 | 0.315 m | 0.260 m (skirt) | bag x +0.002, radial −0.095 |
| `npc_hoodie` | 4,864 | 4,506 | 0.315 m | 0.190 m (hip) | — |
| `npc_firefighter` | 4,904 | 4,948 | 0.315 m | 0.255 m (skirt) | helmet width ratio 1.651; SCBA x −0.105, radial −0.076 |
| `npc_guard` | 4,606 | 4,679 | 0.315 m | 0.194 m (hip) | cap width ratio 1.067 |
| `npc_casual` | 4,226 | 4,119 | 0.315 m | 0.191 m (hip) | backpack x −0.041, radial −0.162 |
| `npc_jacket` | 4,616 | 4,447 | 0.315 m | 0.191 m (hip) | lanyard x −0.116, radial +0.023 (runtime +4 cm forward) |
| `npc_coat` | 5,218 | 4,975 | 0.315 m | 0.225 m (skirt) | sunglasses width ratio 0.641; bag x +0.037, radial −0.130 |
| `npc_vest` | 4,724 | 4,713 | 0.315 m | 0.191 m (hip) | cap width ratio 1.067 |
| `npc_dress` | 4,644 | 4,439 | 0.315 m | 0.260 m (skirt) | bag x +0.002, radial −0.095 |
| `npc_hoodie` | 5,048 | 4,712 | 0.315 m | 0.190 m (hip) | — |
| `npc_firefighter` | 5,088 | 5,240 | 0.315 m | 0.255 m (skirt) | helmet width ratio 1.651; SCBA x −0.105, radial −0.076 |
| `npc_guard` | 4,790 | 4,865 | 0.315 m | 0.194 m (hip) | cap width ratio 1.067 |
`npc_coat` is widest at 5,034 triangles (2,966 below the 8,000 cap);
`npc_firefighter` is closest on vertices at 4,948 (3,052 below the 8,000 cap).
`npc_coat` is widest at 5,218 triangles (2,782 below the 8,000 cap);
`npc_firefighter` is closest on vertices at 5,240 (2,760 below the 8,000 cap).
The refit backpack measures 380 triangles / 406 vertices.
## Baseline findings, ranked
@@ -139,12 +140,13 @@ The refit backpack measures 380 triangles / 406 vertices.
photo prop is now hidden outside `photo`/`film` poses
(`photo_prop_visible_for_pose()`), so it only appears when the character is
actually taking a picture; the after frame confirms one bag at rest.
2. **Face depth. Fixed 2026-11-02.** The sclera was recessed into the socket and
wrapped with a thin upper lid; the separate jaw/cheek spheres were replaced
by a chin plate and cheekbones sculpted into the head mesh. That removes the
dark intersection rings that read as a beard shadow and keeps the mouth on
the surface. The mouth bar and low-poly brows remain stylized, but the eye
socket and jaw/cheek transition now read as one head volume.
2. **Face depth and detail. Fixed 2026-11-02.** The sclera was recessed into the
socket and wrapped with a thin upper lid; the separate jaw/cheek spheres were
replaced by a chin plate and cheekbones sculpted into the head mesh. The nose
now has ala wings and a smaller bridge/tip, the mouth is one rounded slot
instead of a flat bar, and the ears have a darker concha. That removes the
dark intersection rings and the moustache-like read at portrait distance; the
low-poly brows remain stylized.
3. **Backpack straps read as rigid black bars. Refit 2026-11-02.** The straps
were thin round tubes whose chest/waist points sat inside the torso, so only
fragments showed from the front. They are now flattened bands whose front path
@@ -206,6 +208,7 @@ The committed runs are
[`sheet_npc_baseline_compare-body.jpg`](blender_previews/sheet_npc_baseline_compare-body.jpg)
and [`sheet_npc_baseline_compare-face.jpg`](blender_previews/sheet_npc_baseline_compare-face.jpg).
The transition pass is [`sheet_npc_baseline_compare-torso.jpg`](blender_previews/sheet_npc_baseline_compare-torso.jpg).
The face-detail pass is [`sheet_npc_baseline_compare-face-detail.jpg`](blender_previews/sheet_npc_baseline_compare-face-detail.jpg).
The hand pass is [`sheet_npc_hands_compare.jpg`](blender_previews/sheet_npc_hands_compare.jpg).
Keep the baseline sheet as the before artifact and add a new comparison sheet
rather than replacing it.
@@ -217,7 +220,5 @@ rather than replacing it.
2. Add the remaining body-shape variation between variants and re-measure the
LOD/impostor silhouettes.
3. Audit hems and footwear in walk/sit/block poses.
4. Refine nose/lip/ear shapes only if the portrait pass still flags them after
the hands and clothing read cleanly.
5. Re-run the 48-frame baseline, the 96-frame final-context review, the 24-NPC
4. Re-run the 48-frame baseline, the 96-frame final-context review, the 24-NPC
budget and `blender_assets.py audit/fit`; publish the next comparison sheet.
+13 -6
View File
@@ -774,14 +774,19 @@ def _face(mats, spec):
# nose: bridge and ball (nostrils were cut to stay inside the vertex budget)
parts.append(cyl("NoseBridge", 0.016, 0.022, 0.06, (0, -0.145, 1.652), skin, seg=8,
rot=(math.radians(-72), 0, 0), smooth=True))
parts.append(sphere("NoseTip", 0.024, (0, -0.163, 1.616), skin, seg=10, rings=6,
scale=(1.0, 0.9, 0.85)))
parts.append(sphere("NoseTip", 0.022, (0, -0.162, 1.615), skin, seg=10, rings=6,
scale=(1.0, 0.85, 0.80)))
for s in (-1.0, 1.0):
# ear with a lobe, and one crease so it is not a flat disc
# Ala widen the nose base into a small triangle rather than a ball.
parts.append(sphere("NoseWing", 0.011, (0.015 * s, -0.160, 1.606), skin,
seg=6, rings=4, scale=(1.0, 0.65, 0.75)))
# Ear with a lobe and a darker concha; the old disc had no depth cue.
parts.append(sphere("Ear", 0.036, (0.143 * s, 0.008, 1.63), skin, seg=8, rings=5,
scale=(0.42, 1.0, 1.35)))
parts.append(sphere("EarLobe", 0.018, (0.142 * s, -0.005, 1.605), skin, seg=8,
rings=5, scale=(0.45, 1.0, 0.9)))
parts.append(sphere("EarInner", 0.013, (0.149 * s, -0.003, 1.633), acc,
seg=6, rings=4, scale=(0.28, 0.65, 0.90)))
# Eye: recess the sclera into the socket, then wrap it with skin lids.
# The old white ball sat ~1 cm proud of the head; this keeps the iris
# readable without the doll-like bulge.
@@ -798,9 +803,11 @@ def _face(mats, spec):
parts.append(box("Brow", (0.055, 0.014, 0.012), (0.052 * s, -0.143, 1.699), hair,
rot=(0, 0.14 * s, 0.0)))
# lips: upper and lower instead of a single bar
parts.append(box("LipUpper", (0.052, 0.012, 0.008), (0, -0.156, 1.588), acc,
rot=(math.radians(6), 0, 0)))
parts.append(box("LipLower", (0.046, 0.012, 0.009), (0, -0.154, 1.578), acc))
# One rounded mouth slot instead of two flat boxes. Two dark lip blobs read
# as a moustache at portrait distance; a single shallow oval keeps the mouth
# line without adding a second dark mass.
parts.append(sphere("Mouth", 0.028, (0, -0.158, 1.583), acc, seg=8, rings=5,
scale=(1.0, 0.28, 0.12)))
if spec.get("beard"):
parts.append(sphere("Beard", 0.105, (0, -0.06, 1.55), hair, seg=12, rings=8,
scale=(1.0, 0.9, 0.75), cut_below=-0.2))