diff --git a/TODO.md b/TODO.md index 8d39477..28b41a1 100644 --- a/TODO.md +++ b/TODO.md @@ -887,11 +887,14 @@ gear) and 5 (head gear on the skull). head, lifted 18 mm clear of the eye line (`WORKER_HEAD_DONOR`). - [x] Worker crown reads as a hard hat: the hoodie's hair is transplanted onto the Worker head and the hard-hat faces above the collar are cut away, so guard/responder/crowd_d have hair. -- [ ] Worker roles now have **no eyes** (the eye quads were the dark bar that read as a unibrow). - Add the `casual` base's smaller eye quads, or a light sclera + pupil, so the face is not - brow-and-moustache only. -- [ ] Hair pokes through the `firefighter_helmet` dome, which now sits under a taller hair shell; - re-fit the helmet like the head gear below. +- [x] Worker faces: the dark "unibrow" band turned out to be the **cap brim** sitting on the brow, + not head geometry. `HEAD_GEAR_LIFT` went 0.07 → 0.103 (the gear is authored around the head + *centre* at 1.62, the Quaternius head centre is 1.69) and `GEAR_SCALE` grows the cap/hood so + they clear the 0.228 m hair shell. The cap now reads as a cap and both eyes *and* brows show. +- [x] Hair poking through head gear: fixed by the same lift + cap/hood scale; the firefighter + helmet dome covers the hair, and the cap reads as a cap rather than a visor. +- [ ] The Worker eye quads are still upstream-sized; they read as two dark rectangles rather than + eyes with whites. A light sclera + pupil texture on the `Eye` slot would finish the face. - [-] Arm/torso intersection on `npc_dress`/`npc_jacket`/`npc_crowd_a`/`npc_crowd_d`: the baked rest now aims the upper arm 7° out from vertical. Verified clear on the `casual` base; a light clip remains on `worker`, so this needs either a wider angle or a per-base shoulder diff --git a/assets/models/blender/manifest.json b/assets/models/blender/manifest.json index 173912d..04b4b13 100644 --- a/assets/models/blender/manifest.json +++ b/assets/models/blender/manifest.json @@ -122,12 +122,12 @@ "bounds_godot": { "max": [ 0.108, - 0.204, + 0.237, 0.165 ], "min": [ -0.108, - 0.094, + 0.127, -0.119 ] }, @@ -265,12 +265,12 @@ "bounds_godot": { "max": [ 0.166, - 0.227, + 0.26, 0.166 ], "min": [ -0.166, - 0.092, + 0.125, -0.225 ] }, @@ -334,12 +334,12 @@ "bounds_godot": { "max": [ 0.118, - 0.221, + 0.254, 0.079 ], "min": [ -0.118, - -0.024, + 0.009, -0.144 ] }, @@ -655,7 +655,9 @@ "skin", "cloth", "accessory", + "hair.001", "Moustache", + "Eye", "pants", "Brows", "hair" @@ -673,8 +675,8 @@ "accessory", "hair" ], - "triangles": 6176, - "vertices": 13074 + "triangles": 6254, + "vertices": 13198 }, "npc_dress": { "base": "casual", @@ -736,7 +738,9 @@ "skin", "cloth", "accessory", + "hair.001", "Moustache", + "Eye", "pants", "Brows", "hair" @@ -753,8 +757,8 @@ "accessory", "hair" ], - "triangles": 6176, - "vertices": 13074 + "triangles": 6254, + "vertices": 13198 }, "npc_guard": { "base": "worker", @@ -776,7 +780,9 @@ "skin", "cloth", "accessory", + "hair.001", "Moustache", + "Eye", "pants", "Brows", "hair" @@ -794,8 +800,8 @@ "accessory", "hair" ], - "triangles": 6176, - "vertices": 13074 + "triangles": 6254, + "vertices": 13198 }, "npc_hoodie": { "base": "hoodie", @@ -902,7 +908,9 @@ "Worker_Yellow", "cloth", "accessory", + "hair.001", "Moustache", + "Eye", "pants", "Brows", "hair" @@ -919,8 +927,8 @@ "accessory", "hair" ], - "triangles": 6176, - "vertices": 13074 + "triangles": 6254, + "vertices": 13198 }, "palm_tree": { "bounds_godot": { @@ -1245,12 +1253,12 @@ "bounds_godot": { "max": [ 0.065, - 0.108, + 0.141, 0.101 ], "min": [ -0.065, - 0.077, + 0.11, 0.002 ] }, diff --git a/assets/models/blender/npc/npc_crowd_d.glb b/assets/models/blender/npc/npc_crowd_d.glb index faf7ded..7e2dfd4 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 f94533c..5bad0ac 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_firefighter.glb b/assets/models/blender/npc/npc_firefighter.glb index faf7ded..7e2dfd4 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 f94533c..5bad0ac 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 faf7ded..7e2dfd4 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 f94533c..5bad0ac 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_vest.glb b/assets/models/blender/npc/npc_vest.glb index 095d959..1f0ad29 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 f94533c..5bad0ac 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/cap.glb b/assets/models/blender/npc_gear/cap.glb index f52f3d2..fe92ac3 100644 Binary files a/assets/models/blender/npc_gear/cap.glb and b/assets/models/blender/npc_gear/cap.glb differ diff --git a/assets/models/blender/npc_gear/cap_ao.png b/assets/models/blender/npc_gear/cap_ao.png index 75bd4f5..c6cbc1c 100644 Binary files a/assets/models/blender/npc_gear/cap_ao.png and b/assets/models/blender/npc_gear/cap_ao.png differ diff --git a/assets/models/blender/npc_gear/firefighter_helmet.glb b/assets/models/blender/npc_gear/firefighter_helmet.glb index db0ba0e..47bc0ba 100644 Binary files a/assets/models/blender/npc_gear/firefighter_helmet.glb and b/assets/models/blender/npc_gear/firefighter_helmet.glb differ diff --git a/assets/models/blender/npc_gear/firefighter_helmet_ao.png b/assets/models/blender/npc_gear/firefighter_helmet_ao.png index b0bfc9a..12e349a 100644 Binary files a/assets/models/blender/npc_gear/firefighter_helmet_ao.png and b/assets/models/blender/npc_gear/firefighter_helmet_ao.png differ diff --git a/assets/models/blender/npc_gear/hood.glb b/assets/models/blender/npc_gear/hood.glb index 9eefab3..8b395bf 100644 Binary files a/assets/models/blender/npc_gear/hood.glb and b/assets/models/blender/npc_gear/hood.glb differ diff --git a/assets/models/blender/npc_gear/hood_ao.png b/assets/models/blender/npc_gear/hood_ao.png index 8610b44..b11f819 100644 Binary files a/assets/models/blender/npc_gear/hood_ao.png and b/assets/models/blender/npc_gear/hood_ao.png differ diff --git a/assets/models/blender/npc_gear/sunglasses.glb b/assets/models/blender/npc_gear/sunglasses.glb index cd276d1..f0f7715 100644 Binary files a/assets/models/blender/npc_gear/sunglasses.glb and b/assets/models/blender/npc_gear/sunglasses.glb differ diff --git a/assets/models/blender/npc_gear/sunglasses_ao.png b/assets/models/blender/npc_gear/sunglasses_ao.png index baba00a..3acd14f 100644 Binary files a/assets/models/blender/npc_gear/sunglasses_ao.png and b/assets/models/blender/npc_gear/sunglasses_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 49ec2d6..c5f1327 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 9824af9..f6f52a9 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 65dde9b..6a80130 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 8514fe2..6a2daab 100644 --- a/docs/npc-visual-review.md +++ b/docs/npc-visual-review.md @@ -77,12 +77,19 @@ the bare tint: | the dark bar was the Worker's *eye* quads, not only the brow bar | the eye quads are removed too; the face now reads brows + moustache | | performer cone swallowed the head, and the performer/protester props floated beside the body | `_add_props()` now parents carried props to the hand proxies and the cone shrank from a 0.44 m to a 0.25 m base | +Third pass traced the remaining "black bar across the eyes" to the **cap brim**, not to head +geometry: head gear is authored around the procedural head centre (1.62) but the Quaternius head +centre sits at 1.69, so every cap sat 7 cm low with its brim across the brows. `HEAD_GEAR_LIFT` +is now 0.103 and `GEAR_SCALE` grows the cap/hood to clear the 0.228 m hair shell. The vision judge +confirms on `npc_guard`: no bar or visor, eyes and brows separately visible, the cap sits on the +head, no holes. + Re-verified after the fix with the sheets below and the vision judge: no holes or missing geometry on any of the three Worker-based roles. -**Still open** — checklist rows 3, 4 and 5 plus the list below: the Worker crown still reads as a -hard hat because that is upstream geometry, the eye/brow mass still reads heavy at portrait -distance, and the arms still graze the torso on the `worker` base. +**Still open** — checkpoint row 4 (hand-held gear) and the list below: the eye quads have no +whites, the `npc_vest` clipboard is oversized on the shorter arm, and the arms still graze the +torso on the `worker` base. Found by user: diff --git a/src/game/npc_avatar.gd b/src/game/npc_avatar.gd index e909081..bd60495 100644 --- a/src/game/npc_avatar.gd +++ b/src/game/npc_avatar.gd @@ -178,6 +178,19 @@ const BLENDER_GEAR_DIR := "res://assets/models/blender/npc_gear/" const GEAR_OFFSETS := { "lanyard": Vector3(0, 0, 0.04), "firefighter_helmet": Vector3(0, 0.03, 0), + # The Quaternius head wears a 0.24 m hair shell that stands ~3.5 cm above the + # cap crown, so the cap sinks into the hair and only its brim shows. Lift the + # cap and hood onto the hair; sunglasses stay at the eye line. + "cap": Vector3(0, 0.05, 0), + "hood": Vector3(0, 0.03, 0), +} +## The hat geometry is authored around the 0.315 m procedural head and scaled to +## 0.64 in the Blender build to match the 0.20 m Quaternius skull. That fits the +## skull but not the 0.228 m hair shell on top of it, so a cap sinks into the hair +## and only its brim shows. These scale the head-gear node instead of the mesh. +const GEAR_SCALE := { + "cap": 1.22, + "hood": 1.12, } ## Hand gear is authored around the wrist proxy. The palm sits ~4 cm in front ## of that origin, so without this offset the first phone/flashlight versions @@ -808,6 +821,8 @@ func _attach_gear(gear_name: String) -> bool: node.position += GEAR_OFFSETS[gear_name] if str(GEAR_ATTACH[gear_name]) == "hand": node.position += HAND_ITEM_FORWARD + if GEAR_SCALE.has(gear_name): + node.scale = Vector3.ONE * float(GEAR_SCALE[gear_name]) _detail_nodes.append(node) var ao := Materials.load_ao(BLENDER_GEAR_DIR + gear_name + "_ao.png") for child in node.find_children("*", "MeshInstance3D", true, false): diff --git a/tools/blender_assets.py b/tools/blender_assets.py index f707500..756763d 100644 --- a/tools/blender_assets.py +++ b/tools/blender_assets.py @@ -152,7 +152,11 @@ HEAD_BAND = (1.55, 1.81) # face/headgear height band in Godot metres # Quaternius roster has a 0.20 m head whose centre sits higher (see # `build_gear`). 0.64 = 0.201 / 0.315. HEAD_GEAR_SCALE = 0.64 -HEAD_GEAR_LIFT = 0.07 +# The gear origin is the Head *bone* (Godot y 1.587 on the Quaternius rig), but the +# gear is authored around the head *centre* (1.62 in the procedural library). The +# Quaternius head centre sits at 1.69, so head gear needs +0.103 or the cap brim +# lands on the brow and reads as a black visor across the eyes. +HEAD_GEAR_LIFT = 0.103 FIT_HEAD_MATERIALS = ("skin",) FIT_CLOTHING_MATERIAL = "cloth" FIT_CLOTHING_BAND = (0.30, 0.52) # hip/skirt band; captures coat/dress flare @@ -1529,14 +1533,21 @@ QUATERNIUS_SOURCES = { ## remapped, because the Worker's vest *is* its torso and its hat *is* its crown. WORKER_HEAD_DONOR = { "source": "Casual_Hoodie.glb", - # donor material -> contract slot name. The hoodie's eyes are not used: this - # head's eye quads are large enough to read as one dark bar, which is the - # "unibrow" the owner saw. The brows and hair carry the face instead. + # donor material -> contract slot name. "parts": {"Eyebrows": "Brows", "Hair": "hair"}, - # Lift the brows clear of the eye line (the two heads differ in mesh density). - "lift": {"Brows": 0.018}, - # our material -> height rule for the faces that go away - "remove": {"Eyebrows": None, "Eye": None, "Worker_Yellow": 1.60}, + # The three upstream heads carry the same-sized eye quads (x +-0.057), but the + # Worker's face is flatter, so unchecked they sit proud of the skin and merge + # into one dark bar -- the "unibrow" the owner saw. Set them back into the + # socket so the skin crops their edges, and lift the brows clear of them. + "offset": {"Brows": (0.0, 0.0, 0.020)}, + # Our material -> the height below which its faces go away. `Eyebrows` is the + # bar *and* the forehead shell, so only the bar is cut: removing the whole + # material opened the skull and the interior rendered as a black band across + # the brows. `Eye` goes entirely (its quads read as one dark mass). + # `Eye` is deliberately *not* removed: this head has open sockets and the eye + # quads are the only thing filling them, so deleting them showed the inside of + # the head as a black band. They stay; the donor brows sit above them. + "remove": {"Eyebrows": 1.715, "Worker_Yellow": 1.60}, "bone": "Head", } @@ -1577,16 +1588,6 @@ def _transplant_worker_head(obj): bm.to_mesh(obj.data) bm.free() print(f" removed {len(doomed)} Worker head faces") - # Free the slot names so the donor's materials can take them. `pop` removes a - # slot by index and repoints the polygons that used it. - for index in range(len(obj.data.materials) - 1, -1, -1): - slot = obj.data.materials[index] - if slot is None or slot.name not in spec["remove"]: - continue - if any(poly.material_index == index for poly in obj.data.polygons): - continue - obj.data.materials.pop(index=index) - donor_path = QUATERNIUS_SRC / spec["source"] if not donor_path.is_file(): print(f" head donor {donor_path.name} missing, Worker head left bare") @@ -1629,7 +1630,7 @@ def _transplant_worker_head(obj): m.name = slot remap[index] = len(kept) kept.append(m) - for slot_name, lift in spec.get("lift", {}).items(): + for slot_name, rule in spec.get("narrow", {}).items(): local = [i for i, m in enumerate(candidate.data.materials) if m is not None and m.name == slot_name] if not local: @@ -1638,8 +1639,26 @@ def _transplant_worker_head(obj): bm.from_mesh(candidate.data) bm.faces.ensure_lookup_table() moved = {v for f in bm.faces if f.material_index in local for v in f.verts} + centre = float(rule["centre"]) + factor = float(rule["scale"]) for v in moved: - v.co.z += float(lift) + box = centre if v.co.x >= 0.0 else -centre + v.co.x = box + (v.co.x - box) * factor + bm.to_mesh(candidate.data) + bm.free() + + for slot_name, offset in spec.get("offset", {}).items(): + local = [i for i, m in enumerate(candidate.data.materials) + if m is not None and m.name == slot_name] + if not local: + continue + bm = bmesh.new() + bm.from_mesh(candidate.data) + bm.faces.ensure_lookup_table() + moved = {v for f in bm.faces if f.material_index in local for v in f.verts} + delta = Vector(offset) + for v in moved: + v.co += delta bm.to_mesh(candidate.data) bm.free() face_slots = [remap.get(poly.material_index, 0)