Rebuild the NPC hands and add a hand capture
Full build / Import + headless tests (push) Successful in 50s
Full build / Full desktop build + optional release (push) Skipped

This commit is contained in:
zwitschi committed 2026-11-02 19:14:55 +01:00
1 parent 48082c3dfa
commit 24c939d388
27 files changed
+107 -57

No files matched your search

+2
View File
@@ -14,6 +14,8 @@ Semantic Versioning.
cheekbones are sculpted into the head instead of reading as separate spheres.
- Character torsos taper from the chest to the waist and flare slightly into
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.
- 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
@@ -90,6 +90,11 @@ 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 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 now at 4,948 verts (52 below the 5,000 cap); do not add hand/face geometry
without replacing something. 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,
wand allow/deny and review return-to-menu. Keep this smoke in the 0.9.6
+6 -3
View File
@@ -116,9 +116,12 @@ and equipment fit.
### Hands, feet and clothing
- [ ] Refine hands and fingers enough to read at conversation distance without exceeding the
character triangle budget; check grip alignment for phone, cup, clipboard, camera, sign,
flashlight, wand and radio.
- [x] Refine hands and fingers enough to read at conversation distance without exceeding the
character triangle budget. (Wider palm, two-segment bent fingers with rounded tips, separate
thumb; optional `--view=hand` capture and `sheet_npc_hands_compare.jpg`.)
- [ ] Fix grip alignment for phone, cup, clipboard, camera, sign, flashlight, wand and radio
against the new palm/finger geometry. (Several props still occlude the palm or float beside
it in the hand capture.)
- [ ] Audit shoes/boots for silhouette, grounding and contact with the floor; preserve the
responder and firefighter footwear language.
- [ ] Improve clothing layer transitions at collars, cuffs, plackets, waistbands, pockets, badges
+16 -16
View File
@@ -487,8 +487,8 @@
"accessory",
"hair"
],
"triangles": 3762,
"vertices": 3474
"triangles": 4042,
"vertices": 3894
},
"npc_coat": {
"bounds_godot": {
@@ -528,8 +528,8 @@
"accessory",
"hair"
],
"triangles": 4754,
"vertices": 4584
"triangles": 5034,
"vertices": 4814
},
"npc_dress": {
"bounds_godot": {
@@ -567,8 +567,8 @@
"accessory",
"hair"
],
"triangles": 4180,
"vertices": 3863
"triangles": 4460,
"vertices": 4191
},
"npc_firefighter": {
"bounds_godot": {
@@ -608,8 +608,8 @@
"accessory",
"hair"
],
"triangles": 4624,
"vertices": 4583
"triangles": 4904,
"vertices": 4948
},
"npc_guard": {
"bounds_godot": {
@@ -652,8 +652,8 @@
"accessory",
"hair"
],
"triangles": 4326,
"vertices": 4443
"triangles": 4606,
"vertices": 4679
},
"npc_hoodie": {
"bounds_godot": {
@@ -693,8 +693,8 @@
"accessory",
"hair"
],
"triangles": 4584,
"vertices": 4294
"triangles": 4864,
"vertices": 4506
},
"npc_jacket": {
"bounds_godot": {
@@ -734,8 +734,8 @@
"accessory",
"hair"
],
"triangles": 4152,
"vertices": 3900
"triangles": 4432,
"vertices": 4265
},
"npc_vest": {
"bounds_godot": {
@@ -775,8 +775,8 @@
"accessory",
"hair"
],
"triangles": 4260,
"vertices": 4183
"triangles": 4540,
"vertices": 4503
},
"palm_tree": {
"bounds_godot": {
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 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: 57 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 61 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,762–4,754 triangles
and 3,474–4,584 vertices.
the 2026-11-01 face/hand pass). The current range is 4,042–5,034 triangles
and 3,894–4,948 vertices. The vertex cap is now the tighter budget.
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_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) |
| <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: 352 KiB

