diff --git a/CHANGELOG.md b/CHANGELOG.md index a8e629c..9519bf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/MEMORY.md b/MEMORY.md index 72496a8..1d0c8eb 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -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 diff --git a/TODO.md b/TODO.md index 7c10066..8f77843 100644 --- a/TODO.md +++ b/TODO.md @@ -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 diff --git a/assets/models/blender/manifest.json b/assets/models/blender/manifest.json index ebafaf9..cba8252 100644 --- a/assets/models/blender/manifest.json +++ b/assets/models/blender/manifest.json @@ -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": { diff --git a/assets/models/blender/npc/npc_casual.glb b/assets/models/blender/npc/npc_casual.glb index 4d32291..d4d7cc1 100644 Binary files a/assets/models/blender/npc/npc_casual.glb and b/assets/models/blender/npc/npc_casual.glb differ diff --git a/assets/models/blender/npc/npc_casual_ao.png b/assets/models/blender/npc/npc_casual_ao.png index 5112894..95237ec 100644 Binary files a/assets/models/blender/npc/npc_casual_ao.png and b/assets/models/blender/npc/npc_casual_ao.png differ diff --git a/assets/models/blender/npc/npc_coat.glb b/assets/models/blender/npc/npc_coat.glb index 8edea03..9d023eb 100644 Binary files a/assets/models/blender/npc/npc_coat.glb and b/assets/models/blender/npc/npc_coat.glb differ diff --git a/assets/models/blender/npc/npc_coat_ao.png b/assets/models/blender/npc/npc_coat_ao.png index 06fbdbe..c26cdc9 100644 Binary files a/assets/models/blender/npc/npc_coat_ao.png and b/assets/models/blender/npc/npc_coat_ao.png differ diff --git a/assets/models/blender/npc/npc_dress.glb b/assets/models/blender/npc/npc_dress.glb index 5989784..68396b2 100644 Binary files a/assets/models/blender/npc/npc_dress.glb and b/assets/models/blender/npc/npc_dress.glb differ diff --git a/assets/models/blender/npc/npc_dress_ao.png b/assets/models/blender/npc/npc_dress_ao.png index d6e5eea..a70b849 100644 Binary files a/assets/models/blender/npc/npc_dress_ao.png and b/assets/models/blender/npc/npc_dress_ao.png differ diff --git a/assets/models/blender/npc/npc_firefighter.glb b/assets/models/blender/npc/npc_firefighter.glb index d1e6a1a..67081d3 100644 Binary files a/assets/models/blender/npc/npc_firefighter.glb and b/assets/models/blender/npc/npc_firefighter.glb differ diff --git a/assets/models/blender/npc/npc_firefighter_ao.png b/assets/models/blender/npc/npc_firefighter_ao.png index ae6ead2..22e6176 100644 Binary files a/assets/models/blender/npc/npc_firefighter_ao.png and b/assets/models/blender/npc/npc_firefighter_ao.png differ diff --git a/assets/models/blender/npc/npc_guard.glb b/assets/models/blender/npc/npc_guard.glb index d0d5e38..3959e5b 100644 Binary files a/assets/models/blender/npc/npc_guard.glb and b/assets/models/blender/npc/npc_guard.glb differ diff --git a/assets/models/blender/npc/npc_guard_ao.png b/assets/models/blender/npc/npc_guard_ao.png index a9d91a6..8ee029c 100644 Binary files a/assets/models/blender/npc/npc_guard_ao.png and b/assets/models/blender/npc/npc_guard_ao.png differ diff --git a/assets/models/blender/npc/npc_hoodie.glb b/assets/models/blender/npc/npc_hoodie.glb index 06c0804..76d86da 100644 Binary files a/assets/models/blender/npc/npc_hoodie.glb and b/assets/models/blender/npc/npc_hoodie.glb differ diff --git a/assets/models/blender/npc/npc_hoodie_ao.png b/assets/models/blender/npc/npc_hoodie_ao.png index 290a282..c30b301 100644 Binary files a/assets/models/blender/npc/npc_hoodie_ao.png and b/assets/models/blender/npc/npc_hoodie_ao.png differ diff --git a/assets/models/blender/npc/npc_jacket.glb b/assets/models/blender/npc/npc_jacket.glb index 1538d32..f0207f6 100644 Binary files a/assets/models/blender/npc/npc_jacket.glb and b/assets/models/blender/npc/npc_jacket.glb differ diff --git a/assets/models/blender/npc/npc_jacket_ao.png b/assets/models/blender/npc/npc_jacket_ao.png index c8efa99..b8eb5fa 100644 Binary files a/assets/models/blender/npc/npc_jacket_ao.png and b/assets/models/blender/npc/npc_jacket_ao.png differ diff --git a/assets/models/blender/npc/npc_vest.glb b/assets/models/blender/npc/npc_vest.glb index 6e6708f..7761e08 100644 Binary files a/assets/models/blender/npc/npc_vest.glb and b/assets/models/blender/npc/npc_vest.glb differ diff --git a/assets/models/blender/npc/npc_vest_ao.png b/assets/models/blender/npc/npc_vest_ao.png index 4eacd19..f2b948c 100644 Binary files a/assets/models/blender/npc/npc_vest_ao.png and b/assets/models/blender/npc/npc_vest_ao.png differ diff --git a/assets/models/blender/npc_gear/backpack.glb b/assets/models/blender/npc_gear/backpack.glb index 53c0f3c..b0dcfbc 100644 Binary files a/assets/models/blender/npc_gear/backpack.glb and b/assets/models/blender/npc_gear/backpack.glb differ diff --git a/assets/models/blender/npc_gear/backpack_ao.png b/assets/models/blender/npc_gear/backpack_ao.png index f333c63..6e95243 100644 Binary files a/assets/models/blender/npc_gear/backpack_ao.png and b/assets/models/blender/npc_gear/backpack_ao.png differ diff --git a/docs/blender-assets.md b/docs/blender-assets.md index f75ef56..8df6136 100644 --- a/docs/blender-assets.md +++ b/docs/blender-assets.md @@ -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, diff --git a/docs/blender_previews/README.md b/docs/blender_previews/README.md index 0c553ab..dd719ba 100644 --- a/docs/blender_previews/README.md +++ b/docs/blender_previews/README.md @@ -45,6 +45,7 @@ Committed contact sheets and local Blender renders for the character, gear and p | sheet_npc.jpg | [`sheet_npc.jpg`](sheet_npc.jpg) | | sheet_npc_baseline.jpg | [`sheet_npc_baseline.jpg`](sheet_npc_baseline.jpg) | | sheet_npc_baseline_compare-p1.jpg | [`sheet_npc_baseline_compare-p1.jpg`](sheet_npc_baseline_compare-p1.jpg) | +| sheet_npc_baseline_compare-body.jpg | [`sheet_npc_baseline_compare-body.jpg`](sheet_npc_baseline_compare-body.jpg) | | sheet_npc_gear.jpg | [`sheet_npc_gear.jpg`](sheet_npc_gear.jpg) | | sheet_props.jpg | [`sheet_props.jpg`](sheet_props.jpg) | | shoulder_bag.png | [`shoulder_bag.png`](shoulder_bag.png) | diff --git a/docs/blender_previews/sheet_npc_baseline_compare-body.jpg b/docs/blender_previews/sheet_npc_baseline_compare-body.jpg new file mode 100644 index 0000000..d4a41ac Binary files /dev/null and b/docs/blender_previews/sheet_npc_baseline_compare-body.jpg differ diff --git a/docs/development-status.md b/docs/development-status.md index 5ce3f42..1a3a1fa 100644 --- a/docs/development-status.md +++ b/docs/development-status.md @@ -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. diff --git a/docs/npc-proportion-audit.md b/docs/npc-proportion-audit.md index 36e68c0..a054528 100644 --- a/docs/npc-proportion-audit.md +++ b/docs/npc-proportion-audit.md @@ -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; diff --git a/docs/npc-visual-review.md b/docs/npc-visual-review.md index 36c18d0..6bac82f 100644 --- a/docs/npc-visual-review.md +++ b/docs/npc-visual-review.md @@ -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. diff --git a/tools/blender_assets.py b/tools/blender_assets.py index 1dd54cd..914c0ad 100644 --- a/tools/blender_assets.py +++ b/tools/blender_assets.py @@ -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):