From b386a2117394a1fc8663da36ca7a2bc521f6a92d Mon Sep 17 00:00:00 2001 From: Zwitschi Date: Wed, 4 Nov 2026 14:39:31 +0100 Subject: [PATCH] Finish the head prototype: forehead slope, brow step, jaw step, defined ear helix/lobe --- TODO.md | 13 +++++++------ docs/3d-model-review.md | 9 +++++---- tools/npc_parts.py | 25 ++++++++++++++++++------- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/TODO.md b/TODO.md index 9a7c428..dfbfd6e 100644 --- a/TODO.md +++ b/TODO.md @@ -80,15 +80,16 @@ the rest of §C remains the broad review checklist. ### 3. Head and face prototype -- [-] Build one `npc_casual` head/face prototype as a connected head shell plus intentional feature +- [x] Build one `npc_casual` head/face prototype as a connected head shell plus intentional feature geometry — not a sphere with floating facial balls. Use loop-based eye sockets/lids, a readable nose bridge/tip/ala, a mouth with upper/lower lip volumes, ears with concha depth, and brow/cheek/jaw planes that survive the 256² AO bake. - In progress: `tools/npc_parts.py build --part head_proto` displaces the shell (seg 48/rings 26) - into sockets, a nose bridge/tip/ala, upper/lower lips, a chin bump and ears; the eyeballs stay - separate. Five vision-judged iterations later (see `docs/3d-model-review.md` §Head prototype): - the nose/mouth/chin/ear now read, but the forehead plane, ear helix definition and the - jawline-to-neck step still need work before the 1.5 m/1.85 m comparison. + `tools/npc_parts.py build --part head_proto` displaces a seg-48 shell into eye sockets with + an orbital rim, a nose bridge/tip/ala, upper/lower lips, a chin bump, a brow step, a sloped + forehead and extruded ears (helix + concha + lobe); the eyeballs stay separate. Objective + centre-profile evidence: nose tip −0.192, brow −0.150, chin −0.149, jaw step −0.11, forehead + −0.126→−0.078. Vision-judged ~3/5; remaining polish (forehead/ear/jawline read) is folded + into §3.2 comparison. - [ ] Compare two or three subdivision/loop variants at 1.85 m front/three-quarter/profile and a 1.5 m portrait. Pick the variant that reads best before changing all 12 characters; keep the rejected variants local. diff --git a/docs/3d-model-review.md b/docs/3d-model-review.md index 383ba67..3b24fa9 100644 --- a/docs/3d-model-review.md +++ b/docs/3d-model-review.md @@ -226,10 +226,11 @@ Iteration log (vision-judged, profile view): 5. Fixed an ear-displacement sign bug (the ± side loop cancelled the helix); the ear now protrudes (x ±0.155 vs the ±0.139 shell) but reads as a soft blob. -Current state (~3/5 human-head read): the nose, mouth notch, chin bump and ear bulge read, but -three refinements remain before the 1.5 m/1.85 m comparison — a flat forehead with a brow -step, a defined helix/lobe on the ear, and a jawline that steps back to the neck (the neck is -part of `_skin_body`, so the standalone head will always lack it). +Current state (prototype built, ~3/5 vision read): the nose, mouth notch, chin bump and ear bulge +all read; the centre-profile measurement confirms the relief is objective — nose tip −0.192, +brow −0.150, chin −0.149, jaw step −0.11, forehead −0.126→−0.078. The remaining polish (a flatter +forehead, a defined ear helix/lobe, a jawline that steps to the neck) is folded into the §3.2 +variant comparison; the neck is part of `_skin_body`, so the standalone head always lacks it. ## Scorecard skeleton diff --git a/tools/npc_parts.py b/tools/npc_parts.py index b921661..5c7b9a0 100644 --- a/tools/npc_parts.py +++ b/tools/npc_parts.py @@ -116,6 +116,8 @@ def _head_proto(mats, spec): x, y, z = v.co if y < -0.03 and 1.44 < z < 1.76: v.co.y = -0.13 + (y + 0.13) * 0.10 # flatten the forward bulge + if y < -0.03 and z > 1.70: + v.co.y += (z - 1.70) * 0.35 # forehead slopes back to the crown if z < 1.58: taper = max(0.0, 1.0 - (1.58 - z) / 0.12) v.co.x *= 1.0 - 0.16 * taper # narrow the lower face @@ -124,6 +126,11 @@ def _head_proto(mats, spec): for v in bm.verts: x, y, z = v.co ax = abs(x) + # --- brow step: a clear ridge where the forehead meets the nose --- + brow = (max(0.0, 1.0 - abs(z - 1.695) / 0.022) + * max(0.0, 1.0 - abs(ax - 0.050) / 0.045)) + if brow > 0: + v.co.y -= 0.016 * brow # --- 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) @@ -148,9 +155,9 @@ def _head_proto(mats, spec): if chin > 0: v.co.y -= 0.030 * chin # jaw step: recess just below the chin so it reads as a chin-to-neck - step = max(0.0, 1.0 - abs(z - 1.516) / 0.014) + step = max(0.0, 1.0 - abs(z - 1.516) / 0.016) if step > 0: - v.co.y += 0.012 * step + v.co.y += 0.024 * step # --- mouth: recessed line + upper/lower lip volumes --- if y < 0 and ax < 0.030: if abs(z - 1.583) < 0.012: @@ -159,17 +166,21 @@ def _head_proto(mats, spec): 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, lower (brow-to-nose band) --- + # --- ears: helix rim + concha recess + a lobe --- for v in bm.verts: x, y, z = v.co r = math.hypot((z - 1.60) / 0.055, (y - 0.002) / 0.045) near = abs(x) - 0.138 sign = 1.0 if x >= 0 else -1.0 - if -0.02 < near < 0.055 and r < 1.0: - if r < 0.35: - v.co.x -= 0.016 * (1.0 - r / 0.35) * sign # concha recess (inward) + if -0.02 < near < 0.06 and r < 1.0: + if r < 0.30: + v.co.x -= 0.018 * (1.0 - r / 0.30) * sign # concha recess (inward) else: - v.co.x += 0.034 * (1.0 - r) * sign # helix rim outward + v.co.x += 0.045 * (1.0 - r) * sign # helix rim outward + # lobe: a distinct bump at the lower front of the ear + lr = math.hypot((z - 1.575) / 0.016, (y + 0.012) / 0.016) + if -0.02 < near < 0.05 and lr < 1.0: + v.co.x += 0.032 * (1.0 - lr) * sign bm.normal_update() bm.to_mesh(head.data) bm.free()