+1
View File
@@ -42,6 +42,7 @@ 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 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.
- 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.
+41 -23
View File
@@ -1,12 +1,15 @@
# NPC visual baseline and acceptance criteria
Status: baseline captured 2026-11-02. The first P1 fixes (tourist photo-prop
visibility, backpack straps), the neck/shoulder and torso/hip transitions and
the face-depth pass are applied. Remaining hand/clothing work is open; the
baseline remains the reference for 0.9.4. Comparison sheets:
Status: baseline captured 2026-11-02. The P1 fixes (tourist photo-prop
visibility, backpack straps), the neck/shoulder and torso/hip transitions, the
face-depth pass and the first hand pass are applied. Hand-item grip alignment
and clothing work remain open; the baseline remains the reference for 0.9.4.
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)
and the latest [`sheet_npc_baseline_compare-torso.jpg`](blender_previews/sheet_npc_baseline_compare-torso.jpg).
the latest [`sheet_npc_baseline_compare-torso.jpg`](blender_previews/sheet_npc_baseline_compare-torso.jpg),
and the conversation-distance hand pass
[`sheet_npc_hands_compare.jpg`](blender_previews/sheet_npc_hands_compare.jpg).
## What was captured
@@ -37,6 +40,17 @@ python3 tools/npc_baseline.py sheet --dir tmp/npc-baseline
The 48 raw PNGs are local QA under `tmp/` (gitignored). The committed contact
sheet is [`blender_previews/sheet_npc_baseline.jpg`](blender_previews/sheet_npc_baseline.jpg).
For the P2 hand/grip check there is an optional eighth view, captured separately
so the 48-frame baseline stays stable:
```bash
godot --path . --script res://src/test/capture_npc_baseline.gd -- \
--out=res://tmp/npc-hands --view=hand
```
That writes eight `*__hand.png` frames at 0.80 m from the right hand; the
before/after review image is
[`sheet_npc_hands_compare.jpg`](blender_previews/sheet_npc_hands_compare.jpg).
The capture is deterministic apart from engine rasterisation; re-running into
the same directory keeps the earlier files and writes `-vN` copies rather than
overwriting them.
@@ -100,19 +114,19 @@ From `python3 tools/blender_assets.py fit` and the manifest:
| Character | Tris | Verts | Head width | Clothing radius | Notable gear clearances |
| --- | ---: | ---: | ---: | ---: | --- |
| `npc_casual` | 3,762 | 3,474 | 0.315 m | 0.191 m (hip) | backpack x −0.041, radial −0.162 |
| `npc_jacket` | 4,152 | 3,900 | 0.315 m | 0.191 m (hip) | lanyard x −0.116, radial +0.023 (runtime +4 cm forward) |
| `npc_coat` | 4,754 | 4,584 | 0.315 m | 0.225 m (skirt) | sunglasses width ratio 0.641; bag x +0.037, radial −0.130 |
| `npc_vest` | 4,260 | 4,183 | 0.315 m | 0.191 m (hip) | cap width ratio 1.067 |
| `npc_dress` | 4,180 | 3,863 | 0.315 m | 0.260 m (skirt) | bag x +0.002, radial −0.095 |
| `npc_hoodie` | 4,584 | 4,294 | 0.315 m | 0.190 m (hip) | — |
| `npc_firefighter` | 4,624 | 4,583 | 0.315 m | 0.255 m (skirt) | helmet width ratio 1.651; SCBA x −0.105, radial −0.076 |
| `npc_guard` | 4,326 | 4,443 | 0.315 m | 0.194 m (hip) | cap width ratio 1.067 |
| `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 |
The widest character is `npc_coat` at 4,754 triangles and 4,584 vertices, so the
current headroom is 1,246 triangles and 416 vertices per character. Any hand or
clothing refinement must come out of that budget or replace existing detail. The
refit backpack measures 380 triangles / 406 vertices.
`npc_coat` is widest at 5,034 triangles (966 below the cap); `npc_firefighter` is
closest on vertices at 4,948 (only 52 below the 5,000 cap). Any further hand or
clothing detail must replace existing geometry rather than adding it. The refit
backpack measures 380 triangles / 406 vertices.
## Baseline findings, ranked
@@ -140,9 +154,12 @@ refit backpack measures 380 triangles / 406 vertices.
### P2 — fix after the first P1 pass
4. **Hands and grips are not proven at conversation distance.** The full-body
frame shows small block hands; the camera, phone and clipboard need a
close-up grip check before the hand mesh is accepted.
4. **Hands: first pass fixed 2026-11-02; grip alignment still open.** The palm
is wider and shallower, and each finger is now a two-segment bent tube with a
rounded tip and a separate bent thumb. The hand capture shows a readable
four-finger hand instead of flat bars. The camera, phone, cup, clipboard and
sign still need their attach offsets checked: several props occlude the palm
or read as floating beside it.
5. **Clothing detail is tile-driven and transitional seams are visible.** The
fabric pattern repeats strongly on the casual, coat and dress variants, and
collar/cuff/waist edges are abrupt. This is more visible than the shoe/hem
@@ -189,15 +206,16 @@ 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 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.
## Next implementation order
1. Add the remaining body-shape variation between variants and re-measure the
1. Fix hand-item grip offsets (phone, cup, clipboard, camera, sign, flashlight,
wand and radio) against the new palm/finger geometry.
2. Add the remaining body-shape variation between variants and re-measure the
LOD/impostor silhouettes.
2. Add a conversation-distance hand/grip capture and fix hand geometry before
judging hand items.
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.
+12 -4
View File
@@ -30,6 +30,8 @@ const VARIANTS := [
]
## Camera orbits the subject at yaw 0 (front = Godot +Z). Portrait views keep
## the eye/head in frame; the last two show the full body and crowd silhouette.
const HAND_VIEW := {"name": "hand", "yaw": 15.0, "distance": 0.80, "height": 0.72,
"target_x": 0.27, "target_y": 0.72, "fov": 32.0}
const VIEWS := [
{"name": "front", "yaw": 0.0, "distance": 1.85, "height": 1.55,
"target_y": 1.52, "fov": 32.0},
@@ -48,6 +50,8 @@ const SETTLE_FRAMES := 4
const MAX_FRAMES := 4000
var out_dir := "user://screenshots/npc_baseline"
var only_view := ""
var _views: Array = VIEWS
var frames := 0
var _phase := "stage_variant"
var _variant_i := 0
@@ -61,6 +65,10 @@ func _initialize() -> void:
for arg in OS.get_cmdline_user_args():
if str(arg).begins_with("--out="):
out_dir = str(arg).substr(6)
elif str(arg).begins_with("--view="):
only_view = str(arg).substr(7)
if only_view == "hand":
_views = [HAND_VIEW]
root.size = Vector2i(1280, 720)
_build_stage()
@@ -83,7 +91,7 @@ func _process(_delta: float) -> bool:
"shoot":
_shoot()
_view_i += 1
if _view_i >= VIEWS.size():
if _view_i >= _views.size():
_view_i = 0
_variant_i += 1
if _variant_i >= VARIANTS.size():
@@ -169,10 +177,10 @@ func _stage_variant() -> void:
func _frame_view() -> void:
if _avatar == null or not is_instance_valid(_avatar):
return
var view: Dictionary = VIEWS[_view_i]
var view: Dictionary = _views[_view_i]
var yaw := deg_to_rad(float(view["yaw"]))
var distance := float(view["distance"])
var target := Vector3(0.0, float(view["target_y"]), 0.0)
var target := Vector3(float(view.get("target_x", 0.0)), float(view["target_y"]), 0.0)
_camera.position = target + Vector3(sin(yaw) * distance, float(view["height"]) - target.y,
cos(yaw) * distance)
_camera.fov = float(view["fov"])
@@ -180,7 +188,7 @@ func _frame_view() -> void:
func _shoot() -> void:
var variant: Dictionary = VARIANTS[_variant_i]
var view: Dictionary = VIEWS[_view_i]
var view: Dictionary = _views[_view_i]
var dir := ProjectSettings.globalize_path(out_dir)
DirAccess.make_dir_recursive_absolute(dir)
var base := "%s__%s" % [variant["name"], view["name"]]
+21 -9
View File
@@ -813,16 +813,28 @@ def _hands_and_shoes(mats, spec):
w = BUILDS[spec.get("build", "average")][0]
for s in (-1.0, 1.0):
x = 0.275 * s * w
parts.append(sphere("Palm", 0.045, (x, -0.01, 0.735), skin, seg=10, rings=6,
scale=(0.55, 0.95, 1.25)))
parts.append(sphere("Thumb", 0.018, (x - 0.014 * s, -0.048, 0.752), skin,
seg=8, rings=6, scale=(1.0, 1.0, 1.7), rot=(0.4, 0, 0)))
# four fingers: relaxed, slightly curled forward, outer ones shorter
# Wider and shallower than the old ball: a palm, not a wrist knob.
parts.append(sphere("Palm", 0.046, (x, -0.012, 0.742), skin, seg=10, rings=6,
scale=(0.80, 0.62, 0.88)))
# Four bent, tapering fingers with rounded tips. The two segments keep
# the hand readable at conversation distance without adding a full
# finger skeleton; the outer fingers are slightly shorter.
for index, offset in enumerate((-0.024, -0.008, 0.008, 0.024)):
length = (0.046, 0.052, 0.050, 0.040)[index]
parts.append(cyl("Finger", 0.011, 0.009, length,
(x + offset * s, -0.030, 0.714 - length * 0.5), skin,
seg=6, rot=(math.radians(14), 0, 0)))
fx = x + offset * s
tip_z = (0.692, 0.681, 0.684, 0.694)[index]
radius = 0.0115 if index in (1, 2) else 0.0105
parts.append(tube("Finger", [(fx, -0.018, 0.758),
(fx, -0.030, 0.726),
(fx, -0.042, tip_z)],
radius, skin, seg=6, caps=False))
parts.append(sphere("Fingertip", radius, (fx, -0.042, tip_z), skin,
seg=6, rings=4))
parts.append(tube("Thumb", [(x - 0.010 * s, -0.018, 0.758),
(x - 0.028 * s, -0.040, 0.740),
(x - 0.034 * s, -0.055, 0.718)],
0.012, skin, seg=6, caps=False))
parts.append(sphere("ThumbTip", 0.012, (x - 0.034 * s, -0.055, 0.718), skin,
seg=6, rings=4))
parts.append(box("Shoe", (0.10, 0.25, 0.075), (0.12 * s, -0.045, 0.04), shoe,
bevel=0.028, segs=1))
parts.append(box("Sole", (0.106, 0.262, 0.02), (0.12 * s, -0.045, 0.01),