Prepare the modular part intake: inventory, extract, verify, join and the part contract
This commit is contained in:
1 parent
7a46963e3f
commit
f6bb2b1de3
9 files changed
+388
-10
No files matched your search
+47
-2
@@ -1604,6 +1604,47 @@ def _purge_unused_materials(obj):
|
||||
bpy.data.materials.remove(material)
|
||||
|
||||
|
||||
## Outfit/hair parts cut from the modular pack (see `tools/quaternius_parts.py`).
|
||||
## A part is one skinned mesh on the 62-joint rig; the builder joins it onto the base
|
||||
## body, which already owns the skeleton, so the part's weights are kept as-is.
|
||||
QUATERNIUS_PARTS_DIR = SRC_DIR / "quaternius" / "parts"
|
||||
|
||||
|
||||
def _join_quaternius_parts(obj, part_names):
|
||||
"""Join the named part files onto the body. No-ops until the pack is intaken."""
|
||||
joined = []
|
||||
for name in part_names:
|
||||
path = QUATERNIUS_PARTS_DIR / f"{name}.glb"
|
||||
if not path.is_file():
|
||||
print(f" part '{name}' not found in {QUATERNIUS_PARTS_DIR.relative_to(ROOT)}; skipped")
|
||||
continue
|
||||
before = set(o.name for o in bpy.context.scene.objects)
|
||||
bpy.ops.import_scene.gltf(filepath=str(path))
|
||||
added = [o for o in bpy.context.scene.objects if o.name not in before]
|
||||
meshes = [o for o in added if o.type == "MESH"]
|
||||
if not meshes:
|
||||
print(f" part '{name}' imported no mesh; skipped")
|
||||
continue
|
||||
# The part's own armature is redundant: the base body already has the skeleton,
|
||||
# and joining two armatures would duplicate every bone.
|
||||
for obj in added:
|
||||
if obj.type == "ARMATURE":
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
elif obj.type == "MESH" and obj not in meshes:
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
for o in bpy.context.selected_objects:
|
||||
o.select_set(False)
|
||||
obj.select_set(True)
|
||||
for mesh in meshes:
|
||||
if mesh.name in bpy.data.objects:
|
||||
mesh.select_set(True)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
bpy.ops.object.join()
|
||||
joined.append(name)
|
||||
if joined:
|
||||
print(f" joined parts: {', '.join(joined)}")
|
||||
|
||||
|
||||
def _add_quaternius_mouth(obj):
|
||||
"""Give every Quaternius head a readable mouth.
|
||||
|
||||
@@ -2158,7 +2199,7 @@ def _quaternius_work_dir():
|
||||
return path
|
||||
|
||||
|
||||
def _quaternius_tpose_glb(base, overrides, hair_style="short"):
|
||||
def _quaternius_tpose_glb(base, overrides, hair_style="short", parts=()):
|
||||
"""Export (and cache) the joined, slot-mapped, unwrapped T-pose base.
|
||||
|
||||
The arm-down bake only behaves symmetrically on our own export, because the
|
||||
@@ -2169,6 +2210,8 @@ def _quaternius_tpose_glb(base, overrides, hair_style="short"):
|
||||
# The cache key has to cover the slot overrides: two roles can share a source
|
||||
# and still need different slot maps (the delivery vest keeps its hi-vis).
|
||||
key = base + "-" + hair_style
|
||||
if parts:
|
||||
key += "-" + "-".join(parts)
|
||||
if overrides:
|
||||
key += "-" + "-".join(f"{k}:{v}" for k, v in sorted(overrides.items()))
|
||||
path = _quaternius_work_dir() / f"tpose_{key}.glb"
|
||||
@@ -2205,6 +2248,7 @@ def _quaternius_tpose_glb(base, overrides, hair_style="short"):
|
||||
|
||||
slots = dict(source["slots"])
|
||||
slots.update(overrides)
|
||||
_join_quaternius_parts(obj, parts)
|
||||
if source.get("donor_head"):
|
||||
_transplant_worker_head(obj)
|
||||
_add_quaternius_mouth(obj)
|
||||
@@ -2264,7 +2308,8 @@ def build_quaternius_character(name):
|
||||
"""Bake one roster character from its CC0 Quaternius source."""
|
||||
spec = CHARACTERS[name]
|
||||
tpose = _quaternius_tpose_glb(spec["base"], spec.get("slots", {}),
|
||||
str(spec.get("hair", "short")))
|
||||
str(spec.get("hair", "short")),
|
||||
tuple(spec.get("parts", ())))
|
||||
reset_scene()
|
||||
bpy.ops.import_scene.gltf(filepath=str(tpose))
|
||||
arm = next(o for o in bpy.context.scene.objects if o.type == "ARMATURE")
|
||||
|
||||
Reference in new issue
Block a user