Copy the hoodie eye pair onto the Worker head and lift every hat except the firefighter helmet
This commit is contained in:
1 parent
a290fe83c8
commit
98f618c501
15 files changed
+79
-21
No files matched your search
+57
-9
@@ -1510,7 +1510,7 @@ QUATERNIUS_SOURCES = {
|
||||
"above": "hair", "below": "cloth"},
|
||||
# The bar, the oversized eyes and the hard hat are replaced by
|
||||
# `_transplant_worker_head()`; those materials are gone by here.
|
||||
"Eyebrows": "hair",
|
||||
"Eyebrows": "keep",
|
||||
"Brows": "keep",
|
||||
"Eye": "keep", "Moustache": "keep"},
|
||||
"donor_head": True,
|
||||
@@ -1534,24 +1534,59 @@ QUATERNIUS_SOURCES = {
|
||||
WORKER_HEAD_DONOR = {
|
||||
"source": "Casual_Hoodie.glb",
|
||||
# donor material -> contract slot name.
|
||||
"parts": {"Eyebrows": "Brows", "Hair": "hair"},
|
||||
"parts": {"Eyebrows": "Brows", "Eye": "Eye", "Hair": "hair"},
|
||||
# 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)},
|
||||
# Brows lift clear of the eye line; the eyes are pushed 4 mm forward so they
|
||||
# clear the Worker's flatter face instead of sinking into it.
|
||||
"offset": {"Brows": (0.0, 0.0, 0.020), "Eye": (0.0, -0.004, 0.0)},
|
||||
# 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},
|
||||
# The Worker's own eye quads sit inside the head and never show (the owner
|
||||
# reported the Worker roles as eyeless), so they are replaced by the hoodie's
|
||||
# pair, which is the same geometry the other eight characters use.
|
||||
"remove": {"Eyebrows": 1.715, "Worker_Yellow": 1.60, "Eye": None},
|
||||
"bone": "Head",
|
||||
}
|
||||
|
||||
|
||||
def _purge_unused_materials(obj):
|
||||
"""Drop material slots no face uses, so a donor can take the freed name.
|
||||
|
||||
`IDMaterials.pop()` repoints the polygons that used the slot, which corrupted
|
||||
the mesh the first time this was tried; rebuilding the list from the per-face
|
||||
names is explicit and leaves no dangling index.
|
||||
"""
|
||||
names = []
|
||||
for poly in obj.data.polygons:
|
||||
material = obj.data.materials[poly.material_index]
|
||||
names.append(material.name if material else "")
|
||||
kept, index_of = [], {}
|
||||
for name in names:
|
||||
if name in index_of:
|
||||
continue
|
||||
source = bpy.data.materials.get(name)
|
||||
if source is None:
|
||||
continue
|
||||
index_of[name] = len(kept)
|
||||
kept.append(source)
|
||||
if len(kept) == len(obj.data.materials):
|
||||
return
|
||||
obj.data.materials.clear()
|
||||
for material in kept:
|
||||
obj.data.materials.append(material)
|
||||
for poly, name in zip(obj.data.polygons, names):
|
||||
poly.material_index = index_of.get(name, 0)
|
||||
# A removed slot leaves its datablock behind with zero users, which still owns
|
||||
# the name and forces the donor's material to `Eye.001`.
|
||||
for material in [m for m in bpy.data.materials if m.users == 0]:
|
||||
bpy.data.materials.remove(material)
|
||||
|
||||
|
||||
def _base_name(name):
|
||||
"""Strip Blender's `.001` duplicate suffix."""
|
||||
head, sep, tail = name.rpartition(".")
|
||||
@@ -1588,6 +1623,8 @@ def _transplant_worker_head(obj):
|
||||
bm.to_mesh(obj.data)
|
||||
bm.free()
|
||||
print(f" removed {len(doomed)} Worker head faces")
|
||||
_purge_unused_materials(obj)
|
||||
|
||||
donor_path = QUATERNIUS_SRC / spec["source"]
|
||||
if not donor_path.is_file():
|
||||
print(f" head donor {donor_path.name} missing, Worker head left bare")
|
||||
@@ -1632,7 +1669,7 @@ def _transplant_worker_head(obj):
|
||||
kept.append(m)
|
||||
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 m is not None and _base_name(m.name) == slot_name]
|
||||
if not local:
|
||||
continue
|
||||
bm = bmesh.new()
|
||||
@@ -1649,7 +1686,7 @@ def _transplant_worker_head(obj):
|
||||
|
||||
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 m is not None and _base_name(m.name) == slot_name]
|
||||
if not local:
|
||||
continue
|
||||
bm = bmesh.new()
|
||||
@@ -1684,6 +1721,17 @@ def _transplant_worker_head(obj):
|
||||
donor.select_set(True)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
bpy.ops.object.join()
|
||||
# The join can leave `Eye.001` next to a zero-user `Eye`; the contract slot
|
||||
# names must be exact because the runtime tints by name.
|
||||
slot_names = {m.name for m in obj.data.materials if m is not None}
|
||||
for material in obj.data.materials:
|
||||
if material is None:
|
||||
continue
|
||||
base = _base_name(material.name)
|
||||
if base != material.name and base not in slot_names:
|
||||
slot_names.discard(material.name)
|
||||
material.name = base
|
||||
slot_names.add(base)
|
||||
group = obj.vertex_groups.get(spec["bone"]) or obj.vertex_groups.new(name=spec["bone"])
|
||||
group.add([v.index for v in obj.data.vertices if not v.groups], 1.0, "REPLACE")
|
||||
print(f" transplanted {', '.join(sorted(spec['parts'].values()))} "
|
||||
|
||||
Reference in new issue
Block a user