Roll out seven modular outfit bases: eleven distinct outfits across twelve roles
Full build / Import + headless tests (push) Successful in 41s
Full build / Full desktop build + optional release (push) Skipped

This commit is contained in:
zwitschi committed 2026-11-06 19:37:11 +01:00
1 parent 3b9e3b61d8
commit b18236f9b4
27 files changed
+267 -88

No files matched your search

+106 -6
View File
@@ -95,7 +95,7 @@ CHARACTERS = {
"base": "hoodie",
"roles": ["tourist", "confused_visitor", "intoxicated", "guest"]},
"npc_jacket": {"layer": "jacket", "hair": "short", "gear": ["lanyard", "clipboard"],
"base": "casual",
"base": "casual_2",
"roles": ["authorized_staff", "protester", "guest"]},
"npc_coat": {"layer": "coat", "hair": "short", "base": "suit",
"gear": ["sunglasses", "shoulder_bag", "phone"],
@@ -107,10 +107,10 @@ CHARACTERS = {
"slots": {"Worker_Yellow": {"split": True, "z": 1.65,
"above": "hair", "below": "keep"}},
"roles": ["delivery"]},
"npc_dress": {"layer": "dress", "hair": "long", "build": "slim", "base": "casual",
"npc_dress": {"layer": "dress", "hair": "long", "build": "slim", "base": "king",
"gear": ["shoulder_bag"],
"roles": ["performer"]},
"npc_hoodie": {"layer": "hood", "hair": "short", "gear": ["cup"], "base": "hoodie",
"npc_hoodie": {"layer": "hood", "hair": "short", "gear": ["cup"], "base": "punk",
"roles": ["trespasser", "mental_crisis", "violent_threat"]},
"npc_firefighter": {"layer": "responder", "hair": "buzz", "build": "broad",
"base": "worker",
@@ -122,12 +122,12 @@ CHARACTERS = {
"roles": ["player_guard", "venue_security"]},
# Crowd-variety prototypes (character development): the three Quaternius
# sources are spread across the crowd pools so a crowd never shows one mesh.
"npc_crowd_a": {"layer": "none", "hair": "short", "build": "slim", "base": "casual",
"npc_crowd_a": {"layer": "none", "hair": "long", "build": "slim", "base": "beach",
"gear": ["sunglasses"], "roles": ["tourist", "guest"]},
"npc_crowd_b": {"layer": "none", "hair": "buzz", "build": "broad", "base": "casual",
"npc_crowd_b": {"layer": "none", "hair": "short", "build": "broad", "base": "farmer",
"beard": True, "gear": ["shoulder_bag"],
"roles": ["confused_visitor", "guest"]},
"npc_crowd_c": {"layer": "coat", "hair": "long", "base": "hoodie",
"npc_crowd_c": {"layer": "coat", "hair": "short", "base": "adventurer",
"gear": ["umbrella"], "roles": ["intoxicated", "guest"]},
"npc_crowd_d": {"layer": "hood", "hair": "short", "build": "slim",
"base": "worker",
@@ -1513,6 +1513,61 @@ QUATERNIUS_SOURCES = {
"Eye": "keep", "Eyebrows": "keep"},
"notes": "hoodie + shorts + white shoes (the hoodie covers the shoe uppers)",
},
"adventurer": {
"file": "Adventurer.glb",
"slots": {"Skin": "skin", "Suit": "cloth", "Suit_legs": "pants",
"Hair": "hair", "Grey": "accessory", "Black": "accessory",
"LightGreen": "keep", "Gold": "keep", "Brown": "keep", "White": "keep"},
"notes": "adventurer jacket + trousers, boots, satchel; gold buckles fixed",
},
"beach": {
"file": "Beach.glb",
"slots": {"Skin": "skin", "Skin_legs": "skin", "LightBrown": "cloth",
"Red_Dark_legs": "pants", "White_legs": "pants",
"Red_Dark": "accessory", "Hair": "hair", "Earrings": "keep"},
"notes": "beach shirt and shorts, bare legs, flip-flops",
},
"casual_2": {
"file": "Casual_2.glb",
"slots": {"Skin": "skin", "LightBrown": "cloth", "LightBlue_legs": "pants",
"Red_Dark": "accessory", "White": "accessory", "Hair": "hair",
"Skin_Darker": "keep", "Eye": "keep", "Eyebrows": "keep"},
"notes": "second casual outfit: jersey, jeans, sneakers",
},
"farmer": {
"file": "Farmer.glb",
"slots": {"Skin": "skin", "Brown": "cloth", "LightBlue.001": "pants",
"Brown.001": "accessory", "Brown2": "accessory",
"LightBlue": "keep", "Beige": "keep", "Beige.001": "keep",
"Eyebrows": "keep"},
"patch_slots": ["hair"],
"notes": "farmer shirt + overalls + straw hat; no hair shell, so the slot is patched",
},
"king": {
"file": "King.glb",
"slots": {"Skin": "skin", "Metal": "cloth", "Metal_legs": "pants",
"DarkBrown_legs": "pants", "Metal.001": "accessory",
"Hair_White": "hair", "Gold": "keep", "Metal_Dark": "keep",
"Metal_Dark_legs": "keep", "Blue": "keep"},
"notes": "royal armour and cape with a gold crown; stage-performer look",
},
"punk": {
"file": "Punk.glb",
"slots": {"Skin": "skin", "Black": "cloth", "LightBlue_legs": "pants",
"Black.001": "accessory", "Red": "hair",
"White": "keep", "Earrings": "keep", "Red_Dark": "keep",
"Eye": "keep", "Eyebrows": "keep"},
"notes": "punk jacket and jeans; the mohawk is the `Red` material, so it is the hair slot",
},
"swat": {
"file": "Swat.glb",
"slots": {"Skin": "skin", "Swat_Black": "cloth", "Swat_legs": "pants",
"Swat_Black_legs": "pants", "Swat_Black.001": "accessory",
"Swat": "keep", "Swat.001": "keep", "Swat_Black.002": "keep",
"Visor": "keep"},
"patch_slots": ["hair"],
"notes": "tactical uniform with helmet and visor; no hair shell, so the slot is patched",
},
"suit": {
"file": "Suit.glb",
"slots": {"Skin": "skin", "Suit": "cloth", "Suit_legs": "pants",
@@ -1845,6 +1900,50 @@ def _apply_hair_part(obj, style, base):
print(f" hair: {style} scale {sx}/{sy}/{sz} about {part['anchor']}")
## Slots a source cannot paint (a helmeted head has no hair) still have to exist: the
## runtime tints five slots and the audit requires all five as materials. A 4 cm
## triangle is buried inside the skull or the pelvis for each missing one, so the
## material is used instead of being dropped by the exporter, and nothing shows.
SLOT_PATCH_ANCHOR = {"hair": ("Head", 0.0, 0.0, 1.66),
"skin": ("Head", 0.0, 0.0, 1.60),
"pants": ("Hips", 0.0, 0.0, 0.90),
"cloth": ("Chest", 0.0, 0.0, 1.30),
"accessory": ("Hips", 0.0, 0.0, 0.86)}
def _patch_missing_slots(obj, slots):
"""Buried triangles that keep an unpaintable contract slot present."""
present = set()
for poly in obj.data.polygons:
material = obj.data.materials[poly.material_index]
if material is not None:
present.add(_base_name(material.name))
patched = []
for slot in slots:
if slot in present or slot not in SLOT_PATCH_ANCHOR:
continue
bone, x, y, z = SLOT_PATCH_ANCHOR[slot]
material = bpy.data.materials.get(slot) or bpy.data.materials.new(slot)
if material.name not in [m.name for m in obj.data.materials if m]:
obj.data.materials.append(material)
index = [m.name for m in obj.data.materials].index(material.name)
bm = bmesh.new()
bm.from_mesh(obj.data)
before = len(obj.data.vertices)
cube = bmesh.ops.create_cube(bm, size=0.04)
bmesh.ops.translate(bm, verts=cube["verts"], vec=Vector((x, y, z)))
bm.to_mesh(obj.data)
bm.free()
for poly in obj.data.polygons:
if all(v >= before for v in poly.vertices):
poly.material_index = index
group = obj.vertex_groups.get(bone) or obj.vertex_groups.new(name=bone)
group.add(list(range(before, len(obj.data.vertices))), 1.0, "REPLACE")
patched.append(slot)
if patched:
print(f" patched hidden slots: {', '.join(patched)}")
def _base_name(name):
"""Strip Blender's `.001` duplicate suffix."""
head, sep, tail = name.rpartition(".")
@@ -2266,6 +2365,7 @@ def _quaternius_tpose_glb(base, overrides, hair_style="short", parts=()):
_add_quaternius_mouth(obj)
_add_quaternius_eye_whites(obj)
_quaternius_slot_map(obj, slots)
_patch_missing_slots(obj, list(source.get("patch_slots", [])))
_apply_hair_part(obj, hair_style, base)
# Upstream UVs are degenerate (every loop at 0,0), so an AO bake would write