Add the MakeHuman-head character head source (head=mh) with project eyes/brows/mouth and CC0 attribution
This commit is contained in:
1 parent
632ec16aaa
commit
cdd2e02564
5 files changed
+96
-10
No files matched your search
@@ -40,6 +40,15 @@ kept outside version control.
|
|||||||
The CC-BY preview images are included only as development evidence. Any future
|
The CC-BY preview images are included only as development evidence. Any future
|
||||||
shipped derivative of CesiumMan or RiggedFigure must keep the attribution above.
|
shipped derivative of CesiumMan or RiggedFigure must keep the attribution above.
|
||||||
|
|
||||||
|
### MakeHuman head source
|
||||||
|
|
||||||
|
The decimated head used by the character head prototype is derived from the
|
||||||
|
MakeHuman / MPFB base mesh, **CC0** (`assets/models/blender_src/makehuman_head.glb`;
|
||||||
|
source `tmp/npc-research/makehuman/base.obj` with its bundled
|
||||||
|
`LICENSE.md`/`LICENSE.ASSETS.md`). It is retargeted, decimated and re-weighted by
|
||||||
|
project scripts; the exported character GLBs remain project assets. See ADR-0015
|
||||||
|
and `docs/npc-model-sourcing.md`.
|
||||||
|
|
||||||
The venue geometry is authored procedurally in `src/game/world_builder.gd` as a
|
The venue geometry is authored procedurally in `src/game/world_builder.gd` as a
|
||||||
stylized reconstruction. The Joke Shop is a fictional stand-in for the real
|
stylized reconstruction. The Joke Shop is a fictional stand-in for the real
|
||||||
Sunset Strip block; the game does not claim endorsement by, or an exact replica
|
Sunset Strip block; the game does not claim endorsement by, or an exact replica
|
||||||
|
|||||||
Binary file not shown.
+76
-2
@@ -965,6 +965,72 @@ def _head_proto(mats, spec, subdiv=48):
|
|||||||
return parts
|
return parts
|
||||||
|
|
||||||
|
|
||||||
|
def _makehuman_head(mats, spec):
|
||||||
|
"""MakeHuman head shape plus the project's eye/mouth/brow pieces.
|
||||||
|
|
||||||
|
The AI-judged head rework (2026-11-04) picked the decimated MakeHuman head
|
||||||
|
over the displacement shell: it keeps the nose/chin/jaw landmarks at 1,573
|
||||||
|
tris. The MakeHuman mesh has no eyeballs, so the project's sclera/iris/lid/
|
||||||
|
brow pieces are added at the MakeHuman landmarks (eyes ≈ ±0.052, −0.160,
|
||||||
|
1.664) for readability. Blender Z-up, face front −Y.
|
||||||
|
"""
|
||||||
|
skin, acc, hair = mats["skin"], mats["accessory"], mats["hair"]
|
||||||
|
white = material("eye_white", textured_solid=True)
|
||||||
|
src = SRC_DIR / "makehuman_head.glb"
|
||||||
|
before = {o.name for o in bpy.context.scene.objects}
|
||||||
|
bpy.ops.import_scene.gltf(filepath=str(src))
|
||||||
|
# Drop the imported armature / glTF neutral bone; build_character re-weights
|
||||||
|
# the head to the contract rig.
|
||||||
|
for a in list(bpy.context.scene.objects):
|
||||||
|
if a.type == "ARMATURE" and a.name not in before:
|
||||||
|
bpy.data.objects.remove(a, do_unlink=True)
|
||||||
|
# Pick the newly imported mesh, not the already-built body.
|
||||||
|
head = next((o for o in bpy.context.scene.objects
|
||||||
|
if o.type == "MESH" and o.name not in before
|
||||||
|
and o.data.materials), None)
|
||||||
|
if head is None:
|
||||||
|
raise SystemExit(f"no head mesh in {src}")
|
||||||
|
head.name = "HeadShell"
|
||||||
|
# The decimated MakeHuman "hair" scalp slot reads as shaggy shards; collapse
|
||||||
|
# the head to one skin slot so the join does not collide with the body's
|
||||||
|
# cloth/pants slots (the project `_hair` cap provides hair later).
|
||||||
|
head.data.materials.clear()
|
||||||
|
head.data.materials.append(skin)
|
||||||
|
# Clean the ragged neck cut left by the Head-group extraction: bisect below
|
||||||
|
# the jaw and cap the hole so the head ends on a clean solid underside.
|
||||||
|
bm = bmesh.new()
|
||||||
|
bm.from_mesh(head.data)
|
||||||
|
bmesh.ops.bisect_plane(bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:],
|
||||||
|
plane_co=(0, 0, 1.50), plane_no=(0, 0, -1),
|
||||||
|
clear_outer=True)
|
||||||
|
boundary = [e for e in bm.edges if e.is_boundary]
|
||||||
|
if boundary:
|
||||||
|
bmesh.ops.holes_fill(bm, edges=boundary)
|
||||||
|
bm.to_mesh(head.data)
|
||||||
|
bm.free()
|
||||||
|
parts = [head]
|
||||||
|
for s in (-1.0, 1.0):
|
||||||
|
# The MakeHuman socket is a deep cavity (floor ≈ −0.105); keep the
|
||||||
|
# sclera recessed so it never breaks the face silhouette.
|
||||||
|
ex, ey, ez = 0.052 * s, -0.125, 1.664
|
||||||
|
parts.append(sphere("EyeWhite", 0.019, (ex, ey, ez), white, seg=10,
|
||||||
|
rings=6, scale=(1.25, 0.62, 0.85)))
|
||||||
|
parts.append(sphere("Iris", 0.0105, (ex, ey - 0.007, ez), acc, seg=8,
|
||||||
|
rings=5, scale=(1.0, 0.55, 1.0)))
|
||||||
|
parts.append(sphere("UpperLid", 0.020, (ex, -0.135, ez + 0.014),
|
||||||
|
skin, seg=8, rings=4, scale=(1.20, 0.32, 0.24),
|
||||||
|
rot=(math.radians(-10), 0, 0)))
|
||||||
|
parts.append(sphere("LowerLid", 0.017, (ex, -0.135, ez - 0.012),
|
||||||
|
skin, seg=8, rings=4, scale=(1.10, 0.22, 0.18),
|
||||||
|
rot=(math.radians(6), 0, 0)))
|
||||||
|
parts.append(box("Brow", (0.055, 0.014, 0.012), (0.052 * s, -0.165, 1.705),
|
||||||
|
hair, rot=(0, 0.14 * s, 0.0)))
|
||||||
|
# A subtle mouth line on top of the MakeHuman lips for readability.
|
||||||
|
parts.append(sphere("Mouth", 0.028, (0, -0.165, 1.58), acc, seg=8, rings=5,
|
||||||
|
scale=(1.0, 0.28, 0.12)))
|
||||||
|
return parts
|
||||||
|
|
||||||
|
|
||||||
def _hands_and_shoes(mats, spec):
|
def _hands_and_shoes(mats, spec):
|
||||||
skin = mats["skin"]
|
skin = mats["skin"]
|
||||||
shoe = mats["accessory"]
|
shoe = mats["accessory"]
|
||||||
@@ -1122,7 +1188,10 @@ def _clothing(spec, mats):
|
|||||||
return parts
|
return parts
|
||||||
|
|
||||||
|
|
||||||
def build_character(name, head_proto=False):
|
def build_character(name, head="face"):
|
||||||
|
"""Build one character. `head` picks the head source: "face" (current
|
||||||
|
primitive face), "proto" (connected-shell displacement prototype) or
|
||||||
|
"mh" (decimated MakeHuman head + project eyes/brows/mouth)."""
|
||||||
spec = dict(CHARACTERS[name])
|
spec = dict(CHARACTERS[name])
|
||||||
if name == "npc_hoodie":
|
if name == "npc_hoodie":
|
||||||
spec["beard"] = True
|
spec["beard"] = True
|
||||||
@@ -1137,7 +1206,12 @@ def build_character(name, head_proto=False):
|
|||||||
_assign_body_slots(body, spec, body_mats)
|
_assign_body_slots(body, spec, body_mats)
|
||||||
_auto_weights(body, layout)
|
_auto_weights(body, layout)
|
||||||
|
|
||||||
head_parts = _head_proto(mats, spec) if head_proto else _face(mats, spec)
|
if head == "mh":
|
||||||
|
head_parts = _makehuman_head(mats, spec)
|
||||||
|
elif head == "proto":
|
||||||
|
head_parts = _head_proto(mats, spec)
|
||||||
|
else:
|
||||||
|
head_parts = _face(mats, spec)
|
||||||
hair_style = spec["hair"] if spec["layer"] != "hood" else "buzz"
|
hair_style = spec["hair"] if spec["layer"] != "hood" else "buzz"
|
||||||
head_parts += _hair(hair_style, mats["hair"])
|
head_parts += _hair(hair_style, mats["hair"])
|
||||||
head = join(head_parts, "head")
|
head = join(head_parts, "head")
|
||||||
|
|||||||
+10
-7
@@ -54,7 +54,7 @@ PROTOTYPE = "npc_casual" # canonical one-variant prototype
|
|||||||
# The four buildable parts. `head` is the bare head shell; `face` is the shell
|
# 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.
|
# plus nose/eyes/mouth/ears; `hand_l`/`hand_r` are the left/right hand geometry.
|
||||||
PART_NAMES = ("head", "face", "hand_l", "hand_r", "head_proto",
|
PART_NAMES = ("head", "face", "hand_l", "hand_r", "head_proto",
|
||||||
"hand_l_proto", "hand_r_proto")
|
"hand_l_proto", "hand_r_proto", "head_mh")
|
||||||
_HAND_PREFIXES = ("Palm", "Finger", "Fingertip", "Thumb", "ThumbTip")
|
_HAND_PREFIXES = ("Palm", "Finger", "Fingertip", "Thumb", "ThumbTip")
|
||||||
|
|
||||||
|
|
||||||
@@ -109,6 +109,8 @@ def build_part(name, variant, out, subdiv=48):
|
|||||||
obj = ba._hand_proto(mats, spec, side=-1.0)
|
obj = ba._hand_proto(mats, spec, side=-1.0)
|
||||||
elif name == "hand_r_proto":
|
elif name == "hand_r_proto":
|
||||||
obj = ba._hand_proto(mats, spec, side=1.0)
|
obj = ba._hand_proto(mats, spec, side=1.0)
|
||||||
|
elif name == "head_mh":
|
||||||
|
obj = ba.join(ba._makehuman_head(mats, spec), "head_mh")
|
||||||
else:
|
else:
|
||||||
side = -1.0 if name == "hand_l" else 1.0
|
side = -1.0 if name == "hand_l" else 1.0
|
||||||
hs = ba._hands_and_shoes(mats, spec)
|
hs = ba._hands_and_shoes(mats, spec)
|
||||||
@@ -164,14 +166,15 @@ def extract_part(name, variant, out):
|
|||||||
return _extract_group(mesh, group, out)
|
return _extract_group(mesh, group, out)
|
||||||
|
|
||||||
|
|
||||||
def assemble(variant, out, head_proto=False):
|
def assemble(variant, out, head="face"):
|
||||||
"""Assemble a variant (currently rebuilds the procedural character).
|
"""Assemble a variant (currently rebuilds the procedural character).
|
||||||
|
|
||||||
This proves the end-to-end wiring; once parts are approved, this swaps the
|
This proves the end-to-end wiring; once parts are approved, this swaps the
|
||||||
new part GLBs in instead of calling ``build_character`` directly. Pass
|
new part GLBs in instead of calling ``build_character`` directly. Pass
|
||||||
``head_proto=True`` to build with the connected-shell prototype head.
|
``head="mh"`` or ``head="proto"`` to build with the MakeHuman or prototype
|
||||||
|
head source.
|
||||||
"""
|
"""
|
||||||
objs = ba.build_character(variant, head_proto=head_proto)
|
objs = ba.build_character(variant, head=head)
|
||||||
mesh = next(o for o in objs if o.type == "MESH")
|
mesh = next(o for o in objs if o.type == "MESH")
|
||||||
arm = next(o for o in objs if o.type == "ARMATURE")
|
arm = next(o for o in objs if o.type == "ARMATURE")
|
||||||
out = Path(out)
|
out = Path(out)
|
||||||
@@ -279,8 +282,8 @@ def main(argv=None):
|
|||||||
asm = sub.add_parser("assemble")
|
asm = sub.add_parser("assemble")
|
||||||
asm.add_argument("--variant", default=PROTOTYPE)
|
asm.add_argument("--variant", default=PROTOTYPE)
|
||||||
asm.add_argument("--out", required=True)
|
asm.add_argument("--out", required=True)
|
||||||
asm.add_argument("--head-proto", action="store_true",
|
asm.add_argument("--head", default="face", choices=("face", "proto", "mh"),
|
||||||
help="use the connected-shell prototype head")
|
help="head source (face | proto | mh)")
|
||||||
|
|
||||||
insp = sub.add_parser("inspect")
|
insp = sub.add_parser("inspect")
|
||||||
insp.add_argument("--file", required=True)
|
insp.add_argument("--file", required=True)
|
||||||
@@ -316,7 +319,7 @@ def main(argv=None):
|
|||||||
extract_part(args.part, args.variant, args.out)
|
extract_part(args.part, args.variant, args.out)
|
||||||
print(f"extracted {args.part} -> {args.out}")
|
print(f"extracted {args.part} -> {args.out}")
|
||||||
elif args.cmd == "assemble":
|
elif args.cmd == "assemble":
|
||||||
assemble(args.variant, args.out, head_proto=args.head_proto)
|
assemble(args.variant, args.out, head=args.head)
|
||||||
print(f"assembled {args.variant} -> {args.out}")
|
print(f"assembled {args.variant} -> {args.out}")
|
||||||
elif args.cmd == "inspect":
|
elif args.cmd == "inspect":
|
||||||
inspect(args.file)
|
inspect(args.file)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class CliTests(unittest.TestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
set(npc_parts.PART_NAMES),
|
set(npc_parts.PART_NAMES),
|
||||||
{"head", "face", "hand_l", "hand_r", "head_proto",
|
{"head", "face", "hand_l", "hand_r", "head_proto",
|
||||||
"hand_l_proto", "hand_r_proto"},
|
"hand_l_proto", "hand_r_proto", "head_mh"},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_blender_commands_fail_cleanly_without_bpy(self):
|
def test_blender_commands_fail_cleanly_without_bpy(self):
|
||||||
|
|||||||
Reference in new issue
Block a user