Lift and scale head gear for the Quaternius head so the cap stops reading as a visor
No files matched your search
@@ -887,11 +887,14 @@ gear) and 5 (head gear on the skull).
|
|||||||
head, lifted 18 mm clear of the eye line (`WORKER_HEAD_DONOR`).
|
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
|
- [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.
|
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).
|
- [x] Worker faces: the dark "unibrow" band turned out to be the **cap brim** sitting on the brow,
|
||||||
Add the `casual` base's smaller eye quads, or a light sclera + pupil, so the face is not
|
not head geometry. `HEAD_GEAR_LIFT` went 0.07 → 0.103 (the gear is authored around the head
|
||||||
brow-and-moustache only.
|
*centre* at 1.62, the Quaternius head centre is 1.69) and `GEAR_SCALE` grows the cap/hood so
|
||||||
- [ ] Hair pokes through the `firefighter_helmet` dome, which now sits under a taller hair shell;
|
they clear the 0.228 m hair shell. The cap now reads as a cap and both eyes *and* brows show.
|
||||||
re-fit the helmet like the head gear below.
|
- [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
|
- [-] 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
|
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
|
light clip remains on `worker`, so this needs either a wider angle or a per-base shoulder
|
||||||
|
|||||||
@@ -122,12 +122,12 @@
|
|||||||
"bounds_godot": {
|
"bounds_godot": {
|
||||||
"max": [
|
"max": [
|
||||||
0.108,
|
0.108,
|
||||||
0.204,
|
0.237,
|
||||||
0.165
|
0.165
|
||||||
],
|
],
|
||||||
"min": [
|
"min": [
|
||||||
-0.108,
|
-0.108,
|
||||||
0.094,
|
0.127,
|
||||||
-0.119
|
-0.119
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -265,12 +265,12 @@
|
|||||||
"bounds_godot": {
|
"bounds_godot": {
|
||||||
"max": [
|
"max": [
|
||||||
0.166,
|
0.166,
|
||||||
0.227,
|
0.26,
|
||||||
0.166
|
0.166
|
||||||
],
|
],
|
||||||
"min": [
|
"min": [
|
||||||
-0.166,
|
-0.166,
|
||||||
0.092,
|
0.125,
|
||||||
-0.225
|
-0.225
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -334,12 +334,12 @@
|
|||||||
"bounds_godot": {
|
"bounds_godot": {
|
||||||
"max": [
|
"max": [
|
||||||
0.118,
|
0.118,
|
||||||
0.221,
|
0.254,
|
||||||
0.079
|
0.079
|
||||||
],
|
],
|
||||||
"min": [
|
"min": [
|
||||||
-0.118,
|
-0.118,
|
||||||
-0.024,
|
0.009,
|
||||||
-0.144
|
-0.144
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -655,7 +655,9 @@
|
|||||||
"skin",
|
"skin",
|
||||||
"cloth",
|
"cloth",
|
||||||
"accessory",
|
"accessory",
|
||||||
|
"hair.001",
|
||||||
"Moustache",
|
"Moustache",
|
||||||
|
"Eye",
|
||||||
"pants",
|
"pants",
|
||||||
"Brows",
|
"Brows",
|
||||||
"hair"
|
"hair"
|
||||||
@@ -673,8 +675,8 @@
|
|||||||
"accessory",
|
"accessory",
|
||||||
"hair"
|
"hair"
|
||||||
],
|
],
|
||||||
"triangles": 6176,
|
"triangles": 6254,
|
||||||
"vertices": 13074
|
"vertices": 13198
|
||||||
},
|
},
|
||||||
"npc_dress": {
|
"npc_dress": {
|
||||||
"base": "casual",
|
"base": "casual",
|
||||||
@@ -736,7 +738,9 @@
|
|||||||
"skin",
|
"skin",
|
||||||
"cloth",
|
"cloth",
|
||||||
"accessory",
|
"accessory",
|
||||||
|
"hair.001",
|
||||||
"Moustache",
|
"Moustache",
|
||||||
|
"Eye",
|
||||||
"pants",
|
"pants",
|
||||||
"Brows",
|
"Brows",
|
||||||
"hair"
|
"hair"
|
||||||
@@ -753,8 +757,8 @@
|
|||||||
"accessory",
|
"accessory",
|
||||||
"hair"
|
"hair"
|
||||||
],
|
],
|
||||||
"triangles": 6176,
|
"triangles": 6254,
|
||||||
"vertices": 13074
|
"vertices": 13198
|
||||||
},
|
},
|
||||||
"npc_guard": {
|
"npc_guard": {
|
||||||
"base": "worker",
|
"base": "worker",
|
||||||
@@ -776,7 +780,9 @@
|
|||||||
"skin",
|
"skin",
|
||||||
"cloth",
|
"cloth",
|
||||||
"accessory",
|
"accessory",
|
||||||
|
"hair.001",
|
||||||
"Moustache",
|
"Moustache",
|
||||||
|
"Eye",
|
||||||
"pants",
|
"pants",
|
||||||
"Brows",
|
"Brows",
|
||||||
"hair"
|
"hair"
|
||||||
@@ -794,8 +800,8 @@
|
|||||||
"accessory",
|
"accessory",
|
||||||
"hair"
|
"hair"
|
||||||
],
|
],
|
||||||
"triangles": 6176,
|
"triangles": 6254,
|
||||||
"vertices": 13074
|
"vertices": 13198
|
||||||
},
|
},
|
||||||
"npc_hoodie": {
|
"npc_hoodie": {
|
||||||
"base": "hoodie",
|
"base": "hoodie",
|
||||||
@@ -902,7 +908,9 @@
|
|||||||
"Worker_Yellow",
|
"Worker_Yellow",
|
||||||
"cloth",
|
"cloth",
|
||||||
"accessory",
|
"accessory",
|
||||||
|
"hair.001",
|
||||||
"Moustache",
|
"Moustache",
|
||||||
|
"Eye",
|
||||||
"pants",
|
"pants",
|
||||||
"Brows",
|
"Brows",
|
||||||
"hair"
|
"hair"
|
||||||
@@ -919,8 +927,8 @@
|
|||||||
"accessory",
|
"accessory",
|
||||||
"hair"
|
"hair"
|
||||||
],
|
],
|
||||||
"triangles": 6176,
|
"triangles": 6254,
|
||||||
"vertices": 13074
|
"vertices": 13198
|
||||||
},
|
},
|
||||||
"palm_tree": {
|
"palm_tree": {
|
||||||
"bounds_godot": {
|
"bounds_godot": {
|
||||||
@@ -1245,12 +1253,12 @@
|
|||||||
"bounds_godot": {
|
"bounds_godot": {
|
||||||
"max": [
|
"max": [
|
||||||
0.065,
|
0.065,
|
||||||
0.108,
|
0.141,
|
||||||
0.101
|
0.101
|
||||||
],
|
],
|
||||||
"min": [
|
"min": [
|
||||||
-0.065,
|
-0.065,
|
||||||
0.077,
|
0.11,
|
||||||
0.002
|
0.002
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 214 KiB After Width: | Height: | Size: 217 KiB |
@@ -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 |
|
| 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 |
|
| 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
|
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.
|
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
|
**Still open** — checkpoint row 4 (hand-held gear) and the list below: the eye quads have no
|
||||||
hard hat because that is upstream geometry, the eye/brow mass still reads heavy at portrait
|
whites, the `npc_vest` clipboard is oversized on the shorter arm, and the arms still graze the
|
||||||
distance, and the arms still graze the torso on the `worker` base.
|
torso on the `worker` base.
|
||||||
|
|
||||||
Found by user:
|
Found by user:
|
||||||
|
|
||||||
|
|||||||
@@ -178,6 +178,19 @@ const BLENDER_GEAR_DIR := "res://assets/models/blender/npc_gear/"
|
|||||||
const GEAR_OFFSETS := {
|
const GEAR_OFFSETS := {
|
||||||
"lanyard": Vector3(0, 0, 0.04),
|
"lanyard": Vector3(0, 0, 0.04),
|
||||||
"firefighter_helmet": Vector3(0, 0.03, 0),
|
"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
|
## 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
|
## 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]
|
node.position += GEAR_OFFSETS[gear_name]
|
||||||
if str(GEAR_ATTACH[gear_name]) == "hand":
|
if str(GEAR_ATTACH[gear_name]) == "hand":
|
||||||
node.position += HAND_ITEM_FORWARD
|
node.position += HAND_ITEM_FORWARD
|
||||||
|
if GEAR_SCALE.has(gear_name):
|
||||||
|
node.scale = Vector3.ONE * float(GEAR_SCALE[gear_name])
|
||||||
_detail_nodes.append(node)
|
_detail_nodes.append(node)
|
||||||
var ao := Materials.load_ao(BLENDER_GEAR_DIR + gear_name + "_ao.png")
|
var ao := Materials.load_ao(BLENDER_GEAR_DIR + gear_name + "_ao.png")
|
||||||
for child in node.find_children("*", "MeshInstance3D", true, false):
|
for child in node.find_children("*", "MeshInstance3D", true, false):
|
||||||
|
|||||||
@@ -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
|
# Quaternius roster has a 0.20 m head whose centre sits higher (see
|
||||||
# `build_gear`). 0.64 = 0.201 / 0.315.
|
# `build_gear`). 0.64 = 0.201 / 0.315.
|
||||||
HEAD_GEAR_SCALE = 0.64
|
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_HEAD_MATERIALS = ("skin",)
|
||||||
FIT_CLOTHING_MATERIAL = "cloth"
|
FIT_CLOTHING_MATERIAL = "cloth"
|
||||||
FIT_CLOTHING_BAND = (0.30, 0.52) # hip/skirt band; captures coat/dress flare
|
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.
|
## remapped, because the Worker's vest *is* its torso and its hat *is* its crown.
|
||||||
WORKER_HEAD_DONOR = {
|
WORKER_HEAD_DONOR = {
|
||||||
"source": "Casual_Hoodie.glb",
|
"source": "Casual_Hoodie.glb",
|
||||||
# donor material -> contract slot name. The hoodie's eyes are not used: this
|
# donor material -> contract slot name.
|
||||||
# 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.
|
|
||||||
"parts": {"Eyebrows": "Brows", "Hair": "hair"},
|
"parts": {"Eyebrows": "Brows", "Hair": "hair"},
|
||||||
# Lift the brows clear of the eye line (the two heads differ in mesh density).
|
# The three upstream heads carry the same-sized eye quads (x +-0.057), but the
|
||||||
"lift": {"Brows": 0.018},
|
# Worker's face is flatter, so unchecked they sit proud of the skin and merge
|
||||||
# our material -> height rule for the faces that go away
|
# into one dark bar -- the "unibrow" the owner saw. Set them back into the
|
||||||
"remove": {"Eyebrows": None, "Eye": None, "Worker_Yellow": 1.60},
|
# 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",
|
"bone": "Head",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1577,16 +1588,6 @@ def _transplant_worker_head(obj):
|
|||||||
bm.to_mesh(obj.data)
|
bm.to_mesh(obj.data)
|
||||||
bm.free()
|
bm.free()
|
||||||
print(f" removed {len(doomed)} Worker head faces")
|
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"]
|
donor_path = QUATERNIUS_SRC / spec["source"]
|
||||||
if not donor_path.is_file():
|
if not donor_path.is_file():
|
||||||
print(f" head donor {donor_path.name} missing, Worker head left bare")
|
print(f" head donor {donor_path.name} missing, Worker head left bare")
|
||||||
@@ -1629,7 +1630,7 @@ def _transplant_worker_head(obj):
|
|||||||
m.name = slot
|
m.name = slot
|
||||||
remap[index] = len(kept)
|
remap[index] = len(kept)
|
||||||
kept.append(m)
|
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)
|
local = [i for i, m in enumerate(candidate.data.materials)
|
||||||
if m is not None and m.name == slot_name]
|
if m is not None and m.name == slot_name]
|
||||||
if not local:
|
if not local:
|
||||||
@@ -1638,8 +1639,26 @@ def _transplant_worker_head(obj):
|
|||||||
bm.from_mesh(candidate.data)
|
bm.from_mesh(candidate.data)
|
||||||
bm.faces.ensure_lookup_table()
|
bm.faces.ensure_lookup_table()
|
||||||
moved = {v for f in bm.faces if f.material_index in local for v in f.verts}
|
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:
|
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.to_mesh(candidate.data)
|
||||||
bm.free()
|
bm.free()
|
||||||
face_slots = [remap.get(poly.material_index, 0)
|
face_slots = [remap.get(poly.material_index, 0)
|
||||||
|
|||||||