Compare commits

..
11 Commits
14 changed files with 556 additions and 55 deletions

No files matched your search

+8 -4
View File
@@ -15,6 +15,10 @@ been cleared for public distribution.
## 3D characters, gear and props
The local MakeHuman head prototype uses the CC0 MakeHuman/MPFB base mesh
(see [`docs/makehuman-head-review.md`](docs/makehuman-head-review.md)); it is a
development prototype, not a shipped character.
The character, gear and prop library is generated by project-owned scripts
(`tools/blender_assets.py`, `tools/npc_pipeline.py`, `tools/model_pipeline.py`)
using Blender. The `.blend` working sources are developer artifacts and are not
@@ -42,10 +46,10 @@ 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
The standalone MakeHuman 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 cut, retopologised and re-weighted by
project scripts; the exported character GLBs remain project assets. See ADR-0015
and `docs/npc-model-sourcing.md`.
+76 -1
View File
@@ -10,6 +10,58 @@ canonical implemented-features list in `docs/development-status.md`.
the licence gate. Downloads live in `tmp/research/` (gitignored): DDG image search, street-view
frames, neighbouring buildings — never committed, never shipped. Promotion into
`docs/screens/refs/` always passes the sidecar licence + privacy gate and `CREDITS.md`.
- **MakeHuman head accepted (2026-11-04, owner):** the basic human head is a pass.
`assets/models/blender_src/makehuman_head.glb` is the CC0 MakeHuman-derived head
(12,642 tris / 7,690 split verts). The automated vision gate still records REJECT on
the eye/lid region (globe depth, lid overlap, iris at three-quarter) as deferred
polish, not a blocker. The assembled path (`build_character(head="mh")`) adds a
skull-conforming hair cap derived from the closed head shell, hides the scalp under
that cap, and narrows the neck/shoulder body. Default head source stays `face` until
the owner reviews the hair/body and the 12-variant rollout starts.
- **AI-assisted asset review loop (2026-11-04):** head/face shape decisions use a concept +
judge loop — generate a target concept (`generate_textures.generate_image`, Seedream) and use
`tools/describe_image.py` as the vision judge, iterating on the mesh until the judge's specific
fixes converge. This is what picked the MakeHuman head over the procedural displacement head
(`_head_proto` oscillates between "decorated egg" and "spiky wedges" at ~3/5 on the coarse
sphere). Review page: `docs/screens/head-review/README.md`; methodology note:
`docs/3d-model-review.md` §AI-driven head rework.
- **MakeHuman hair/body prototype (2026-11-04):** `npc_casual --head mh` is
14,753 tris / 12,822 raw / 12,823 seam-split verts / 14 bones / 9 materials,
`inspect` clean. All 12 variants were counted at 14,693–15,987 tris. Hair uses flat
shading normals because the old hair normal map caused dark tangent patches on the
new cap. Scalp faces fully covered by the hair cap are deleted (saves ~950 tris);
the accepted face geometry is untouched. The procedural body was narrowed at the
neck/shoulders (shoulder root z 1.345, chest 0.158, neck 0.060/0.054) and its skin
decimate dropped to 0.21. The local renders are in
`tmp/npc-parts/prototype_mh/renders/`.
- **MPFB full-body prototype (2026-11-04):** the current body candidate is the local MPFB 2.0.18
pipeline at `tmp/npc-research/build_mpfb_npc.py` (dev-only; no `.mhclo` clothing installed).
It creates a macro-driven human, bakes targets, adds the `game_engine` rig, captures arm
points/weights, removes the rig, applies the helper MASK, scales to 1.81 m, retargets the A-pose
arms to the contract wrist, cuts the neck at 1.505, decimates, bisects the clothing boundaries,
paints the five slots, adds a short neck flare, project hands, rounded shoes and cuffs, the
accepted head and the 14-bone contract armature. Current output: **15,317 tris / 13,690 glTF
verts / 13,702 seam-split verts / 1.810 m / 14 bones / 10 materials**; `inspect` clean and feet
grounded. The torso is inflated radially by 12% through z 0.72–1.40 to loosen the shirt. The
neck is a radial cut of the decimated column bridged by a four-ring, 20-segment lathe (lower
rings painted cloth, reads as a crew neck); the ankles are hidden by a bevelled 0.11 m shoe
shell, an inset 0.111 × 0.286 m sole, a tongue and a 0.08 m pant cuff. Sleeve cuffs use the
`cloth` slot and the `accessory` slot is carried by the sole. A light smoothing pass on the
closed thigh/hip region softens the decimated silhouette. Earlier torus collar, cloth cylinder
collar, skin neck-blend sphere and destructive neck `smooth_vert` pass failed: the shells read as a
brace, shelf or donut, and vertex smoothing tore the open collar boundary into a dark hole. An
untucked shirt-hem band at the hips also failed: it read as a hanging white flap at the crotch.
Protruding flat sole plates read as skates/bricks and were replaced by an inset sole under a
rounded shell; laces were dropped for triangle budget. The neck skin rule needed a `z > 1.35`
guard: without it the `radius < 0.08` test painted skin at the crotch. Lowering the hem to
z 0.80 (unitard), tilting it up at the front (high-cut swimwear) and a 13% hem flare (peplum)
were all rejected; the remaining crotch V is the dark hollow under a body-painted shirt. A quick
open, flared solidified tube (0.150→0.196 m, z 1.05→0.80, 24 segments, ~288 tris) also failed:
it read as a hanging apron/tabard. The real fix is a shaped shirt shell with armholes and a
curved hem, not a tube. Remaining polish: crotch V, partly loosened shirt,
angular thigh facets. The prototype uses flat slot colours because the one-shot MPFB UV unwrap
does not match the per-slot NPC atlases. Renders:
`tmp/npc-parts/mpfb_proto/renders84/`; body-only: `body_only.glb`.
- **Project agent skills (2026-11-03):** two tracked skills live in `.agents/skills/`:
`gig-line-blender-asset-contract` and `gig-line-release-qa`. They are guidance only,
add no runtime dependency and are referenced from `tools/README.md`. The third-party
@@ -105,6 +157,27 @@ canonical implemented-features list in `docs/development-status.md`.
out of a portrait frame (park the rest of the crowd away instead).
- Blender: `bisect_plane` and thin bevels leave zero-area triangles (Godot "non-finite normal in
LOD generation"); `_clean()` + a weld after bisect fixes them and the audit rejects them.
- MakeHuman head/hair gotchas: the imported head is broad and flat, so a separate hair
ellipsoid floats or leaves a bald patch; derive the cap from the closed head shell and
boolean-cut the hairline. Do not reuse the skull's UVs with the old `hair_normal.png`
(dark tangent patches) — hair skips the normal map. Generate the cap before deleting
the hidden scalp, or the cap inherits the hole. The flatpak sandbox cannot read `/tmp`;
helper scripts must live under `$PWD`.
- MakeHuman OBJ intake transform (`base.obj`): the source is Y-up, 16.946 units tall. To reach
the project Blender Z-up / metres / front -Y space: scale 0.10739, translate y +0.907 (ground
the feet), then rotate +90° around X (maps +Y→+Z up and +Z→−Y front). The full-res head carries
internal geometry (eyes/teeth/mouth shells) and is non-manifold; voxel-remesh and Catmull-Clark
subdivision both fragment it into shards. On the decimated head, weld first
(`remove_doubles` ~0.0002) then Laplacian `smooth_vert`; subdivision without the weld splits
along the GLB seam vertices.
- MPFB gotchas: call `TargetService.bake_targets(body)` before any decimate (shape-key
modifier error); add the built-in rig and capture arm weights before applying the MASK
modifier, or `build_basemesh_position_info` divides by zero on an emptied bone group;
decimate before bisecting the clothing loops, or the decimator collapses the bisect
into a sawtooth. Clear custom split normals after joining (imported MPFB normals leave
flat-shaded thigh facets). Imported GLBs may include helper `Cube`/`Icosphere` meshes at
±1.0, so select `npc_base` explicitly. Delete the MPFB hands by hand/finger vertex-group
weights, not only by `|x|`, or leftover fingers become pants-coloured fins.
- Image models have per-model size floors/caps (Seedream needs 1920+; `GEN_SIZE=2048`, do not
lower; gpt-5-image-mini caps lower). `generate_image` applies model-specific sizing; concept
generation defaults to `--request-size 1536`.
@@ -169,6 +242,8 @@ canonical implemented-features list in `docs/development-status.md`.
- Character checks: `python3 tools/blender_assets.py inspect|audit|fit`,
`python3 tools/npc_proportions.py`
- Docs: `python3 tools/check_docs.py` (every tracked image must be linked from its folder README)
- Head/part pipeline: `python3 tools/npc_parts.py build|extract|assemble|inspect|sheet|compare`
(Blender subcommands run under flatpak). Head sources: `build_character(head="face"|"proto"|"mh")`.
- Reference research: `python3 tools/refcollect.py search <q> --slug <block> --limit N`
(local-only), `tools/refboard.py` (Commons), `tools/three_up.py compose/rows`,
`tools/scene_concepts.py generate/anchor/realism`
@@ -188,6 +263,6 @@ Comedy Store map rebuild are complete. **Priority (2026-11-04, owner):** 1) the
model overhaul** (head/face/hands, local part pipeline), 2) finish the MakeHuman character + the
3D model and map improvements, 3) ship it all together in the **0.10.0 minor**. The 0.9.7/0.9.8
character patches are published; `TODO.md` is the priority source. Test counts as of 2026-11-04:
Godot 277/277, Python 162/162, `check_docs` OK.
Godot 277/277, Python 167/167, `check_docs` OK.
<!-- b64: RWFzdGVyIGVnZzogYSByYXJlIHJhZGlvIHRyYW5zbWlzc2lvbiAiwr9Ew7NuZGUgZXN0w6EgbGEgbW90YT8iIGZpcmVzIHZpYSBHYW1lTWFuYWdlci5fbWF5YmVfcmFkaW9fZWFzdGVyX2VnZyAoMi41JSBjaGFuY2UgYWZ0ZXIgYSBzdWNjZXNzZnVsIHJhZGlvIGNhbGwpLiBTdHJpbmcga2V5IHJhZGlvLmVhc3Rlcl9lZ2cgaW4gY29udGVudC9zdHJpbmdzLmVuLmpzb24uIERvIG5vdCByZW1vdmUu -->
+1
View File
@@ -160,6 +160,7 @@ Full setup, every run/test/smoke command, and the controls table are in
- [`TODO.md`](TODO.md) — Priority-ordered tasks: character and local-map development, release milestones, and future work.
- [`docs/README.md`](docs/README.md) — Documentation index, current priorities and visual previews.
- [`docs/npc-model-sourcing.md`](docs/npc-model-sourcing.md) — Character-source evaluation, licence rules and the character-development plan.
- [`docs/makehuman-head-review.md`](docs/makehuman-head-review.md) — Local MakeHuman head prototype, provenance and review verdict.
- [`AGENTS.md`](AGENTS.md) — Development, commit, build and collaboration workflow.
- [`DONE.md`](DONE.md) — Completed task sections.
- [`MEMORY.md`](MEMORY.md) — Persistent session notes.
+112 -12
View File
@@ -26,10 +26,12 @@ five tint slots, 16,000/16,000 character cap, optional extra bones beyond the re
**Current state:** `assets/models/npc/` contains the tracked frozen `npc_base.blend` and its
gitignored `.blend1`; the 12 production characters are assembled procedurally by
`tools/blender_assets.py` from `_shape_head_mesh`, `_face`, `_hair` and `_hands_and_shoes`.
There is no per-part source asset library yet. The MakeHuman body prototype and its temporary
head/hand swap live under `tmp/npc-research/` and are not the shipped path. This overhaul supplies
the reusable head/face and hand parts that both the procedural characters and the MakeHuman body
can consume.
The MakeHuman head is now a second head source (`build_character(head="mh")`,
`build --head mh`, `npc_parts.py assemble --head mh`) and all 12 variants assemble inside the
16,000-triangle cap with it. The procedural body, hands and clothing remain unchanged. The
MakeHuman body prototype and its temporary head/hand swap live under `tmp/npc-research/` and are
not the shipped path. This overhaul supplies the reusable head/face and hand parts that both the
procedural characters and the MakeHuman body can consume.
This workstream supersedes the face/head and hands items in §C until the prototypes are approved;
the rest of §C remains the broad review checklist.
@@ -80,8 +82,6 @@ the rest of §C remains the broad review checklist.
### 3. Head and face prototype
### 3. Head and face prototype
- [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,
@@ -95,6 +95,19 @@ the rest of §C remains the broad review checklist.
- [x] 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.
- [x] Rebuild a standalone MakeHuman-derived head from the researched CC0 base and save it to
`assets/models/blender_src/makehuman_head.glb`; record provenance/review in
`docs/makehuman-head-review.md` and the preview sheet. Final build: 12,642 tris /
7,690 split verts, bounds x −0.094..0.094, y −0.180..0.042, z 1.530..1.803. The owner
accepted the **basic human head** on 2026-11-04; the automated vision gate still records
the eye-region REJECT as deferred polish (globe depth, lid overlap, iris at three-quarter).
The assembled path now adds a skull-conforming hair cap, a hidden-scalp trim and a narrowed
neck/shoulder body. Assembled `npc_casual --head mh`: 14,753 tris / 12,822 raw / 12,823
seam-split verts / 14 bones / 9 materials, `inspect` clean.
- [ ] Build true upper/lower eyelid masses that overlap the globe in front, three-quarter and
profile views (not a socket hole), then re-run the vision gate. Deferred: the owner accepted
the basic head, so this is polish, not a rollout blocker.
Compared `head_proto` at seg 24 (670 verts), 48 (1,558 verts) and 64 (2,470 verts) at 1.85 m
and 1.5 m, front/three-quarter/profile. Vision-judged: **s48 wins** — s24 is too faceted
(nose/lips melt, jagged silhouette), s64 is visually identical to s48 at both distances
@@ -108,12 +121,11 @@ the rest of §C remains the broad review checklist.
imported GLB came out black and is superseded by the proper `.blend`-source bake). Flag: the
prototype ears reach x ±0.162 vs the 0.1575 head-radius reference, so headgear may clip and
must be re-measured at rollout.
- [ ] Get owner sign-off on the one-variant prototype, then roll the approved head/face into the
other 11 variants one asset family at a time, with a before/after sheet per batch.
**Blocked on owner sign-off.** Sign-off material is ready: the s48 prototype vs the current
`_face` at 1.85 m front/three-quarter/profile
(`docs/blender_previews/sheet_head_proto_before_after.jpg`); the s24/s48/s64 comparison table
is in `docs/3d-model-review.md`. Rollout waits for the owner's go/no-go.
- [-] Owner review of the assembled one-variant prototype (`npc_casual --head mh`) before the
12 shipped GLBs change. The basic human head is accepted (2026-11-04); the review now covers
the skull-conforming hair cap and the narrowed neck/shoulder body silhouette. Local renders:
`tmp/npc-parts/prototype_mh/renders/` (portrait front/three-quarter/profile + full body).
Rollout starts only after this review.
### 4. Hands and grip prototype
@@ -142,17 +154,105 @@ the rest of §C remains the broad review checklist.
### 5. Assembly and rollout
**MakeHuman 12-variant rollout.** - [x] Make the head source selectable from the main builder:
`blender_assets.py build --head mh --only npc_casual,...` and
`npc_parts.py assemble --variant <name> --head mh --out <stem>`. The default stays `face`
until the hair/body review passes.
- [x] Verify every variant assembles with the MakeHuman head inside the 16,000/16,000 cap
(pre-export triangle counts, `inspect` clean; `npc_casual` seam-split verts 12,823):
`npc_casual 14,753`, `npc_jacket 15,153`, `npc_coat 15,817`, `npc_vest 15,579`,
`npc_dress 15,215`, `npc_hoodie 15,837`, `npc_firefighter 15,987`, `npc_guard 15,729`,
`npc_crowd_a 14,947`, `npc_crowd_b 14,693`, `npc_crowd_c 15,433`, `npc_crowd_d 15,833`.
- [x] Recover head budget without touching the accepted face: remove head faces fully covered by
the hair cap, keep boundary faces as a safety band, and trim the hidden neck plug to 12
segments. This saves ~950 tris on every variant.
- [ ] Owner approves the hair cap and full-body silhouette on the local `npc_casual` prototype.
**Status (2026-11-04):** the current body candidate is the MPFB full-body prototype below,
not the narrowed procedural body; the 12-variant rebuild is blocked on that review.
- [ ] Rebuild the 12 committed character GLBs with `--head mh`, in asset-family batches, then
regenerate their AO maps and manifest entries. Never run a no-op full-library rebuild.
- [ ] Run `blender_assets.py audit`, `fit`, `inspect` and `texels`, plus
`python3 tools/npc_proportions.py report`; record the 12-variant proportion table.
- [ ] Re-run the Godot NPC tests and capture the 12 × 6 baseline
(`src/test/capture_npc_baseline.gd` → `python3 tools/npc_baseline.py sheet`); inspect the
sheet for outliers before committing.
- [ ] Update `docs/makehuman-head-review.md`, `docs/3d-model-review.md`,
`docs/blender_previews/README.md`, `docs/npc-model-sourcing.md`, `CREDITS.md` and
`docs/development-status.md` with the accepted head/hair/body state and final counts.
- [ ] Integrate the approved parts into `build_character` with deterministic joins, weights, UVs and
AO. The final output remains one `npc_base` skinned mesh under the existing 13-core armature;
optional extra bones are allowed but the core rest positions stay fixed;
no second rig, no material-slot rename.
- [ ] Keep the MakeHuman body prototype consumable with the same approved head/hands; do not let it
grow a second face/hand implementation while the part pipeline is being proven.
- [ ] Rebuild only the changed character GLBs, AO maps and manifest entries; never run a no-op full
library rebuild. Commit one asset family per change.
- [ ] Re-run `blender_assets.py audit`/`fit`/`inspect`, `npc_proportions.py`, the 24-NPC budget,
Godot and Python tests, the 48-frame baseline and the 96-frame final-context review.
### Full-body MPFB prototype
Goal: assemble the accepted MakeHuman head onto a CC0 MPFB body with project hands, shoes and
14-bone contract rig, inside the 16,000/16,000 character budget. Local development only until the
owner reviews the silhouette and a production integration decision is recorded.
- [x] Build the dev pipeline at `tmp/npc-research/build_mpfb_npc.py`: create a macro-driven MPFB
human, bake targets, add the `game_engine` rig, capture arm points/weights, remove the rig,
apply the helper MASK, scale to 1.81 m, retarget the A-pose arms to the contract wrist
(`x=±0.27`, `z=0.78`), cut the neck at 1.505, decimate, bisect the clothing boundaries, paint
the five slots, add a short neck flare, project hands, rounded shoe shells, cuffs, the
accepted head and the 14-bone armature. Current output: **15,317 tris / 13,690 glTF verts /
13,702 seam-split verts / 1.810 m / 14 bones / 10 materials**; `inspect` clean, feet grounded.
The torso is inflated radially by 12% through z 0.72–1.40 (tapering out at the collar) so the
shirt reads as fabric rather than body paint.
- [x] Replace the spiky decimated MPFB hands with the project palm/finger primitives, straighten the
splayed MPFB feet so the shoes point forward, and use rounded shoe shells + cuffs instead of
flat blocks. Use flat slot colours in the prototype because the one-shot MPFB UV unwrap does
not match the per-slot NPC atlases.
- [x] Fix the neck junction without touching the accepted face: cut the decimated neck column
radially (all-vertex test inside r=0.085) so the shirt shoulder silhouette stays intact,
then bridge the opening with a smooth four-ring lathe neck (r 0.100 → 0.056, 20 segments).
The lower rings are painted cloth so the opening reads as a crew neck rather than bare
chest. No torus, disk shelf, neck brace or sawtooth collar.
- [x] Reject destructive vertex smoothing at the neck: a `smooth_vert` pass moved the open body
boundary away from the neck cone, opening a dark hole. The lathe gives smooth shading
without moving the collar boundary.
- [x] Shrink the shoes and seat the ankles: shoe shell 0.12 × 0.30 × 0.11 m, an inset 0.111 ×
0.286 m black sole, a tongue and shoe centre x=0.17, with a 0.08 m pant cuff (r 0.055/0.061)
bridging leg and shoe. The sleeve cuffs now use the `cloth` slot and the `accessory` slot is
carried by the sole, so the cuffs read as sleeve ends instead of metal bands.
- [x] Replace the bulbous toe/heel spheres with a single bevelled shoe shell (bevel 0.032, 2
segments) plus tongue and inset sole, and add one light smoothing pass to the closed
thigh/hip region. Protruding flat sole plates read as skates or bricks; laces were dropped
for triangle budget after they proved too small to read.
- [x] Reject the untucked shirt-hem band: both the 0.09 m and 0.12 m cloth cylinders read as a
hanging white flap at the crotch. The V hem needs a proper clothing shell, not a band.
- [x] Fix the neck material rule: the `radius < 0.08` skin test had no Z floor, so it painted a
skin patch at the crotch. It is now guarded to `z > 1.35`; the crotch stays pants.
- [x] Reject three cheap hem fixes: lowering the shirt to z 0.80 turns it into a unitard; tilting
the hem up at the front reads as high-cut swimwear; a 13% hem flare reads as a peplum.
The remaining V is the dark crotch hollow under a body-painted shirt, not a slot bug.
- [ ] Owner reviews the updated MPFB body renders (`tmp/npc-parts/mpfb_proto/renders84/`).
Remaining known defects: the crotch hollow still reads as a V under the shirt hem, the
shirt is only partly loosened by the 12% torso inflation, and the 2k-tri body budget leaves
some angular thigh facets. A separate loose shirt shell is the proper fix for the first
two; these are polish, not blockers for the body direction.
- [ ] Build a proper loose shirt shell instead of body-painted cloth. The quick open, flared
solidified tube (0.150→0.196 m, z 1.05→0.80, 24 segments, ~288 tris) read as a hanging
apron/tabard and was rejected. The shell needs a shaped torso, armholes and a curved hem,
not a tube. It must fit in the 683 spare triangles or come with a head/body rebalance.
- [ ] Rebalance the triangle budget before rollout: 683 spare is below the ~1,200 the firefighter
and hoodie variants need. Protect the head budget first; do not lower the accepted face.
- [ ] Fix the low-budget seams before any rollout: weld the torso/hip/arm components after the
bisects, protect the hip/knee loops during decimation (or rebalance head/body triangles).
- [ ] Record the production integration decision: either bake the approved MPFB body to a committed
source GLB (CI and fresh checkouts stay MPFB-free) or keep the procedural body as the shipped
path. Do not add the MPFB extension as a build/CI dependency.
- [ ] If approved, roll the MPFB body into the 12 variants and regenerate AO maps, manifest entries,
`inspect`/`fit`/`audit`/`texels`, the proportion report and the 12 × 6 baseline.
### 6. QA, docs and exit
- [ ] Capture front/three-quarter/profile head comparisons and open/grip hand comparisons; add
Binary file not shown.
+13
View File
@@ -257,6 +257,16 @@ at the MakeHuman landmarks. `build_character(name, head="mh")` assembles it; the
`inspect` (14 bones, skinned, no issues). The judge reads the face ~3.5/5; remaining polish is
welding the added eye/brow pieces into the head shell and a skull-conforming hair cap.
**Standalone head re-review (2026-11-04):** the separate
`assets/models/blender_src/makehuman_head.glb` prototype (12,641 tris / 7,690 split verts,
with a skin eyelid mask, recessed globes, flat iris/pupil discs and surface-anchored brows)
was **accepted by the owner** as a basic human head. The automated vision gate still returns
REJECT on the eye/lid region (globe depth, lid overlap, iris at three-quarter) as deferred
polish. The assembled `npc_casual --head mh` now adds a skull-conforming hair cap, a hidden
scalp trim and a narrowed neck/shoulder body: 14,753 tris / 12,823 seam-split verts /
14 bones / 9 materials, `inspect` clean. All 12 variants count 14,693–15,987 tris. See
[`makehuman-head-review.md`](makehuman-head-review.md).
### Head subdivision comparison (2026-11-04)
Three `head_proto` variants at UV-sphere segments 24/48/64 were rendered at 1.85 m and 1.5 m in
@@ -485,6 +495,9 @@ and the full carded-cluster foliage pass is the one remaining owner-gated P1. No
## Next actions
- Review the assembled [`makehuman-head-review.md`](makehuman-head-review.md) prototype (hair cap and full-body silhouette) before rebuilding the 12 shipped character GLBs.
Tracked in [`TODO.md`](../TODO.md) `3D model re-evaluation`: the remaining `A. Baseline…` item is
this owner review; `B`/`C`/`D` carry the per-family work, and `E` adds the pose/weight,
collider-coverage and scorecard-coverage gates this document found missing.
+1
View File
@@ -13,6 +13,7 @@ This folder holds the project documentation, visual references and generated QA
- [`../PLAN.md`](../PLAN.md) — product, design intent and development priorities
- [`../TODO.md`](../TODO.md) — priority-ordered active tasks and roadmap
- [`npc-model-sourcing.md`](npc-model-sourcing.md) — character-source evaluation, licence rules and the character-development plan
- [`makehuman-head-review.md`](makehuman-head-review.md) — local MakeHuman head prototype, provenance and review verdict
- [`architecture.md`](architecture.md) — arc42 system architecture
- [`getting-started.md`](getting-started.md) — setup, run/test commands and controls
- [`asset-tooling.md`](asset-tooling.md) — image/model pipelines and screenshot gates
+2
View File
@@ -140,11 +140,13 @@ MakeHuman sources against the character contract and records the 2026-11-03 dire
```bash
flatpak run org.blender.Blender --background --python "$PWD/tools/blender_assets.py" -- build
flatpak run org.blender.Blender --background --python "$PWD/tools/blender_assets.py" -- build --head mh
flatpak run org.blender.Blender --background --python "$PWD/tools/blender_assets.py" -- render
python3 tools/blender_assets.py sheet # docs/blender_previews/sheet_*.jpg
python3 tools/blender_assets.py audit # no Blender needed
python3 tools/blender_assets.py fit # gear-vs-body fit report
python3 tools/blender_assets.py texels # per-slot texel density (px/m) of the characters
# `build --head mh` selects the MakeHuman head; the default stays `face` until rollout.
godot --headless --path . --import
```
+1
View File
@@ -9,6 +9,7 @@ capture harnesses.
| --- | --- |
| [![sheet_npc.jpg](sheet_npc.jpg)](sheet_npc.jpg) <br> `sheet_npc.jpg` | Eight skinned character variants (casual, jacket, coat, hi-vis vest, dress, hoodie, firefighter, guard) with their gear attach points. |
| [![sheet_npc_baseline.jpg](sheet_npc_baseline.jpg)](sheet_npc_baseline.jpg) <br> `sheet_npc_baseline.jpg` | Neutral studio baseline: 8 characters x front, three-quarter, profile, back, full body and crowd views. |
| [![sheet_makehuman_head.jpg](sheet_makehuman_head.jpg)](sheet_makehuman_head.jpg) <br> `sheet_makehuman_head.jpg` | MakeHuman head: front, three-quarter and profile review views. The owner accepted the basic head; the automated vision gate still rejects the eye/lid region as deferred polish. See [`../makehuman-head-review.md`](../makehuman-head-review.md). |
| [![sheet_npc_baseline_compare-body.jpg](sheet_npc_baseline_compare-body.jpg)](sheet_npc_baseline_compare-body.jpg) <br> `sheet_npc_baseline_compare-body.jpg` | Neck/shoulder transition: fuller neck base and a lower shoulder root. |
| [![sheet_npc_baseline_compare-face-detail.jpg](sheet_npc_baseline_compare-face-detail.jpg)](sheet_npc_baseline_compare-face-detail.jpg) <br> `sheet_npc_baseline_compare-face-detail.jpg` | Face detail: nose ala wings, a single rounded mouth and a darker ear concha. |
| [![sheet_npc_baseline_compare-face.jpg](sheet_npc_baseline_compare-face.jpg)](sheet_npc_baseline_compare-face.jpg) <br> `sheet_npc_baseline_compare-face.jpg` | Face-depth pass: recessed sclera, upper lid and sculpted chin/cheek planes. |
Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

