diff --git a/CHANGELOG.md b/CHANGELOG.md index 66bcf95..e3801e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ Semantic Versioning. - Removed every hat and hood from the NPC roster: the firefighter helmet is the only head gear left, so no character hides its face behind a cap. The street performer no longer wears the cone hat and carries the baton and board in the hands instead. +- Every NPC now has a visible mouth; the Quaternius heads shipped without one, so the faces read as + brow-and-eye masks. - Fixed the modular Worker characters: the guard, delivery, firefighter and crowd roles had holes in the torso and an open crown, a continuous unibrow and no visible eyes. They now have a closed torso, hair, two eyebrows and two eyes. diff --git a/TODO.md b/TODO.md index f1cd212..cfacef5 100644 --- a/TODO.md +++ b/TODO.md @@ -930,6 +930,37 @@ gear) and 5 (head gear on the skull). - [ ] Refresh `CHARACTERS`, the manifest roles/tint data, `CREDITS.md` and the review doc rows as each family lands. +### Prop and gear fit pass (new NPC attach points) + +Goal: every gear piece and role prop is measured against, and re-authored for, the Quaternius +skeleton. The 0.9.9 roster swap moved the hand from y 0.78 to the wrist at y 1.017, the head centre +from 1.62 to 1.69 and the shoulder width from 0.27 to 0.15, so anything authored for the procedural +body is now the wrong size or in the wrong place. Nothing in this workstream changes the runtime +contract. + +- [ ] Extend `tools/blender_assets.py fit` to measure **hand** and **chest** gear (it currently + reports head, back and chest only, and skips `hand` outright). Report each item's grip + offset from the wrist, its overall size and whether it clears the thigh at rest. +- [ ] Record a measurement table for the nine hand items (`phone`, `cup`, `clipboard`, `umbrella`, + `camera`, `protest_sign`, `flashlight`, `wand`, `radio`) against the Quaternius wrist + (Godot y 1.017, palm 4 cm forward) and pick a grip scale per item. +- [ ] Re-author the oversized hand items: `clipboard` (0.23 x 0.31 m) and `umbrella` (0.70 m + shaft) are the two the captures flag; rebuild, re-bake AO and re-check in the capture gate. +- [ ] Re-fit the role props in `NpcAvatar._add_props()` and `_add_hand_item()` — delivery box, + confused-visitor tray, influencer photo prop, tourist camera — against the same wrist + anchors, and delete the now-unused primitive cap/hood fallbacks, if any remain. +- [ ] Retire the gear GLBs the roster no longer uses (`cap`, `hood`, `beanie`) or mark them + fallback-only, and update `CREDITS.md`, the manifest and `docs/blender-assets.md`. +- [ ] Re-measure `fit` for `backpack`, `shoulder_bag`, `scba_tank` and `lanyard` on the slimmer + Quaternius torso and re-fit the straps where they no longer touch the body. +- [ ] Rebuild `docs/blender_previews/sheet_npc_gear.jpg` and add a gear-on-character sheet to the + review page, then re-run the 24-NPC budget smoke. +- [ ] Update `docs/npc-visual-review.md` row 4 (hands and hand-held gear) once the pass lands. + +**Exit:** every gear piece and role prop has a measured fit against the Quaternius attach points, +the hand-held items are within +/-10 % of their authored real-world size, and the review page row 4 +is signed off. + ### Gates and rollout - [ ] Re-bake AO per rebuilt GLB and refresh the stale `.godot` import cache (scratch-copy diff --git a/assets/models/blender/manifest.json b/assets/models/blender/manifest.json index b2e2604..aab0037 100644 --- a/assets/models/blender/manifest.json +++ b/assets/models/blender/manifest.json @@ -450,7 +450,8 @@ "Eyebrows", "Eye", "hair", - "pants" + "pants", + "mouth" ], "mesh_node": "npc_base", "path": "assets/models/blender/npc/npc_casual.glb", @@ -467,8 +468,8 @@ "accessory", "hair" ], - "triangles": 6206, - "vertices": 12988 + "triangles": 6218, + "vertices": 13012 }, "npc_coat": { "base": "hoodie", @@ -493,7 +494,8 @@ "Eyebrows", "Eye", "hair", - "pants" + "pants", + "mouth" ], "mesh_node": "npc_base", "path": "assets/models/blender/npc/npc_coat.glb", @@ -509,8 +511,8 @@ "accessory", "hair" ], - "triangles": 6206, - "vertices": 12988 + "triangles": 6218, + "vertices": 13012 }, "npc_crowd_a": { "base": "casual", @@ -535,7 +537,8 @@ "Hair_Brown", "Brown", "hair", - "pants" + "pants", + "mouth" ], "mesh_node": "npc_base", "path": "assets/models/blender/npc/npc_crowd_a.glb", @@ -550,8 +553,8 @@ "accessory", "hair" ], - "triangles": 6424, - "vertices": 13439 + "triangles": 6436, + "vertices": 13459 }, "npc_crowd_b": { "base": "casual", @@ -576,7 +579,8 @@ "Hair_Brown", "Brown", "hair", - "pants" + "pants", + "mouth" ], "mesh_node": "npc_base", "path": "assets/models/blender/npc/npc_crowd_b.glb", @@ -591,8 +595,8 @@ "accessory", "hair" ], - "triangles": 6424, - "vertices": 13439 + "triangles": 6436, + "vertices": 13459 }, "npc_crowd_c": { "base": "hoodie", @@ -617,7 +621,8 @@ "Eyebrows", "Eye", "hair", - "pants" + "pants", + "mouth" ], "mesh_node": "npc_base", "path": "assets/models/blender/npc/npc_crowd_c.glb", @@ -632,8 +637,8 @@ "accessory", "hair" ], - "triangles": 6206, - "vertices": 12988 + "triangles": 6218, + "vertices": 13012 }, "npc_crowd_d": { "base": "worker", @@ -660,7 +665,8 @@ "pants", "Brows", "Eye.001", - "hair" + "hair", + "mouth" ], "mesh_node": "npc_base", "path": "assets/models/blender/npc/npc_crowd_d.glb", @@ -675,8 +681,8 @@ "accessory", "hair" ], - "triangles": 6254, - "vertices": 13198 + "triangles": 6266, + "vertices": 13222 }, "npc_dress": { "base": "casual", @@ -701,7 +707,8 @@ "Hair_Brown", "Brown", "hair", - "pants" + "pants", + "mouth" ], "mesh_node": "npc_base", "path": "assets/models/blender/npc/npc_dress.glb", @@ -715,8 +722,8 @@ "accessory", "hair" ], - "triangles": 6424, - "vertices": 13439 + "triangles": 6436, + "vertices": 13459 }, "npc_firefighter": { "base": "worker", @@ -743,7 +750,8 @@ "pants", "Brows", "Eye.001", - "hair" + "hair", + "mouth" ], "mesh_node": "npc_base", "path": "assets/models/blender/npc/npc_firefighter.glb", @@ -757,8 +765,8 @@ "accessory", "hair" ], - "triangles": 6254, - "vertices": 13198 + "triangles": 6266, + "vertices": 13222 }, "npc_guard": { "base": "worker", @@ -785,7 +793,8 @@ "pants", "Brows", "Eye.001", - "hair" + "hair", + "mouth" ], "mesh_node": "npc_base", "path": "assets/models/blender/npc/npc_guard.glb", @@ -800,8 +809,8 @@ "accessory", "hair" ], - "triangles": 6254, - "vertices": 13198 + "triangles": 6266, + "vertices": 13222 }, "npc_hoodie": { "base": "hoodie", @@ -826,7 +835,8 @@ "Eyebrows", "Eye", "hair", - "pants" + "pants", + "mouth" ], "mesh_node": "npc_base", "path": "assets/models/blender/npc/npc_hoodie.glb", @@ -842,8 +852,8 @@ "accessory", "hair" ], - "triangles": 6206, - "vertices": 12988 + "triangles": 6218, + "vertices": 13012 }, "npc_jacket": { "base": "casual", @@ -868,7 +878,8 @@ "Hair_Brown", "Brown", "hair", - "pants" + "pants", + "mouth" ], "mesh_node": "npc_base", "path": "assets/models/blender/npc/npc_jacket.glb", @@ -884,8 +895,8 @@ "accessory", "hair" ], - "triangles": 6424, - "vertices": 13439 + "triangles": 6436, + "vertices": 13459 }, "npc_vest": { "base": "worker", @@ -913,7 +924,8 @@ "pants", "Brows", "Eye.001", - "hair" + "hair", + "mouth" ], "mesh_node": "npc_base", "path": "assets/models/blender/npc/npc_vest.glb", @@ -927,8 +939,8 @@ "accessory", "hair" ], - "triangles": 6254, - "vertices": 13198 + "triangles": 6266, + "vertices": 13222 }, "palm_tree": { "bounds_godot": { diff --git a/assets/models/blender/npc/npc_casual.glb b/assets/models/blender/npc/npc_casual.glb index 6cce457..79a39bb 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 27dc18e..2948cfe 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 6cce457..79a39bb 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 27dc18e..2948cfe 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_crowd_a.glb b/assets/models/blender/npc/npc_crowd_a.glb index 41811dc..f466e02 100644 Binary files a/assets/models/blender/npc/npc_crowd_a.glb and b/assets/models/blender/npc/npc_crowd_a.glb differ diff --git a/assets/models/blender/npc/npc_crowd_a_ao.png b/assets/models/blender/npc/npc_crowd_a_ao.png index 45cebb6..82a54b5 100644 Binary files a/assets/models/blender/npc/npc_crowd_a_ao.png and b/assets/models/blender/npc/npc_crowd_a_ao.png differ diff --git a/assets/models/blender/npc/npc_crowd_b.glb b/assets/models/blender/npc/npc_crowd_b.glb index 41811dc..f466e02 100644 Binary files a/assets/models/blender/npc/npc_crowd_b.glb and b/assets/models/blender/npc/npc_crowd_b.glb differ diff --git a/assets/models/blender/npc/npc_crowd_b_ao.png b/assets/models/blender/npc/npc_crowd_b_ao.png index 45cebb6..82a54b5 100644 Binary files a/assets/models/blender/npc/npc_crowd_b_ao.png and b/assets/models/blender/npc/npc_crowd_b_ao.png differ diff --git a/assets/models/blender/npc/npc_crowd_c.glb b/assets/models/blender/npc/npc_crowd_c.glb index 6cce457..79a39bb 100644 Binary files a/assets/models/blender/npc/npc_crowd_c.glb and b/assets/models/blender/npc/npc_crowd_c.glb differ diff --git a/assets/models/blender/npc/npc_crowd_c_ao.png b/assets/models/blender/npc/npc_crowd_c_ao.png index 27dc18e..2948cfe 100644 Binary files a/assets/models/blender/npc/npc_crowd_c_ao.png and b/assets/models/blender/npc/npc_crowd_c_ao.png differ diff --git a/assets/models/blender/npc/npc_crowd_d.glb b/assets/models/blender/npc/npc_crowd_d.glb index 5f93500..365fe78 100644 Binary files a/assets/models/blender/npc/npc_crowd_d.glb and b/assets/models/blender/npc/npc_crowd_d.glb differ diff --git a/assets/models/blender/npc/npc_crowd_d_ao.png b/assets/models/blender/npc/npc_crowd_d_ao.png index f4e3170..9b040c6 100644 Binary files a/assets/models/blender/npc/npc_crowd_d_ao.png and b/assets/models/blender/npc/npc_crowd_d_ao.png differ diff --git a/assets/models/blender/npc/npc_dress.glb b/assets/models/blender/npc/npc_dress.glb index 41811dc..f466e02 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 45cebb6..82a54b5 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 5f93500..365fe78 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 f4e3170..9b040c6 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 5f93500..365fe78 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 f4e3170..9b040c6 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 6cce457..79a39bb 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 27dc18e..2948cfe 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 41811dc..f466e02 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 45cebb6..82a54b5 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 5b81c81..e0db1c8 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 f4e3170..9b040c6 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/docs/blender_previews/sheet_npc_roster_0.9.9.jpg b/docs/blender_previews/sheet_npc_roster_0.9.9.jpg index 163c739..d359622 100644 Binary files a/docs/blender_previews/sheet_npc_roster_0.9.9.jpg and b/docs/blender_previews/sheet_npc_roster_0.9.9.jpg differ diff --git a/docs/blender_previews/sheet_npc_roster_crowd_0.9.9.jpg b/docs/blender_previews/sheet_npc_roster_crowd_0.9.9.jpg index cf9a765..99c9460 100644 Binary files a/docs/blender_previews/sheet_npc_roster_crowd_0.9.9.jpg and b/docs/blender_previews/sheet_npc_roster_crowd_0.9.9.jpg differ diff --git a/docs/blender_previews/sheet_npc_roster_views_0.9.9.jpg b/docs/blender_previews/sheet_npc_roster_views_0.9.9.jpg index a2f1562..e22acde 100644 Binary files a/docs/blender_previews/sheet_npc_roster_views_0.9.9.jpg and b/docs/blender_previews/sheet_npc_roster_views_0.9.9.jpg differ diff --git a/docs/npc-visual-review.md b/docs/npc-visual-review.md index 25ffb13..f8533cd 100644 --- a/docs/npc-visual-review.md +++ b/docs/npc-visual-review.md @@ -22,6 +22,7 @@ all build defects, not art. This revision covers what was fixed and what is stil | arms intersecting the body on `npc_dress`, `npc_jacket`, `npc_crowd_a`, `npc_crowd_d` | the baked rest aimed the upper arm straight down, which fails on the narrow bases | reduced — the arm rests 7° out from vertical (mesh width 0.180 → 0.220 on `casual`, 0.228 → 0.262 on `hoodie`) | | head gear did not sit on the skull | gear is authored around the procedural head centre (1.62); the Quaternius head centre is 1.69 | **resolved by removal** — see below | | performer cone swallowed the head | a 0.44 m cone authored for the old body | removed — the performer carries a baton and board in the hands | +| faces had **no mouth at all** | none of the three upstream heads carries a mouth material | fixed — a small lip block is placed on the measured skin surface for every base (dark maroon, not tinted) | ## Hats and head items @@ -91,6 +92,10 @@ Recorded as tasks in `TODO.md` under **Owner review findings**: 1. **Eyes have no whites.** Every base uses single-colour quads, so eyes read as dark rectangles at portrait distance. A light sclera plus pupil on the `Eye` slot would finish the face. +2. **Hands and hand-held gear are still authored for the old body.** The wrist moved from y 0.78 to + 1.017 and the arm got shorter, so the clipboard and the umbrella are oversized. A dedicated + prop-and-gear fit pass is queued in [`../TODO.md`](../TODO.md) under **Prop and gear fit pass + (new NPC attach points)** — it starts by teaching `fit` to measure hand gear at all. 2. **Arms still graze the torso on the `worker` base** — the 7° rest tilt cleared the `casual` base but not this one. 3. **The `npc_vest` clipboard is oversized** on the shorter Quaternius arm. diff --git a/tools/blender_assets.py b/tools/blender_assets.py index 33c080a..b326ae6 100644 --- a/tools/blender_assets.py +++ b/tools/blender_assets.py @@ -1587,6 +1587,65 @@ def _purge_unused_materials(obj): bpy.data.materials.remove(material) +def _add_quaternius_mouth(obj): + """Give every Quaternius head a readable mouth. + + None of the three upstream heads carries a mouth material: the face is skin + only, so at gameplay distance the characters read as brow-and-eye masks. A + small flat lip block is placed on the measured skin surface, which is why it + works for all three bases without per-source tuning. + """ + # This runs before the slot map, so the material is still the upstream `Skin`. + skin = next((i for i, m in enumerate(obj.data.materials) + if m is not None and _base_name(m.name).lower() == "skin"), None) + if skin is None: + return + band = [] + for poly in obj.data.polygons: + if poly.material_index != skin: + continue + for index in poly.vertices: + co = obj.data.vertices[index].co + if 1.600 <= co.z <= 1.632 and abs(co.x) <= 0.035: + band.append(co.y) + if not band: + return + front = min(band) # Blender -Y is the face; the lip sits on the surface + height = 1.616 + material = bpy.data.materials.new("mouth") + material.use_nodes = True + bsdf = material.node_tree.nodes.get("Principled BSDF") + if bsdf is not None: + # Blender node colours are linear; 0.16 linear reads as mauve once + # Tonemapped in Godot, so the lip is set well below the skin value. + bsdf.inputs["Base Color"].default_value = (0.055, 0.018, 0.02, 1.0) + bsdf.inputs["Roughness"].default_value = 0.6 + else: + print(" mouth: no Principled node, colour left at default") + material.diffuse_color = (0.055, 0.018, 0.02, 1.0) + bm = bmesh.new() + bmesh.ops.create_cube(bm, size=1.0) + for vert in bm.verts: + vert.co.x *= 0.058 + vert.co.y *= 0.012 + vert.co.z *= 0.011 + vert.co += Vector((0.0, front + 0.003, height)) + mouth = bpy.data.objects.new("Mouth", bpy.data.meshes.new("Mouth")) + bm.to_mesh(mouth.data) + bm.free() + mouth.data.materials.append(material) + bpy.context.scene.collection.objects.link(mouth) + for o in bpy.context.selected_objects: + o.select_set(False) + obj.select_set(True) + mouth.select_set(True) + bpy.context.view_layer.objects.active = obj + bpy.ops.object.join() + group = obj.vertex_groups.get("Head") or obj.vertex_groups.new(name="Head") + group.add([v.index for v in obj.data.vertices if not v.groups], 1.0, "REPLACE") + print(f" mouth placed at y {front + 0.003:+.3f} z {height:.3f}") + + def _base_name(name): """Strip Blender's `.001` duplicate suffix.""" head, sep, tail = name.rpartition(".") @@ -1998,6 +2057,7 @@ def _quaternius_tpose_glb(base, overrides): slots.update(overrides) if source.get("donor_head"): _transplant_worker_head(obj) + _add_quaternius_mouth(obj) _quaternius_slot_map(obj, slots) # Upstream UVs are degenerate (every loop at 0,0), so an AO bake would write