diff --git a/MEMORY.md b/MEMORY.md index fad3278..3379ba2 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -10,15 +10,23 @@ 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 prototype (2026-11-04):** `assets/models/blender_src/makehuman_head.glb` - is a local 12,641-tri/7,690-split-vert head derived from the CC0 MakeHuman base, - with a review sheet and `docs/makehuman-head-review.md`. The latest build uses a - skin eyelid mask (almond aperture cut from a sphere), recessed globes, flat iris/pupil - discs and surface-anchored brows. The automated vision gate still rejected it: - the eye depth reads inconsistently across front/three-quarter/profile and the lid - does not visibly overlap the globe. It is not rigged, textured, integrated or - shipped; keep the source/rejected parts in `tmp/` until real eyelid masses pass - the gate. +- **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. +- **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/`. - **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 @@ -114,6 +122,12 @@ 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`. - 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`. diff --git a/TODO.md b/TODO.md index b72c02b..bf187fe 100644 --- a/TODO.md +++ b/TODO.md @@ -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. @@ -93,16 +95,18 @@ 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. -- [-] Rebuild a standalone MakeHuman-derived head from the researched CC0 base and save it to +- [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,641 tris / 7,690 - split verts; assembled `npc_casual` 15,532 tris / 13,310 verts / 14 bones, `inspect` clean. - Latest attempt: skin eyelid mask, recessed globes, flat iris/pupil discs, surface-anchored - brows. Vision verdict: still **REJECT** — globe depth is inconsistent between front, - three-quarter and profile, the lid does not visibly clip the globe, and the iris reads - decal-locked at three-quarter; prototype stays local and un-integrated.) + `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 before integrating the head. + 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 @@ -117,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 @@ -151,6 +154,30 @@ 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 --head mh --out `. 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. +- [ ] 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; diff --git a/docs/3d-model-review.md b/docs/3d-model-review.md index 12df208..5ac34a7 100644 --- a/docs/3d-model-review.md +++ b/docs/3d-model-review.md @@ -260,9 +260,11 @@ welding the added eye/brow pieces into the head shell and a skull-conforming hai **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 **rejected** by the vision gate: the globe depth reads inconsistently between front, -three-quarter and profile, the iris reads decal-locked at three-quarter, and the lid does -not visibly clip the globe. It stays local until real eyelid geometry passes; see +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) @@ -493,7 +495,7 @@ and the full carded-cluster foliage pass is the one remaining owner-gated P1. No ## Next actions -- Review the standalone [`makehuman-head-review.md`](makehuman-head-review.md) prototype before integrating any head/hand part into the character library. +- 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 diff --git a/docs/asset-tooling.md b/docs/asset-tooling.md index c3752d9..03b7cb0 100644 --- a/docs/asset-tooling.md +++ b/docs/asset-tooling.md @@ -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 ``` diff --git a/docs/blender_previews/README.md b/docs/blender_previews/README.md index de4238c..319a162 100644 --- a/docs/blender_previews/README.md +++ b/docs/blender_previews/README.md @@ -9,7 +9,7 @@ capture harnesses. | --- | --- | | [![sheet_npc.jpg](sheet_npc.jpg)](sheet_npc.jpg)
`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)
`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)
`sheet_makehuman_head.jpg` | Local MakeHuman head prototype: front, three-quarter and profile review views. The vision gate rejected this build (inconsistent globe depth and no visible lid overlap); see [`../makehuman-head-review.md`](../makehuman-head-review.md). | +| [![sheet_makehuman_head.jpg](sheet_makehuman_head.jpg)](sheet_makehuman_head.jpg)
`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)
`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)
`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)
`sheet_npc_baseline_compare-face.jpg` | Face-depth pass: recessed sclera, upper lid and sculpted chin/cheek planes. | diff --git a/docs/makehuman-head-review.md b/docs/makehuman-head-review.md index aa09185..2a22ce8 100644 --- a/docs/makehuman-head-review.md +++ b/docs/makehuman-head-review.md @@ -1,9 +1,12 @@ # MakeHuman head prototype -Status: **local prototype, rejected by the automated visual gate, not integrated.** -The asset is a standalone head/neck extracted from the CC0 MakeHuman base in -`tmp/npc-research/makehuman/base.obj`. It is kept as a reviewable experiment and -is not part of the shipped NPC contract. +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 @@ -14,10 +17,12 @@ is not part of the shipped NPC contract. 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 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. It is inside the 16,000/16,000 - character cap. +- 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. @@ -69,8 +74,10 @@ Accept only if: ## Judge result -**VERDICT: REJECT** (vision judge `z-ai/glm-5.3-flash`, final build render set -above). The gate is blocked; the prototype is not presented as an accepted head. +**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: @@ -96,24 +103,37 @@ Should-fix findings: - 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** -with no issues, and an assembled `npc_casual` with this head passes `inspect` at -**15,532 / 16,000 triangles, 13,310 split vertices, 14 bones, no issues**. +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. -## Next steps after the gate passes +## 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. -- Compare against the project `_face` baseline and decide whether this head is - the base for the local head/face overhaul. -- Add a clean UV/material pass and a contract armature/weights if it becomes - part of a library character. -- Keep the source script under `tmp/`; promote only the reviewed GLB and durable - docs/preview sheet. +- 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. diff --git a/docs/npc-model-sourcing.md b/docs/npc-model-sourcing.md index 77e879e..27a7bda 100644 --- a/docs/npc-model-sourcing.md +++ b/docs/npc-model-sourcing.md @@ -46,13 +46,13 @@ 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. -A standalone MakeHuman head prototype is recorded in -[`makehuman-head-review.md`](makehuman-head-review.md); it remains local/un-integrated -until the overhaul accepts it. - -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. ## Provenance rules diff --git a/docs/screens/head-review/README.md b/docs/screens/head-review/README.md index 9dd66b6..bb19ce3 100644 --- a/docs/screens/head-review/README.md +++ b/docs/screens/head-review/README.md @@ -3,8 +3,8 @@ > **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; its automated vision gate returned **REJECT** (inconsistent -> globe depth across views and no visible lid overlap). See +> 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. @@ -60,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. diff --git a/tools/blender_assets.py b/tools/blender_assets.py index 728d2de..dc65b8c 100644 --- a/tools/blender_assets.py +++ b/tools/blender_assets.py @@ -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": @@ -1005,7 +1104,15 @@ def _makehuman_head(mats, spec): target = remap.get(mat.name.split(".")[0]) if target is not None: head.data.materials[index] = target - return [head] + # 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 @@ -1245,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") @@ -1811,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: @@ -2764,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") @@ -2854,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"]])