Refine the MakeHuman head (weld lids/brows, add hair cap) and add the user review page
Full build / Import + headless tests (push) Successful in 40s
Full build / Full desktop build + optional release (push) Skipped

This commit is contained in:
zwitschi committed 2026-11-04 15:36:55 +01:00
1 parent 0b2f124ffe
commit 2e1e4d7f70
5 files changed
+78 -9

No files matched your search

+53
View File
@@ -0,0 +1,53 @@
# Head rework — review page (2026-11-04)
This page is the review checkpoint for the NPC head rework. The goal was to replace the
sphere-and-floating-primitive face with a head that has real nose / chin / jaw landmarks at
portrait distance.
## What changed
The original procedural face is a head sphere plus separate nose, mouth, ear and eye primitives.
The new head uses the **decimated MakeHuman head** (CC0, 1,573 tris) as the base shape and keeps
the project's separate eyeballs, brow and mouth pieces on top. An AI judge was used to drive the
decision: it scored the procedural rework at ~3/5 and then picked the MakeHuman head as the
clearer silhouette.
| | Source | Tris (head) | Read |
| --- | --- | --- | --- |
| **Before** | procedural `_face` (sphere + primitives) | ~1,900 | flat face, slit eyes |
| **After** | MakeHuman head + project eyes/brows/mouth | ~2,180 | nose/chin/jaw landmarks |
## AI-generated target concept
The rework was guided by two generated concept frames (front + profile, `bytedance-seed/seedream-4.5`).
![concept](sheet_concept.jpg)
## 1.85 m comparison (top = current, bottom = new)
Left to right: front, three-quarter, profile.
![1.85 m comparison](sheet_compare_1.85.jpg)
## New head at 1.5 m (portrait)
![1.5 m portrait](sheet_head_1.5.jpg)
## Contract and checks
- `blender_assets.py inspect` on the assembled `npc_casual` with the new head: **14 bones,
skinned, no issues**.
- Source: `assets/models/blender_src/makehuman_head.glb` (CC0, attributed in `CREDITS.md`).
- Python 167/167, Godot 277/277, `check_docs` OK.
## Remaining polish (known, not blockers)
1. The hair cap still reads as a stepped block instead of hugging the skull.
2. The brows weld to the face but can read as a slightly separate bar.
3. The neck underside cap is plain; it is hidden by the body neck in the assembled character.
## Decision needed
Approve or reject the MakeHuman-head direction. If approved, the next step is rolling it into the
other 11 variants (one asset family at a time) with a before/after sheet per batch, and re-running
the 24-NPC budget + AO bake.
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

+25 -9
View File
@@ -1008,7 +1008,28 @@ def _makehuman_head(mats, spec):
bmesh.ops.holes_fill(bm, edges=boundary) bmesh.ops.holes_fill(bm, edges=boundary)
bm.to_mesh(head.data) bm.to_mesh(head.data)
bm.free() bm.free()
parts = [head]
# Lids and brows weld into the head shell so the face reads as one surface;
# the sclera/iris stay separate eyeballs.
shell_parts = [head]
for s in (-1.0, 1.0):
ex, ez = 0.052 * s, 1.664
shell_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)))
shell_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)))
shell_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)))
shell = join(shell_parts, "HeadShell")
bm = bmesh.new()
bm.from_mesh(shell.data)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.008)
bm.to_mesh(shell.data)
bm.free()
parts = [shell]
for s in (-1.0, 1.0): for s in (-1.0, 1.0):
# The MakeHuman socket is a deep cavity (floor ≈ −0.105); keep the # The MakeHuman socket is a deep cavity (floor ≈ −0.105); keep the
# sclera recessed so it never breaks the face silhouette. # sclera recessed so it never breaks the face silhouette.
@@ -1017,17 +1038,12 @@ def _makehuman_head(mats, spec):
rings=6, scale=(1.25, 0.62, 0.85))) rings=6, scale=(1.25, 0.62, 0.85)))
parts.append(sphere("Iris", 0.0105, (ex, ey - 0.007, ez), acc, seg=8, parts.append(sphere("Iris", 0.0105, (ex, ey - 0.007, ez), acc, seg=8,
rings=5, scale=(1.0, 0.55, 1.0))) 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. # 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, parts.append(sphere("Mouth", 0.028, (0, -0.165, 1.58), acc, seg=8, rings=5,
scale=(1.0, 0.28, 0.12))) scale=(1.0, 0.28, 0.12)))
# A skull-conforming buzz cap sized for the smaller MakeHuman cranium.
parts.append(_hair_cap("Hair", hair, 1.70, 1.56, radius=0.115,
center=(0, 0.008, 1.66), scale=(0.95, 1.04, 1.05)))
return parts return parts