Add the connected-shell head prototype (head_proto) with sculpted sockets/nose/lips/ears
This commit is contained in:
1 parent
b8d8869e03
commit
50edc47480
3 files changed
+103
-3
No files matched your search
+84
-2
@@ -35,6 +35,7 @@ absolute and derived from this file's location.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -52,7 +53,7 @@ PROTOTYPE = "npc_casual" # canonical one-variant prototype
|
||||
|
||||
# The four buildable parts. `head` is the bare head shell; `face` is the shell
|
||||
# plus nose/eyes/mouth/ears; `hand_l`/`hand_r` are the left/right hand geometry.
|
||||
PART_NAMES = ("head", "face", "hand_l", "hand_r")
|
||||
PART_NAMES = ("head", "face", "hand_l", "hand_r", "head_proto")
|
||||
_HAND_PREFIXES = ("Palm", "Finger", "Fingertip", "Thumb", "ThumbTip")
|
||||
|
||||
|
||||
@@ -92,6 +93,85 @@ def _export_obj(obj, path):
|
||||
return path
|
||||
|
||||
|
||||
def _head_proto(mats, spec):
|
||||
"""Connected head shell with sculpted sockets, nose, lips, ears and brow/cheek/jaw.
|
||||
|
||||
This is the §3 prototype: the skin features are displaced out of one head
|
||||
mesh instead of floating spheres, so the face is a single connected surface
|
||||
with loop-based eye sockets and protruding nose/lip/ear volumes. The eyeballs
|
||||
(sclera/iris/lids) stay separate — they are not skin. Blender Z-up, face
|
||||
front −Y.
|
||||
"""
|
||||
skin, acc, hair = mats["skin"], mats["accessory"], mats["hair"]
|
||||
white = ba.material("eye_white", textured_solid=True)
|
||||
head = ba.sphere("HeadShell", 0.155, (0, 0, 1.635), skin, seg=48, rings=26,
|
||||
scale=(0.92, 1.0, 1.12))
|
||||
ba._shape_head_mesh(head) # brow ridge + cheekbone + jaw/chin planes
|
||||
bm = ba.bmesh.new()
|
||||
bm.from_mesh(head.data)
|
||||
|
||||
for v in bm.verts:
|
||||
x, y, z = v.co
|
||||
ax = abs(x)
|
||||
# --- eye sockets: recess the floor, leave a raised orbital rim ---
|
||||
for s in (-1.0, 1.0):
|
||||
r = math.hypot(x - 0.052 * s, z - 1.664)
|
||||
if r < 0.036:
|
||||
v.co.y += 0.018 * (1.0 - r / 0.036) # recess inward (+y)
|
||||
elif r < 0.046:
|
||||
v.co.y -= 0.010 * (1.0 - (r - 0.036) / 0.010) # rim forward
|
||||
# --- nose: bridge/tip protrusion plus ala wings ---
|
||||
if y < 0 and ax < 0.048:
|
||||
band = max(0.0, 1.0 - abs(z - 1.628) / 0.052) # 1.576..1.680
|
||||
if band > 0:
|
||||
tip = max(0.0, 1.0 - abs(z - 1.615) / 0.022)
|
||||
v.co.y -= (0.035 * tip + 0.012 * (band - tip)) * (1.0 - ax / 0.048)
|
||||
for s in (-1.0, 1.0):
|
||||
ar = math.hypot((x - 0.017 * s) / 0.011, (z - 1.606) / 0.011)
|
||||
if ar < 1.0:
|
||||
v.co.y -= 0.018 * (1.0 - ar)
|
||||
# --- mouth: recessed line + upper/lower lip volumes ---
|
||||
if y < 0 and ax < 0.030:
|
||||
if abs(z - 1.583) < 0.012:
|
||||
v.co.y += 0.008 # mouth slit recess
|
||||
if abs(z - 1.590) < 0.008:
|
||||
v.co.y -= 0.014 # upper lip
|
||||
if abs(z - 1.577) < 0.010:
|
||||
v.co.y -= 0.018 # fuller lower lip
|
||||
# --- ears: extrude outward with a concha recess ---
|
||||
for v in bm.verts:
|
||||
x, y, z = v.co
|
||||
for s in (-1.0, 1.0):
|
||||
r = math.hypot((z - 1.63) / 0.055, (y - 0.008) / 0.045)
|
||||
near = abs(x) - 0.140
|
||||
if -0.02 < near < 0.05 and r < 1.0:
|
||||
if r < 0.35:
|
||||
v.co.x -= 0.014 * (1.0 - r / 0.35) * s # concha recess (inward)
|
||||
else:
|
||||
v.co.x += 0.028 * (1.0 - r) * s # helix rim outward
|
||||
bm.normal_update()
|
||||
bm.to_mesh(head.data)
|
||||
bm.free()
|
||||
|
||||
# Eyeballs stay separate for readability; lids wrap the socket rim.
|
||||
parts = [head]
|
||||
for s in (-1.0, 1.0):
|
||||
ex, ey, ez = 0.052 * s, -0.134, 1.664
|
||||
parts.append(ba.sphere("EyeWhite", 0.019, (ex, ey, ez), white, seg=10,
|
||||
rings=6, scale=(1.25, 0.62, 0.85)))
|
||||
parts.append(ba.sphere("Iris", 0.0105, (ex, ey - 0.007, ez), acc, seg=8,
|
||||
rings=5, scale=(1.0, 0.55, 1.0)))
|
||||
parts.append(ba.sphere("UpperLid", 0.020, (ex, ey - 0.006, ez + 0.014),
|
||||
skin, seg=8, rings=4, scale=(1.20, 0.32, 0.24),
|
||||
rot=(math.radians(-10), 0, 0)))
|
||||
parts.append(ba.sphere("LowerLid", 0.017, (ex, ey - 0.004, ez - 0.012),
|
||||
skin, seg=8, rings=4, scale=(1.10, 0.22, 0.18),
|
||||
rot=(math.radians(6), 0, 0)))
|
||||
parts.append(ba.box("Brow", (0.055, 0.014, 0.012), (0.052 * s, -0.143, 1.699),
|
||||
hair, rot=(0, 0.14 * s, 0.0)))
|
||||
return parts
|
||||
|
||||
|
||||
def build_part(name, variant, out):
|
||||
"""Build one part fresh from the current builders (Blender-only)."""
|
||||
mats, spec = _mats_and_spec(variant)
|
||||
@@ -101,6 +181,8 @@ def build_part(name, variant, out):
|
||||
ba._shape_head_mesh(obj)
|
||||
elif name == "face":
|
||||
obj = ba.join(ba._face(mats, spec), "face")
|
||||
elif name == "head_proto":
|
||||
obj = ba.join(_head_proto(mats, spec), "head_proto")
|
||||
else:
|
||||
side = -1.0 if name == "hand_l" else 1.0
|
||||
hs = ba._hands_and_shoes(mats, spec)
|
||||
@@ -151,7 +233,7 @@ def extract_part(name, variant, out):
|
||||
and o.name == "npc_base"), None)
|
||||
if mesh is None:
|
||||
raise SystemExit(f"no npc_base mesh in {src}")
|
||||
group = ("Head" if name in ("head", "face")
|
||||
group = ("Head" if name in ("head", "face", "head_proto")
|
||||
else f"Hand{'L' if name == 'hand_l' else 'R'}")
|
||||
return _extract_group(mesh, group, out)
|
||||
|
||||
|
||||
Reference in new issue
Block a user