Add eye whites and pupils to every base and archive the 0.9.9 milestone
Full build / Import + headless tests (push) Successful in 43s
Full build / Full desktop build + optional release (push) Skipped

This commit is contained in:
zwitschi committed 2026-11-06 18:44:39 +01:00
1 parent c70a523f0f
commit a0dbf75806
22 files changed
+129 -34

No files matched your search

+76
View File
@@ -1663,6 +1663,81 @@ def _add_quaternius_mouth(obj):
print(f" mouth placed at y {front + 0.003:+.3f} z {height:.3f}")
def _add_quaternius_eye_whites(obj):
"""Give the eye quads a sclera and a pupil.
Every base ships one dark quad per eye, so the face reads as two dark
rectangles at portrait distance. The quads are rewired instead of textured: the
existing faces become the sclera (light), and a shrunken copy in front of each
becomes the pupil (dark). Two draw calls per head, no new UV work.
"""
eye = next((i for i, m in enumerate(obj.data.materials)
if m is not None and _base_name(m.name).lower() == "eye"), None)
if eye is None:
return
sclera = obj.data.materials[eye]
sclera.name = "sclera"
sclera.use_nodes = True
bsdf = sclera.node_tree.nodes.get("Principled BSDF")
if bsdf is not None:
# Linear values: 0.62 reads as an off-white sclera, not a glowing one.
bsdf.inputs["Base Color"].default_value = (0.62, 0.60, 0.55, 1.0)
bsdf.inputs["Roughness"].default_value = 0.45
sclera.use_backface_culling = False
pupil = bpy.data.materials.new("pupil")
pupil.use_nodes = True
pupil_bsdf = pupil.node_tree.nodes.get("Principled BSDF")
if pupil_bsdf is not None:
pupil_bsdf.inputs["Base Color"].default_value = (0.012, 0.010, 0.014, 1.0)
pupil_bsdf.inputs["Roughness"].default_value = 0.35
# A single-sided pupil can vanish depending on the exported winding; the extra
# draw cost on two 0.02 m quads is irrelevant.
pupil.use_backface_culling = False
# Collect the eye faces before adding anything, then inset a pupil per face.
faces = [p for p in obj.data.polygons if p.material_index == eye]
if not faces:
return
obj.data.materials.append(pupil)
pupil_index = len(obj.data.materials) - 1
bm = bmesh.new()
bm.from_mesh(obj.data)
bm.faces.ensure_lookup_table()
# Hold face *references*: adding vertices invalidates the index tables.
targets = [bm.faces[i] for i in range(len(bm.faces))
if bm.faces[i].material_index == eye]
# Shrink toward the *eye* centre, not each triangle's own centre: the quad is two
# triangles, and per-face insets produced two offset wedges instead of a pupil.
eye_centre = {}
for face in targets:
side = 1 if face.calc_center_median().x >= 0.0 else -1
eye_centre.setdefault(side, []).append(face.calc_center_median())
side_centre = {side: sum(points, Vector()) / len(points)
for side, points in eye_centre.items()}
new_faces = []
for face in targets:
centre = side_centre[1 if face.calc_center_median().x >= 0.0 else -1]
# Push the pupil in front of the sclera so it cannot z-fight, then shrink it
# about its own centre. Blender -Y is the face, so "in front" is -Y.
# Push the pupil out along the sclera's own normal: offsetting in world -Y
# only worked while the eye plane was axis-aligned, and this one is tilted.
nudge = face.normal.normalized() * 0.0032
verts = []
for vert in face.verts:
offset = vert.co - centre
verts.append(bm.verts.new(centre
+ Vector((offset.x * 0.74,
offset.y * 0.74,
offset.z * 0.70))
+ nudge))
new_face = bm.faces.new(verts)
new_face.material_index = pupil_index
new_faces.append(new_face)
bm.to_mesh(obj.data)
bm.free()
print(f" eye whites: {len(faces)} sclera quads, {len(new_faces)} pupil quads")
def _base_name(name):
"""Strip Blender's `.001` duplicate suffix."""
head, sep, tail = name.rpartition(".")
@@ -2075,6 +2150,7 @@ def _quaternius_tpose_glb(base, overrides):
if source.get("donor_head"):
_transplant_worker_head(obj)
_add_quaternius_mouth(obj)
_add_quaternius_eye_whites(obj)
_quaternius_slot_map(obj, slots)
# Upstream UVs are degenerate (every loop at 0,0), so an AO bake would write