+139
View File
@@ -0,0 +1,139 @@
# MakeHuman head prototype
Status: **owner-accepted head, assembled prototype, not yet shipped.**
The owner accepted the basic human head on 2026-11-04. The automated vision gate
still records REJECT on the eye/lid region as deferred polish. The asset is a
standalone head/neck extracted from the CC0 MakeHuman base in
`tmp/npc-research/makehuman/base.obj`; the assembled path now adds hair, a hidden
scalp trim and the procedural body. The 12 shipped GLBs have not been rebuilt
with it yet.
## Asset
- GLB: [`assets/models/blender_src/makehuman_head.glb`](../assets/models/blender_src/makehuman_head.glb)
- Review sheet: [`blender_previews/sheet_makehuman_head.jpg`](blender_previews/sheet_makehuman_head.jpg)
- Source workflow (development-only): `tmp/npc-research/build_makehuman_head.py`
— extracts the MakeHuman head, cuts it at the neck, keeps the head/eye/mouth
components, cuts an almond aperture through a skin eyelid mask, adds recessed
eyeballs with flat iris/pupil discs, anchors brows to the face surface, renders
front/three-quarter/profile and exports the standalone GLB.
- Measurements (final standalone build, `inspect`): **12,641 triangles / 7,690
split vertices**; bounds approximately `x ±0.094 m`, `y -0.180…0.042 m`,
`z 1.530…1.803 m` in project Z-up space.
- Assembled `npc_casual --head mh`: **14,753 triangles / 12,823 seam-split
vertices / 14 bones / 9 materials**, `inspect` clean. All 12 variants count
14,693–15,987 triangles with the same head.
- Materials used: `skin`, `eye_white`, `iris`, `pupil`, `hair`. No textures are
baked into this prototype.
![MakeHuman head review sheet](blender_previews/sheet_makehuman_head.jpg)
## What was done
1. Imported the CC0 MakeHuman base OBJ and cleared the importer axis rotation.
2. Applied the project scale/axis transform: 1.82 m body, Z-up, metres,
front −Y.
3. Cut the head at the neck (`z ≈ 1.53`) rather than keeping the cut shoulders;
capped the neck opening.
4. Built an eyelid mask by subtracting an almond aperture from a skin sphere
centred on each eye (`solver = "EXACT"`), then recessed the globes so the
cornea sits behind the lid plane.
5. Added flat iris/pupil discs on the globe axis; disabled backface culling so
the discs stay visible at three-quarter angles.
6. Anchored brow strips to the skin with object-space ray casts and added a
shallow supraorbital ridge so they do not float, then reduced the crown
height to improve the profile silhouette.
7. Fixed a material-remap bug introduced by joining the extra eye objects: the
joined head faces were pointing at the `eye_white` slot, making the whole
head render as white clay. The largest connected component is now forced
back onto `skin` after the join.
8. Rendered front/three-quarter/profile views in a neutral review studio and
exported the standalone GLB.
## Provenance
- Base mesh: MakeHuman / MPFB base, CC0, from the local research set
`tmp/npc-research/makehuman/`; see [`CREDITS.md`](../CREDITS.md).
- The head is a **derived prototype**, not a shipped NPC. It does not enter
`assets/models/blender/npc/`, `npc_base`, the runtime `NpcAvatar` path, CI, or
a release package until the NPC overhaul plan accepts it.
- Source photographs/proportions are not used as textures here; this is a
geometry-only head built from the researched model data.
## Review criteria
Accept only if:
- the head reads as human at front/three-quarter/profile and the neck is a
clean head/bust silhouette, not a shoulder cut;
- eyes, brow, nose, mouth and ears are readable without spikes, holes or
floating shells;
- the asset is inside the character budget and has no script/contract violation;
- the prototype remains clearly local and un-integrated until the part pipeline
is approved.
## Judge result
**Owner verdict: ACCEPT (basic human head, 2026-11-04).**
**Automated vision gate: REJECT** (vision judge `z-ai/glm-5.3-flash`, final
standalone build render set above). The eye/lid findings below remain deferred
polish; they are no longer a rollout blocker because the owner accepted the head.
Blocking findings:
- **P0 — globe depth is inconsistent across views.** The front/three-quarter
reads still show a bead-like iris with a pale sclera halo, while the profile
reduces the eye to a brown sliver. The judge reads this as a faked recess
rather than a socketed globe under a lid.
- **P0 — no convincing lid overlap.** The iris remains nearly circular in front
and the upper lid does not visibly clip the globe; the skin lid mask reads as
a crater around a bead.
- **P0 — iris orientation reads decal-locked** at three-quarter, so the gaze
appears to drift off the head axis.
- **P0 — the aperture/lid mask is too circular** and lacks medial/lateral
canthi, so the eye still reads as a doll eye at portrait distance.
Should-fix findings:
- the brow tubes are better anchored but the medial ends still detach in
three-quarter/profile;
- nose lacks a defined tip/wing and nostrils;
- mouth is a faint geometric seam with no lip volume;
- crown/back-of-skull is still faceted and egg-like in profile;
- the catchlight is baked geometry and is not consistent between eyes.
What passed: the skin/eye material remap is correct, the eye openings are
closed, the standalone GLB reports **12,641 triangles / 7,690 split vertices**,
and an assembled `npc_casual` with this head passes `inspect` at **14,753 /
16,000 triangles, 12,823 seam-split vertices, 14 bones, no issues**.
Known limits: bald mannequin-like presentation, stylized geometry-only
iris/pupil, no rig, no UV texture pass, and the neck cap is a review cut rather
than a body-welded seam.
## Assembled prototype (2026-11-04)
- `_makehuman_head` imports the GLB and adds a **skull-conforming hair cap**
derived from the closed head shell (scaled shell + boolean front-sphere
cut + decimate). Hair skips the old `hair_normal.png` because the skull UVs
produced large dark tangent patches.
- Head faces fully covered by the cap are deleted to recover ~950 triangles;
boundary faces stay as a safety band, so the accepted face geometry is
untouched. The hidden neck plug uses 12 segments.
- The procedural body was narrowed at the neck and shoulders (chest 0.158,
neck base 0.060, neck top 0.054, shoulder root z 1.345) and its skin decimate
dropped to 0.21 so all 12 variants fit the 16,000 cap.
- Local review renders: `tmp/npc-parts/prototype_mh/renders/` (portrait
front/three-quarter/profile and full-body front/three-quarter).
## Deferred polish
- Replace the radial lid mask with real upper/lower lid masses that fold over
the globe, with pointed canthi, and pass the profile view.
- Move the iris onto a curved cornea cap so its orientation follows the globe at
three-quarter angles, and use a lighting-driven catchlight instead of baked
white geometry.
- Refine the hair cap silhouette (current short cap reads as a helmet) and
improve the full-body shoulder/arm shape before the 12-variant rollout.
- Add a clean UV/material pass and a contract armature/weights when the head
becomes part of the shipped library characters.
+29 -3
View File
@@ -46,9 +46,35 @@ project head/hands swap reduced this to **6,407 / 16,000 tris and**
**6,651 / 16,000 verts**, but manual shoulder/neck retopology and part cleanup
remain before it can be considered shippable.
The head/face/hand overhaul in [`TODO.md`](../TODO.md) is the prerequisite: the
MakeHuman body should reuse the approved local head/hand parts rather than
forking a second face implementation.
A standalone MakeHuman head is recorded in
[`makehuman-head-review.md`](makehuman-head-review.md). The owner accepted the basic
human head on 2026-11-04; the assembled path (`build_character(head="mh")`) adds a
skull-conforming hair cap and hides the scalp under it. The 12 shipped GLBs still use
the procedural head until the hair/body review and rollout in [`TODO.md`](../TODO.md).
The MakeHuman body should reuse the approved local head/hand parts rather than forking
a second face implementation.
The current local MPFB full-body prototype (`tmp/npc-research/build_mpfb_npc.py`) assembles the
accepted head on an MPFB 2.0.18 body at **15,317 / 16,000 tris**, 1.810 m, 14 bones and 10
materials, with project hands, bevelled shoe shells with inset soles and tongues, pant cuffs,
cloth sleeve cuffs and flat slot colours. The torso is radially inflated by 12% through z 0.72–1.40
to loosen the shirt. It is a review candidate, not a shipped asset. The neck is a radial cut
of the decimated column bridged by a four-ring, 20-segment lathe (lower rings painted cloth,
reading as a crew neck); the ankles are hidden by 0.11 m shoe shells and 0.08 m pant cuffs, and a
light smoothing pass softens the closed thigh/hip region. The neck skin test is guarded to z >
1.35 so it cannot paint the crotch. Remaining polish: the crotch hollow still reads as a V under
the shirt hem, a partly loosened shirt and angular thigh facets; a separate loose shirt shell is
the fix. Lowering the hem to z 0.80, tilting it up at the front and a 13% hem flare were rejected
as a unitard, high-cut swimwear and a peplum respectively. A quick open, flared solidified tube
(0.150→0.196 m, z 1.05→0.80, 24 segments) read as a hanging apron/tabard; the shell needs a shaped
torso, armholes and a curved hem.
A torus collar, cloth cylinder collar, skin neck-blend sphere, destructive vertex-smoothing pass
and untucked shirt-hem band were tried and removed because they read as a brace, shelf, donut or
hanging flap, or tore a hole at the collar. Protruding flat sole plates read as skates or bricks,
so the shoe detail is an inset sole and tongue under a rounded shell; laces were dropped for
triangle budget. The production integration decision (bake a committed source GLB or keep the
procedural body) and the owner review are tracked in `TODO.md`; MPFB itself must not become a
CI/build dependency.
## Provenance rules
+13 -4
View File
@@ -1,5 +1,13 @@
# Head rework — review page (2026-11-04)
> **Historical page / changed source (2026-11-04).** The numbers below describe the
> earlier decimated MakeHuman head (1,548-tri skin). The shared source
> `assets/models/blender_src/makehuman_head.glb` now holds the higher-resolution
> standalone prototype. The owner accepted the basic head; the automated vision gate
> still records REJECT on the eye/lid region as deferred polish. See
> [`../../makehuman-head-review.md`](../../makehuman-head-review.md). Re-run the
> library head build before relying on the comparison counts below.
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.
@@ -52,8 +60,9 @@ Left to right: front, three-quarter, profile.
4. The head skin is 1.5k tris (smooth but low-poly); a retopology pass could add resolution if a
portrait shows faceting.
## Decision needed
## Decision (2026-11-04, owner)
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.
The MakeHuman-head direction is approved for the basic human head. The remaining review is the
assembled prototype's hair cap and full-body silhouette. After that, roll the head into the other
11 variants (one asset family at a time) with a before/after sheet per batch, then re-run the
24-NPC budget + AO bake.
+161 -31
View File
@@ -276,13 +276,17 @@ def material(name, textured_solid=False):
check_existing=True)
albedo.image.colorspace_settings.name = "sRGB"
links.new(albedo.outputs["Color"], bsdf.inputs["Base Color"])
normal_tex = nodes.new("ShaderNodeTexImage")
normal_tex.image = bpy.data.images.load(
str(NPC_TEX_DIR / f"{name}_normal.png"), check_existing=True)
normal_tex.image.colorspace_settings.name = "Non-Color"
nmap = nodes.new("ShaderNodeNormalMap")
links.new(normal_tex.outputs["Color"], nmap.inputs["Color"])
links.new(nmap.outputs["Normal"], bsdf.inputs["Normal"])
# The hair normal map was authored against the old cap UVs. Reusing it
# on the skull-conforming MakeHuman cap produces large dark tangent
# patches, so hair keeps its albedo but takes flat shading normals.
if name != "hair":
normal_tex = nodes.new("ShaderNodeTexImage")
normal_tex.image = bpy.data.images.load(
str(NPC_TEX_DIR / f"{name}_normal.png"), check_existing=True)
normal_tex.image.colorspace_settings.name = "Non-Color"
nmap = nodes.new("ShaderNodeNormalMap")
links.new(normal_tex.outputs["Color"], nmap.inputs["Color"])
links.new(nmap.outputs["Normal"], bsdf.inputs["Normal"])
rough = {"skin": 0.6, "cloth": 0.86, "pants": 0.92,
"accessory": 0.5, "hair": 0.7}[name]
bsdf.inputs["Roughness"].default_value = rough
@@ -550,22 +554,21 @@ def _skin_body(spec):
"guard": 0.05}.get(layer, 0.0)
leg = 1.0 + {"responder": 0.1, "dress": -0.1}.get(layer, 0.0)
nodes = [ # (pos, radius_x, radius_y)
((0, 0, 0.80), 0.168 * w, 0.116 * d), # 0 pelvis
((0, 0, 0.96), 0.146 * w * bulk, 0.103 * d * bulk), # 1 waist
((0, 0.0, 1.18), 0.168 * w * bulk, 0.114 * d * bulk), # 2 chest
((0, 0.0, 1.355), 0.170 * w * bulk, 0.108 * d * bulk), # 3 upper chest
# A slightly fuller neck base and a lower shoulder root give the
# head/neck/shoulder line a trapezius slope instead of a thin post on
# a hanger. The head and collar still sit above this band.
((0, 0, 1.43), 0.074, 0.072), # 4 neck base
((0, -0.005, 1.53), 0.060, 0.060), # 5 neck top
((0, 0, 0.80), 0.158 * w, 0.112 * d), # 0 pelvis
((0, 0, 0.96), 0.142 * w * bulk, 0.101 * d * bulk), # 1 waist
((0, 0.0, 1.18), 0.155 * w * bulk, 0.110 * d * bulk), # 2 chest
((0, 0.0, 1.335), 0.158 * w * bulk, 0.105 * d * bulk), # 3 upper chest
# Narrow neck and a lower, sloped shoulder root. The old wide neck base
# and horizontal shoulder shelf made the accepted head read as small.
((0, 0, 1.42), 0.060, 0.060), # 4 neck base
((0, -0.005, 1.53), 0.054, 0.054), # 5 neck top
]
edges = [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]
for s in (-1.0, 1.0):
base = len(nodes)
nodes += [
((0.20 * s * w, 0, 1.37), 0.080 * t * sleeve, 0.074 * t * sleeve), # shoulder
((0.27 * s * w, 0, 1.23), 0.058 * t * sleeve, 0.058 * t * sleeve), # upper arm
((0.19 * s * w, 0, 1.345), 0.070 * t * sleeve, 0.068 * t * sleeve), # shoulder
((0.27 * s * w, 0, 1.22), 0.054 * t * sleeve, 0.055 * t * sleeve), # upper arm
((0.275 * s * w, 0.005, 1.06), 0.046 * t * sleeve, 0.048 * t * sleeve), # elbow
((0.275 * s * w, -0.01, 0.78), 0.034 * t, 0.030 * t), # wrist
]
@@ -591,7 +594,7 @@ def _skin_body(spec):
sub = obj.modifiers.new("Sub", "SUBSURF")
sub.levels = 2
dec = obj.modifiers.new("Decimate", "DECIMATE")
dec.ratio = 0.26
dec.ratio = 0.21
dec.use_symmetry = True
dec.symmetry_axis = "X"
_apply_all(obj)
@@ -716,6 +719,102 @@ def _hair_cap(name, mat, front_z, back_z, radius=0.163, center=(0, 0.008, 1.645)
return _finish(_link(name, bm), mat, smooth=True, sharp_angle=70)
def _hair_curve(style):
"""Hairline endpoints and skull offset for a hair style."""
if style == "buzz":
return 1.734, 1.608, 1.035
return 1.714, 1.588, 1.055
def _trim_hidden_scalp(head, style):
"""Delete the scalp that sits under the hair cap to free triangle budget.
The cap is a scaled copy of the skull, so any head face whose scaled
vertices all fall outside the hairline sphere is fully covered. Keeping
boundary-touching faces leaves a safety band under the hair rim; the open
hole is never visible and the accepted face geometry is untouched.
"""
front_z, back_z, scale = _hair_curve(style)
center = Vector((0.0, 0.0, 1.665))
cutter_z = (front_z + back_z) * 0.5 - 0.396
bm = bmesh.new()
bm.from_mesh(head.data)
def covered(p):
q = center + (p - center) * scale
return (q.x * q.x + (q.y + 0.41) ** 2
+ (q.z - cutter_z) ** 2) > 0.56 ** 2
doomed = [f for f in bm.faces if all(covered(v.co) for v in f.verts)]
if doomed:
bmesh.ops.delete(bm, geom=doomed, context="FACES")
loose = [v for v in bm.verts if not v.link_faces]
if loose:
bmesh.ops.delete(bm, geom=loose, context="VERTS")
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
bm.to_mesh(head.data)
bm.free()
def _hair_from_head(source, style, mat_hair):
"""Build the hair cap as a solid shell derived from the skull.
A separate hair ellipsoid cannot follow the broad MakeHuman crown, and an
open, face-deleted copy produces a sawtooth hairline. Scaling the closed
head shell outwards and boolean-cutting the face with a large front sphere
gives a watertight cap whose curved hairline is smooth from every angle.
"""
front_z, back_z, scale = _hair_curve(style)
cap = source.copy()
cap.data = source.data.copy()
cap.name = "HairCap"
bpy.context.scene.collection.objects.link(cap)
center = Vector((0.0, 0.0, 1.665))
for v in cap.data.vertices:
v.co = center + (v.co - center) * scale
# The cutter is a big sphere in front of the face: its upper surface rises
# toward the brow and falls toward the nape, which is the hairline curve.
mid_z = (front_z + back_z) * 0.5
cutter_z = mid_z - 0.396
bpy.ops.mesh.primitive_uv_sphere_add(segments=32, ring_count=20, radius=0.56,
location=(0.0, -0.41, cutter_z))
cutter = bpy.context.object
bpy.context.view_layer.objects.active = cap
mod = cap.modifiers.new("Hairline", "BOOLEAN")
mod.operation = "DIFFERENCE"
mod.solver = "EXACT"
mod.object = cutter
bpy.ops.object.modifier_apply(modifier=mod.name)
bpy.data.objects.remove(cutter, do_unlink=True)
# The boolean result is still head-density. Hair needs silhouette, not
# pores, and the character budget has to stay under 16k triangles.
dec = cap.modifiers.new("HairReduce", "DECIMATE")
dec.ratio = 0.34
dec.use_collapse_triangulate = True
bpy.ops.object.modifier_apply(modifier=dec.name)
bm = bmesh.new()
bm.from_mesh(cap.data)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
bm.to_mesh(cap.data)
bm.free()
cap.data.materials.clear()
cap.data.materials.append(mat_hair)
cap.data.shade_smooth()
cap.data.set_sharp_from_angle(angle=math.radians(55))
parts = [cap]
if style == "long":
parts.append(sphere("HairBack", 0.095, (0, 0.060, 1.560), mat_hair,
seg=14, rings=9, scale=(0.98, 0.55, 1.55)))
for s in (-1.0, 1.0):
parts.append(sphere("Lock", 0.048, (0.089 * s, -0.012, 1.585),
mat_hair, seg=8, rings=6,
scale=(0.55, 0.85, 2.05)))
if style == "bun":
parts.append(sphere("Bun", 0.060, (0, 0.100, 1.775), mat_hair,
seg=12, rings=8, scale=(1.0, 0.9, 0.95)))
return parts
def _hair(style, mat_hair, broad=False):
parts = []
if style == "buzz":
@@ -968,11 +1067,12 @@ def _head_proto(mats, spec, subdiv=48):
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.
The AI-judged head rework (2026-11-04) picked the MakeHuman head over the
displacement shell: it keeps the nose/chin/jaw landmarks. Newer part builds
carry their own embedded eye/iris/pupil/mouth geometry, so those materials
are preserved; the older shell-only asset still gets the project's
sclera/iris/lid/brow pieces added at the MakeHuman landmarks. Blender Z-up,
face front −Y.
"""
skin, acc, hair = mats["skin"], mats["accessory"], mats["hair"]
white = material("eye_white", textured_solid=True)
@@ -991,9 +1091,32 @@ def _makehuman_head(mats, spec):
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).
# Newer MakeHuman part builds carry their own eye/mouth/brow materials and
# geometry. Use those instead of adding the project's primitive face pieces
# a second time; only the old shell-only asset falls through to that path.
embedded = {m.name.split(".")[0] for m in head.data.materials if m}
if "eye_white" in embedded:
# Preserve the embedded iris/pupil/mouth materials; only map the
# project contract slots onto the imported head.
remap = {"skin": skin, "eye_white": white, "hair": hair}
for index, mat in enumerate(list(head.data.materials)):
if mat:
target = remap.get(mat.name.split(".")[0])
if target is not None:
head.data.materials[index] = target
# Bridge the 1.48-1.53 m neck gap between the MakeHuman cut and the
# procedurally built torso. The plug sits inside both shells, so only
# the visible band reads as neck and no seam ring reaches the surface.
plug = cyl("NeckPlug", 0.062, 0.052, 0.095, (0, 0.002, 1.500), skin,
seg=12, scale=(1.10, 1.0, 1.0))
hair_style = "buzz" if spec["layer"] == "hood" else spec["hair"]
hair_parts = _hair_from_head(head, hair_style, hair)
_trim_hidden_scalp(head, hair_style)
return [head, plug] + hair_parts
# The old 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
@@ -1229,7 +1352,8 @@ def build_character(name, head="face"):
else:
head_parts = _face(mats, spec)
hair_style = spec["hair"] if spec["layer"] != "hood" else "buzz"
head_parts += _hair(hair_style, mats["hair"])
if head != "mh":
head_parts += _hair(hair_style, mats["hair"])
head = join(head_parts, "head")
_auto_weights(head, layout, rigid=lambda p: "Head")
@@ -1795,10 +1919,10 @@ def build_prop(name):
# Build + render entry points (inside Blender)
# ---------------------------------------------------------------------------
def _build_one(category, name):
def _build_one(category, name, head="face"):
reset_scene()
if category == "npc":
objs = build_character(name)
objs = build_character(name, head=head)
elif category == "npc_gear":
objs = build_gear(name)
else:
@@ -2748,6 +2872,9 @@ def main(argv=None):
for cmd in ("build", "render"):
s = sub.add_parser(cmd)
s.add_argument("--only", default="")
if cmd == "build":
s.add_argument("--head", default="face", choices=("face", "proto", "mh"),
help="character head source (default: face)")
a = sub.add_parser("audit")
a.add_argument("--json", action="store_true")
fit = sub.add_parser("fit")
@@ -2838,7 +2965,10 @@ def main(argv=None):
file=sys.stderr)
return 2
for cat, name in _selected(args.only):
(_build_one if args.cmd == "build" else _render_one)(cat, name)
if args.cmd == "build":
_build_one(cat, name, head=args.head)
else:
_render_one(cat, name)
if args.cmd == "build":
results = [audit_asset(c, n) for c, n in catalogue()]
write_manifest([r for r in results if r["vertices"]])