diff --git a/characters/REGISTRY.md b/characters/REGISTRY.md index 89f7c52..fba0188 100644 --- a/characters/REGISTRY.md +++ b/characters/REGISTRY.md @@ -23,7 +23,7 @@ characters/ lena_base_v01/ SHIPPED 2026-08-10 <- frozen lena_base_v02/ the next one to ship male/ - mako_base_v01/ (none yet — mako has never shipped) + mako_base_v01/ SHIPPED 2026-08-12 <- frozen work/ lena/ the live lane: recipes + masters. Grammar-exempt. mako/ @@ -136,7 +136,7 @@ names without touching the immutable delivered files. | character | gender | parent original | ships so far | |---|---|---|---| | `lena` | female | `female_lena_tripo` | `lena_base_v01` | -| `mako` | male | `male_base_bald_tripo_v1` | none | +| `mako` | male | `male_base_bald_tripo_v1` | `mako_base_v01` | Spelling history for the male: game wiki "Mako", AccuRig-era typo "moka", also "moko". Canonical filename token is **`mako`**; the others are never used in new files. @@ -150,6 +150,7 @@ Status vocabulary: `shipped / superseded / rolled-back`. | folder | character | date | status | ship name in ariki-game | sha256 (repo-side body) | |---|---|---|---|---|---| | `female/lena_base_v01` | `lena` | 2026-08-10 | shipped | `Ariki_Female_QuatSkin_Nude.glb`, and swapped into `Ariki_Female_QuatSkin.glb` | `0d602accef0992a6a0dfffa5b73b1f9b702400df5258ff7517f786f11050f4d0` | +| `male/mako_base_v01` | `mako` | 2026-08-12 | shipped | `Ariki_Male_Mako.glb` (male Ariki default, lineage `MALE-MAKO-FULLHEAD-V1`) | `220714a2fb6283188be3834214877204533fde78de9a6ffa03ed85e1167735d3` | ### `lena_base_v01` — what it is @@ -178,3 +179,21 @@ Ancestry, condensed from the 20 intermediate stages that produced her (all prune mannequin with no muscle definition. The shipped body has athletic definition (`42_musculature.py` strength 1.0, `SPINE=0.34`). Flagged here so it is a known decision rather than a drift. + +### `mako_base_v01` — what it is + +The Tripo male sculpt with the body decimated and the **head untouched** (158,575 tris, +full scan resolution, cut at the neck at 86.5% of height): body 24k / hands 40k. First +mako-geometry body ever in the game — it replaced the July graft body (Superhero_Male + +Lena's head, `MALE-MAKO-D-V1`, kept on disk as comparison) as the male Ariki default. + +| step | what happened | +|---|---| +| decimate | body/hands only (`work/mako/01_decimate_fullhead.py`); head gated bit-identical → 222,575 tris | +| rig | AccuRig 1.10, manual, on a 1.700 m materials-free carrier → 101 CC_Base_* bones | +| graft | index-exact (122,062 verts both sides), mesh moved 0 mm (`work/mako/02_graft.py`) | +| quatskin | `ariki-game/tools/make_mako_fullhead_quatskin_accurig.py` `HEAD_SAFE=1` → 65 joints, 1.818 m, rigid mitts, body-only weld + body-only split-normals clear | + +**Known decisions:** height 1.818 m is provisional (IB-lane taller-than-Lena convention, +not eye-locked); ship name avoids the `QuatSkin`/`QuatBody` tokens because +`PlayerController` string-gates on them; no male LOD1 yet. Details in the folder README. diff --git a/characters/male/mako_base_v01/README.md b/characters/male/mako_base_v01/README.md new file mode 100644 index 0000000..aa12170 --- /dev/null +++ b/characters/male/mako_base_v01/README.md @@ -0,0 +1,43 @@ +# mako_base_v01 — first shipped mako + +**Shipped 2026-08-12** as `Ariki_Male_Mako.glb` → wired as the male Ariki player default +in ariki-game (`CharacterBuildData.GetBodyPath` male branch, lineage +`MALE-MAKO-FULLHEAD-V1`). The first mako ship ever: until this folder, the in-game +"Mako" was the July graft (Quaternius Superhero_Male body + Lena's head, `MALE-MAKO-D-V1`), +kept on disk as the comparison body. + +## What it is + +Özlem's Tripo male sculpt (`originals/male/male_base_bald_tripo_v1`, ~1.9M tris) with the +**body decimated and the head untouched**: body 24k tris, hands 40k, head 158,575 tris at +full scan resolution — cut at the neck (86.5% of bbox height). Face features (brows, lash +lines, lips) are raised geometry on the scan, which is why the head rides along whole. + +| stage | result | +|---|---| +| decimate (body/hands only) | 222,575 tris, head bit-identical (`work/mako/01_decimate_fullhead.py`) | +| carrier | 1.700 m, materials stripped (Lena stage-45 recipe) | +| AccuRig | 1.10, manual session by Jeremy 2026-08-12 — **`rig/mako_base_accurig_fbx_v01/` is irreplaceable** | +| graft | index-exact, 101 CC_Base_* bones; mesh moved 0 mm; rest drift 0.0003 mm (`work/mako/02_graft.py`) | +| quatskin | `ariki-game/tools/make_mako_fullhead_quatskin_accurig.py`, `HEAD_SAFE=1` → 65 joints, 1.818 m, rigid-mitt fingers, body-only weld + body-only split-normals clear (head shading preserved) | + +## Files + +- `mako_base_accurig_glb_v01.glb` — the repo-side body (101 CC bones), input to the game converter +- `rig/mako_base_accurig_fbx_v01/` — manual AccuRig output; no script regenerates it +- `rig/mako_base_tpose_fbx_v01.fbx` — the carrier fed into AccuRig; regenerable via the work-lane recipes +- `preview/` — converter T-pose renders, full-res head closeup, hand closeup, in-game loco bed shot + +## Known decisions / departures + +- **Height 1.818 m is provisional** — pelvis-anchored then stretched, following the IB-lane + "male reads taller than Lena (1.777)" convention. Not eye-locked by Ozan/Can. +- **Neck cut is hard** (86.5% line, head in every protect group). Lena's v02 candidate uses a + feathered ramp (`work/lena/48_decimate_headsafe.py`) — nicer transition; adopt if v02 ships. +- The ship name deliberately contains neither `QuatSkin` nor `QuatBody`: + `PlayerController` gates Lena's eyes/brows/hair offsets on `Contains("QuatSkin")` and the + Mako-D runtime warp on `Contains("QuatBody")`. +- AccuRig moved some carrier verts (similarity-fit residual p99 6.5 mm) — weights copied by + index regardless and every rest-pose gate passed; don't treat the carrier FBX as + geometrically pristine. +- No male LOD1 yet — `CharacterBodyLodCatalog` still returns null for males. diff --git a/characters/male/mako_base_v01/mako_base_accurig_glb_v01.glb b/characters/male/mako_base_v01/mako_base_accurig_glb_v01.glb new file mode 100644 index 0000000..11d7090 --- /dev/null +++ b/characters/male/mako_base_v01/mako_base_accurig_glb_v01.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:220714a2fb6283188be3834214877204533fde78de9a6ffa03ed85e1167735d3 +size 10404036 diff --git a/characters/male/mako_base_v01/preview/full_front.png b/characters/male/mako_base_v01/preview/full_front.png new file mode 100644 index 0000000..5696a30 Binary files /dev/null and b/characters/male/mako_base_v01/preview/full_front.png differ diff --git a/characters/male/mako_base_v01/preview/game_loco.png b/characters/male/mako_base_v01/preview/game_loco.png new file mode 100644 index 0000000..e306b2b Binary files /dev/null and b/characters/male/mako_base_v01/preview/game_loco.png differ diff --git a/characters/male/mako_base_v01/preview/hand.png b/characters/male/mako_base_v01/preview/hand.png new file mode 100644 index 0000000..bc785c2 Binary files /dev/null and b/characters/male/mako_base_v01/preview/hand.png differ diff --git a/characters/male/mako_base_v01/preview/head.png b/characters/male/mako_base_v01/preview/head.png new file mode 100644 index 0000000..7ef4ba9 Binary files /dev/null and b/characters/male/mako_base_v01/preview/head.png differ diff --git a/characters/male/mako_base_v01/preview/q34.png b/characters/male/mako_base_v01/preview/q34.png new file mode 100644 index 0000000..eada3d9 Binary files /dev/null and b/characters/male/mako_base_v01/preview/q34.png differ diff --git a/characters/male/mako_base_v01/rig/mako_base_accurig_fbx_v01/mako_base_accurig_fbx_v01.fbx b/characters/male/mako_base_v01/rig/mako_base_accurig_fbx_v01/mako_base_accurig_fbx_v01.fbx new file mode 100644 index 0000000..e34943c --- /dev/null +++ b/characters/male/mako_base_v01/rig/mako_base_accurig_fbx_v01/mako_base_accurig_fbx_v01.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ab615fba00cc98fa99c7772ac1a2ddb7ac510e08932abbd04cafe9b493c75c2 +size 8913040 diff --git a/characters/male/mako_base_v01/rig/mako_base_accurig_fbx_v01/mako_base_accurig_fbx_v01.json b/characters/male/mako_base_v01/rig/mako_base_accurig_fbx_v01/mako_base_accurig_fbx_v01.json new file mode 100644 index 0000000..920f748 --- /dev/null +++ b/characters/male/mako_base_v01/rig/mako_base_accurig_fbx_v01/mako_base_accurig_fbx_v01.json @@ -0,0 +1,429 @@ +{ + "mako_fullhead_rigged": { + "Version": "1.10.1822.1", + "Scene": { + "Name": true, + "SupportShaderSelect": true + }, + "Export Directory": "C:/Users/Jeremy/tinqs/animation/characters/rig-work", + "Object": { + "mako_fullhead_rigged": { + "Generation": "AccuRig", + "Physics": { + "Collision Shapes": { + "RL_BoneRoot": {}, + "CC_Base_Hip": {}, + "CC_Base_Pelvis": {}, + "CC_Base_L_Thigh": {}, + "CC_Base_L_Calf": {}, + "CC_Base_L_Foot": {}, + "CC_Base_L_ToeBaseShareBone": {}, + "CC_Base_L_ToeBase": {}, + "CC_Base_L_PinkyToe1": {}, + "CC_Base_L_RingToe1": {}, + "CC_Base_L_MidToe1": {}, + "CC_Base_L_IndexToe1": {}, + "CC_Base_L_BigToe1": {}, + "CC_Base_L_CalfTwist01": {}, + "CC_Base_L_CalfTwist02": {}, + "CC_Base_L_KneeShareBone": {}, + "CC_Base_L_ThighTwist01": {}, + "CC_Base_L_ThighTwist02": {}, + "CC_Base_R_Thigh": {}, + "CC_Base_R_ThighTwist01": {}, + "CC_Base_R_ThighTwist02": {}, + "CC_Base_R_Calf": {}, + "CC_Base_R_Foot": {}, + "CC_Base_R_ToeBase": {}, + "CC_Base_R_PinkyToe1": {}, + "CC_Base_R_BigToe1": {}, + "CC_Base_R_IndexToe1": {}, + "CC_Base_R_MidToe1": {}, + "CC_Base_R_RingToe1": {}, + "CC_Base_R_ToeBaseShareBone": {}, + "CC_Base_R_KneeShareBone": {}, + "CC_Base_R_CalfTwist01": {}, + "CC_Base_R_CalfTwist02": {}, + "CC_Base_Waist": {}, + "CC_Base_Spine01": {}, + "CC_Base_Spine02": {}, + "CC_Base_NeckTwist01": {}, + "CC_Base_NeckTwist02": {}, + "CC_Base_Head": {}, + "CC_Base_FacialBone": {}, + "CC_Base_JawRoot": {}, + "CC_Base_Tongue01": {}, + "CC_Base_Tongue02": {}, + "CC_Base_Tongue03": {}, + "CC_Base_Teeth02": {}, + "CC_Base_R_Eye": {}, + "CC_Base_L_Eye": {}, + "CC_Base_UpperJaw": {}, + "CC_Base_Teeth01": {}, + "CC_Base_L_Clavicle": {}, + "CC_Base_L_Upperarm": {}, + "CC_Base_L_Forearm": {}, + "CC_Base_L_ForearmTwist01": {}, + "CC_Base_L_ForearmTwist02": {}, + "CC_Base_L_ElbowShareBone": {}, + "CC_Base_L_Hand": {}, + "CC_Base_L_Mid1": {}, + "CC_Base_L_Mid2": {}, + "CC_Base_L_Mid3": {}, + "CC_Base_L_Index1": {}, + "CC_Base_L_Index2": {}, + "CC_Base_L_Index3": {}, + "CC_Base_L_Ring1": {}, + "CC_Base_L_Ring2": {}, + "CC_Base_L_Ring3": {}, + "CC_Base_L_Pinky1": {}, + "CC_Base_L_Pinky2": {}, + "CC_Base_L_Pinky3": {}, + "CC_Base_L_Thumb1": {}, + "CC_Base_L_Thumb2": {}, + "CC_Base_L_Thumb3": {}, + "CC_Base_L_UpperarmTwist01": {}, + "CC_Base_L_UpperarmTwist02": {}, + "CC_Base_R_Clavicle": {}, + "CC_Base_R_Upperarm": {}, + "CC_Base_R_UpperarmTwist01": {}, + "CC_Base_R_UpperarmTwist02": {}, + "CC_Base_R_Forearm": {}, + "CC_Base_R_ForearmTwist01": {}, + "CC_Base_R_ForearmTwist02": {}, + "CC_Base_R_ElbowShareBone": {}, + "CC_Base_R_Hand": {}, + "CC_Base_R_Mid1": {}, + "CC_Base_R_Mid2": {}, + "CC_Base_R_Mid3": {}, + "CC_Base_R_Ring1": {}, + "CC_Base_R_Ring2": {}, + "CC_Base_R_Ring3": {}, + "CC_Base_R_Thumb1": {}, + "CC_Base_R_Thumb2": {}, + "CC_Base_R_Thumb3": {}, + "CC_Base_R_Index1": {}, + "CC_Base_R_Index2": {}, + "CC_Base_R_Index3": {}, + "CC_Base_R_Pinky1": {}, + "CC_Base_R_Pinky2": {}, + "CC_Base_R_Pinky3": {}, + "CC_Base_R_RibsTwist": {}, + "CC_Base_R_Breast": {}, + "CC_Base_L_RibsTwist": {}, + "CC_Base_L_Breast": {}, + "meshes_0_": {} + } + }, + "ExpressionSet": "4", + "Expression": { + "V_None": { + "Bones": {} + }, + "V_Open": { + "Bones": {} + }, + "V_Explosive": { + "Bones": {} + }, + "V_Dental_Lip": { + "Bones": {} + }, + "V_Tight_O": { + "Bones": {} + }, + "V_Tight": { + "Bones": {} + }, + "V_Wide": { + "Bones": {} + }, + "V_Affricate": { + "Bones": {} + }, + "V_Lip_Open": { + "Bones": {} + }, + "V_Tongue_up": { + "Bones": {} + }, + "V_Tongue_Raise": { + "Bones": {} + }, + "V_Tongue_Out": { + "Bones": {} + }, + "V_Tongue_Narrow": { + "Bones": {} + }, + "V_Tongue_Lower": { + "Bones": {} + }, + "V_Tongue_Curl_U": { + "Bones": {} + }, + "V_Tongue_Curl_D": { + "Bones": {} + }, + "Brow_Raise_Inner_L": { + "Bones": {} + }, + "Brow_Raise_Inner_R": { + "Bones": {} + }, + "Brow_Raise_Outer_L": { + "Bones": {} + }, + "Brow_Raise_Outer_R": { + "Bones": {} + }, + "Brow_Drop_L": { + "Bones": {} + }, + "Brow_Drop_R": { + "Bones": {} + }, + "Nose_Sneer_L": { + "Bones": {} + }, + "Nose_Sneer_R": { + "Bones": {} + }, + "Cheek_Raise_L": { + "Bones": {} + }, + "Cheek_Raise_R": { + "Bones": {} + }, + "Cheek_Puff_L": { + "Bones": {} + }, + "Cheek_Puff_R": { + "Bones": {} + }, + "Head_Turn_Up": { + "Bones": {} + }, + "Head_Turn_Down": { + "Bones": {} + }, + "Head_Turn_L": { + "Bones": {} + }, + "Head_Turn_R": { + "Bones": {} + }, + "Head_Tilt_L": { + "Bones": {} + }, + "Head_Tilt_R": { + "Bones": {} + }, + "Head_L": { + "Bones": {} + }, + "Head_R": { + "Bones": {} + }, + "Head_Forward": { + "Bones": {} + }, + "Head_Backward": { + "Bones": {} + }, + "Jaw_Open": { + "Bones": {} + }, + "Jaw_Forward": { + "Bones": {} + }, + "Jaw_L": { + "Bones": {} + }, + "Jaw_R": { + "Bones": {} + }, + "Tongue_Out": { + "Bones": {} + }, + "Tongue_Up": { + "Bones": {} + }, + "Tongue_Down": { + "Bones": {} + }, + "Tongue_Tip_Up": { + "Bones": {} + }, + "Tongue_Tip_Down": { + "Bones": {} + }, + "Tongue_Narrow": { + "Bones": {} + }, + "Tongue_Wide": { + "Bones": {} + }, + "Tongue_Roll": { + "Bones": {} + }, + "Tongue_L": { + "Bones": {} + }, + "Tongue_R": { + "Bones": {} + }, + "Tongue_Bulge_L": { + "Bones": {} + }, + "Tongue_Bulge_R": { + "Bones": {} + }, + "Eye_Blink_L": { + "Bones": {} + }, + "Eye_Blink_R": { + "Bones": {} + }, + "Eye_Squint_L": { + "Bones": {} + }, + "Eye_Squint_R": { + "Bones": {} + }, + "Eye_Wide_L": { + "Bones": {} + }, + "Eye_Wide_R": { + "Bones": {} + }, + "Eye_L_Look_L": { + "Bones": {} + }, + "Eye_R_Look_L": { + "Bones": {} + }, + "Eye_L_Look_R": { + "Bones": {} + }, + "Eye_R_Look_R": { + "Bones": {} + }, + "Eye_L_Look_Up": { + "Bones": {} + }, + "Eye_R_Look_Up": { + "Bones": {} + }, + "Eye_L_Look_Down": { + "Bones": {} + }, + "Eye_R_Look_Down": { + "Bones": {} + }, + "Mouth_Smile_L": { + "Bones": {} + }, + "Mouth_Smile_R": { + "Bones": {} + }, + "Mouth_Frown_L": { + "Bones": {} + }, + "Mouth_Frown_R": { + "Bones": {} + }, + "Mouth_Stretch_L": { + "Bones": {} + }, + "Mouth_Stretch_R": { + "Bones": {} + }, + "Mouth_Dimple_L": { + "Bones": {} + }, + "Mouth_Dimple_R": { + "Bones": {} + }, + "Mouth_Press_L": { + "Bones": {} + }, + "Mouth_Press_R": { + "Bones": {} + }, + "Mouth_Pucker": { + "Bones": {} + }, + "Mouth_Funnel": { + "Bones": {} + }, + "Mouth_Roll_In_Upper": { + "Bones": {} + }, + "Mouth_Roll_In_Lower": { + "Bones": {} + }, + "Mouth_L": { + "Bones": {} + }, + "Mouth_R": { + "Bones": {} + }, + "Mouth_Shrug_Upper": { + "Bones": {} + }, + "Mouth_Shrug_Lower": { + "Bones": {} + }, + "Mouth_Up_Upper_L": { + "Bones": {} + }, + "Mouth_Up_Upper_R": { + "Bones": {} + }, + "Mouth_Down_Lower_L": { + "Bones": {} + }, + "Mouth_Down_Lower_R": { + "Bones": {} + }, + "Mouth_Close": { + "Bones": {} + } + }, + "Constraint": {}, + "Meshes": { + "meshes_0_": { + "SubD Level": 0, + "Materials": { + "Default": { + "Material Type": "Pbr", + "MultiUV Index": 0, + "Two Side": true, + "Diffuse Color": [ + 255.0, + 255.0, + 255.0 + ], + "Ambient Color": [ + 255.0, + 255.0, + 255.0 + ], + "Specular Color": [ + 255.0, + 255.0, + 255.0 + ], + "Opacity": 1.0, + "Self Illumination": 0.0, + "Textures": {}, + "Resource Textures": {} + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/characters/male/mako_base_v01/rig/mako_base_tpose_fbx_v01.fbx b/characters/male/mako_base_v01/rig/mako_base_tpose_fbx_v01.fbx new file mode 100644 index 0000000..2af8571 --- /dev/null +++ b/characters/male/mako_base_v01/rig/mako_base_tpose_fbx_v01.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76a3fd2c3fb77aa81d629a434ece1f61c00a1c97ca95988e65e6dfd68a4648c6 +size 6633420 diff --git a/characters/rig-work/mako_fullhead_rigbait.fbx b/characters/rig-work/mako_fullhead_rigbait.fbx new file mode 100644 index 0000000..2af8571 --- /dev/null +++ b/characters/rig-work/mako_fullhead_rigbait.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76a3fd2c3fb77aa81d629a434ece1f61c00a1c97ca95988e65e6dfd68a4648c6 +size 6633420 diff --git a/characters/rig-work/mako_fullhead_rigged.fbx b/characters/rig-work/mako_fullhead_rigged.fbx new file mode 100644 index 0000000..e34943c --- /dev/null +++ b/characters/rig-work/mako_fullhead_rigged.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ab615fba00cc98fa99c7772ac1a2ddb7ac510e08932abbd04cafe9b493c75c2 +size 8913040 diff --git a/characters/rig-work/mako_fullhead_rigged.json b/characters/rig-work/mako_fullhead_rigged.json new file mode 100644 index 0000000..920f748 --- /dev/null +++ b/characters/rig-work/mako_fullhead_rigged.json @@ -0,0 +1,429 @@ +{ + "mako_fullhead_rigged": { + "Version": "1.10.1822.1", + "Scene": { + "Name": true, + "SupportShaderSelect": true + }, + "Export Directory": "C:/Users/Jeremy/tinqs/animation/characters/rig-work", + "Object": { + "mako_fullhead_rigged": { + "Generation": "AccuRig", + "Physics": { + "Collision Shapes": { + "RL_BoneRoot": {}, + "CC_Base_Hip": {}, + "CC_Base_Pelvis": {}, + "CC_Base_L_Thigh": {}, + "CC_Base_L_Calf": {}, + "CC_Base_L_Foot": {}, + "CC_Base_L_ToeBaseShareBone": {}, + "CC_Base_L_ToeBase": {}, + "CC_Base_L_PinkyToe1": {}, + "CC_Base_L_RingToe1": {}, + "CC_Base_L_MidToe1": {}, + "CC_Base_L_IndexToe1": {}, + "CC_Base_L_BigToe1": {}, + "CC_Base_L_CalfTwist01": {}, + "CC_Base_L_CalfTwist02": {}, + "CC_Base_L_KneeShareBone": {}, + "CC_Base_L_ThighTwist01": {}, + "CC_Base_L_ThighTwist02": {}, + "CC_Base_R_Thigh": {}, + "CC_Base_R_ThighTwist01": {}, + "CC_Base_R_ThighTwist02": {}, + "CC_Base_R_Calf": {}, + "CC_Base_R_Foot": {}, + "CC_Base_R_ToeBase": {}, + "CC_Base_R_PinkyToe1": {}, + "CC_Base_R_BigToe1": {}, + "CC_Base_R_IndexToe1": {}, + "CC_Base_R_MidToe1": {}, + "CC_Base_R_RingToe1": {}, + "CC_Base_R_ToeBaseShareBone": {}, + "CC_Base_R_KneeShareBone": {}, + "CC_Base_R_CalfTwist01": {}, + "CC_Base_R_CalfTwist02": {}, + "CC_Base_Waist": {}, + "CC_Base_Spine01": {}, + "CC_Base_Spine02": {}, + "CC_Base_NeckTwist01": {}, + "CC_Base_NeckTwist02": {}, + "CC_Base_Head": {}, + "CC_Base_FacialBone": {}, + "CC_Base_JawRoot": {}, + "CC_Base_Tongue01": {}, + "CC_Base_Tongue02": {}, + "CC_Base_Tongue03": {}, + "CC_Base_Teeth02": {}, + "CC_Base_R_Eye": {}, + "CC_Base_L_Eye": {}, + "CC_Base_UpperJaw": {}, + "CC_Base_Teeth01": {}, + "CC_Base_L_Clavicle": {}, + "CC_Base_L_Upperarm": {}, + "CC_Base_L_Forearm": {}, + "CC_Base_L_ForearmTwist01": {}, + "CC_Base_L_ForearmTwist02": {}, + "CC_Base_L_ElbowShareBone": {}, + "CC_Base_L_Hand": {}, + "CC_Base_L_Mid1": {}, + "CC_Base_L_Mid2": {}, + "CC_Base_L_Mid3": {}, + "CC_Base_L_Index1": {}, + "CC_Base_L_Index2": {}, + "CC_Base_L_Index3": {}, + "CC_Base_L_Ring1": {}, + "CC_Base_L_Ring2": {}, + "CC_Base_L_Ring3": {}, + "CC_Base_L_Pinky1": {}, + "CC_Base_L_Pinky2": {}, + "CC_Base_L_Pinky3": {}, + "CC_Base_L_Thumb1": {}, + "CC_Base_L_Thumb2": {}, + "CC_Base_L_Thumb3": {}, + "CC_Base_L_UpperarmTwist01": {}, + "CC_Base_L_UpperarmTwist02": {}, + "CC_Base_R_Clavicle": {}, + "CC_Base_R_Upperarm": {}, + "CC_Base_R_UpperarmTwist01": {}, + "CC_Base_R_UpperarmTwist02": {}, + "CC_Base_R_Forearm": {}, + "CC_Base_R_ForearmTwist01": {}, + "CC_Base_R_ForearmTwist02": {}, + "CC_Base_R_ElbowShareBone": {}, + "CC_Base_R_Hand": {}, + "CC_Base_R_Mid1": {}, + "CC_Base_R_Mid2": {}, + "CC_Base_R_Mid3": {}, + "CC_Base_R_Ring1": {}, + "CC_Base_R_Ring2": {}, + "CC_Base_R_Ring3": {}, + "CC_Base_R_Thumb1": {}, + "CC_Base_R_Thumb2": {}, + "CC_Base_R_Thumb3": {}, + "CC_Base_R_Index1": {}, + "CC_Base_R_Index2": {}, + "CC_Base_R_Index3": {}, + "CC_Base_R_Pinky1": {}, + "CC_Base_R_Pinky2": {}, + "CC_Base_R_Pinky3": {}, + "CC_Base_R_RibsTwist": {}, + "CC_Base_R_Breast": {}, + "CC_Base_L_RibsTwist": {}, + "CC_Base_L_Breast": {}, + "meshes_0_": {} + } + }, + "ExpressionSet": "4", + "Expression": { + "V_None": { + "Bones": {} + }, + "V_Open": { + "Bones": {} + }, + "V_Explosive": { + "Bones": {} + }, + "V_Dental_Lip": { + "Bones": {} + }, + "V_Tight_O": { + "Bones": {} + }, + "V_Tight": { + "Bones": {} + }, + "V_Wide": { + "Bones": {} + }, + "V_Affricate": { + "Bones": {} + }, + "V_Lip_Open": { + "Bones": {} + }, + "V_Tongue_up": { + "Bones": {} + }, + "V_Tongue_Raise": { + "Bones": {} + }, + "V_Tongue_Out": { + "Bones": {} + }, + "V_Tongue_Narrow": { + "Bones": {} + }, + "V_Tongue_Lower": { + "Bones": {} + }, + "V_Tongue_Curl_U": { + "Bones": {} + }, + "V_Tongue_Curl_D": { + "Bones": {} + }, + "Brow_Raise_Inner_L": { + "Bones": {} + }, + "Brow_Raise_Inner_R": { + "Bones": {} + }, + "Brow_Raise_Outer_L": { + "Bones": {} + }, + "Brow_Raise_Outer_R": { + "Bones": {} + }, + "Brow_Drop_L": { + "Bones": {} + }, + "Brow_Drop_R": { + "Bones": {} + }, + "Nose_Sneer_L": { + "Bones": {} + }, + "Nose_Sneer_R": { + "Bones": {} + }, + "Cheek_Raise_L": { + "Bones": {} + }, + "Cheek_Raise_R": { + "Bones": {} + }, + "Cheek_Puff_L": { + "Bones": {} + }, + "Cheek_Puff_R": { + "Bones": {} + }, + "Head_Turn_Up": { + "Bones": {} + }, + "Head_Turn_Down": { + "Bones": {} + }, + "Head_Turn_L": { + "Bones": {} + }, + "Head_Turn_R": { + "Bones": {} + }, + "Head_Tilt_L": { + "Bones": {} + }, + "Head_Tilt_R": { + "Bones": {} + }, + "Head_L": { + "Bones": {} + }, + "Head_R": { + "Bones": {} + }, + "Head_Forward": { + "Bones": {} + }, + "Head_Backward": { + "Bones": {} + }, + "Jaw_Open": { + "Bones": {} + }, + "Jaw_Forward": { + "Bones": {} + }, + "Jaw_L": { + "Bones": {} + }, + "Jaw_R": { + "Bones": {} + }, + "Tongue_Out": { + "Bones": {} + }, + "Tongue_Up": { + "Bones": {} + }, + "Tongue_Down": { + "Bones": {} + }, + "Tongue_Tip_Up": { + "Bones": {} + }, + "Tongue_Tip_Down": { + "Bones": {} + }, + "Tongue_Narrow": { + "Bones": {} + }, + "Tongue_Wide": { + "Bones": {} + }, + "Tongue_Roll": { + "Bones": {} + }, + "Tongue_L": { + "Bones": {} + }, + "Tongue_R": { + "Bones": {} + }, + "Tongue_Bulge_L": { + "Bones": {} + }, + "Tongue_Bulge_R": { + "Bones": {} + }, + "Eye_Blink_L": { + "Bones": {} + }, + "Eye_Blink_R": { + "Bones": {} + }, + "Eye_Squint_L": { + "Bones": {} + }, + "Eye_Squint_R": { + "Bones": {} + }, + "Eye_Wide_L": { + "Bones": {} + }, + "Eye_Wide_R": { + "Bones": {} + }, + "Eye_L_Look_L": { + "Bones": {} + }, + "Eye_R_Look_L": { + "Bones": {} + }, + "Eye_L_Look_R": { + "Bones": {} + }, + "Eye_R_Look_R": { + "Bones": {} + }, + "Eye_L_Look_Up": { + "Bones": {} + }, + "Eye_R_Look_Up": { + "Bones": {} + }, + "Eye_L_Look_Down": { + "Bones": {} + }, + "Eye_R_Look_Down": { + "Bones": {} + }, + "Mouth_Smile_L": { + "Bones": {} + }, + "Mouth_Smile_R": { + "Bones": {} + }, + "Mouth_Frown_L": { + "Bones": {} + }, + "Mouth_Frown_R": { + "Bones": {} + }, + "Mouth_Stretch_L": { + "Bones": {} + }, + "Mouth_Stretch_R": { + "Bones": {} + }, + "Mouth_Dimple_L": { + "Bones": {} + }, + "Mouth_Dimple_R": { + "Bones": {} + }, + "Mouth_Press_L": { + "Bones": {} + }, + "Mouth_Press_R": { + "Bones": {} + }, + "Mouth_Pucker": { + "Bones": {} + }, + "Mouth_Funnel": { + "Bones": {} + }, + "Mouth_Roll_In_Upper": { + "Bones": {} + }, + "Mouth_Roll_In_Lower": { + "Bones": {} + }, + "Mouth_L": { + "Bones": {} + }, + "Mouth_R": { + "Bones": {} + }, + "Mouth_Shrug_Upper": { + "Bones": {} + }, + "Mouth_Shrug_Lower": { + "Bones": {} + }, + "Mouth_Up_Upper_L": { + "Bones": {} + }, + "Mouth_Up_Upper_R": { + "Bones": {} + }, + "Mouth_Down_Lower_L": { + "Bones": {} + }, + "Mouth_Down_Lower_R": { + "Bones": {} + }, + "Mouth_Close": { + "Bones": {} + } + }, + "Constraint": {}, + "Meshes": { + "meshes_0_": { + "SubD Level": 0, + "Materials": { + "Default": { + "Material Type": "Pbr", + "MultiUV Index": 0, + "Two Side": true, + "Diffuse Color": [ + 255.0, + 255.0, + 255.0 + ], + "Ambient Color": [ + 255.0, + 255.0, + 255.0 + ], + "Specular Color": [ + 255.0, + 255.0, + 255.0 + ], + "Opacity": 1.0, + "Self Illumination": 0.0, + "Textures": {}, + "Resource Textures": {} + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/characters/work/mako/.lanekeep b/characters/work/mako/.lanekeep new file mode 100644 index 0000000..1f96edc --- /dev/null +++ b/characters/work/mako/.lanekeep @@ -0,0 +1,2 @@ +# Masters for the mako fullhead lane (ship: male/mako_base_v01) +mako_fullhead_rigged.blend # 02_graft output — the rigged master the ship GLB came from diff --git a/characters/work/mako/01_decimate_fullhead.py b/characters/work/mako/01_decimate_fullhead.py new file mode 100644 index 0000000..225ce1c --- /dev/null +++ b/characters/work/mako/01_decimate_fullhead.py @@ -0,0 +1,114 @@ +# Mako work lane, stage 01: decimate the Tripo original with the HEAD FULLY PROTECTED. +# Body -> 24k tris, hands -> 40k tris, head above the neck line -> UNTOUCHED (full +# Tripo resolution). Jeremy's ask 2026-08-11: a decimated mako that cuts at the neck +# like the Lena head graft did, so the face keeps every tri for later head work. +# +# Differences from tools/rigbait_decimate.py (same region geometry, different intent): +# * NO P_HEAD pass — head verts sit in every protect group instead. +# * Output is a GLB master (packed textures carried), not an FBX rig bait. +# Same as the bait: NO scale / recenter (native ~0.98 m, feet at Z=0); region cuts are +# bbox-relative — neck at 86.5% of height, hands beyond 75.6% of half-span. +# +# blender --background --python 01_decimate_fullhead.py -- [body hand] +import bpy, os, sys, math +from mathutils import Vector + +args = sys.argv[sys.argv.index("--") + 1:] +SRC, OUT_GLB, REVIEW = args[0], args[1], args[2] +T_BODY, T_HAND = (int(a) for a in args[3:5]) if len(args) >= 5 else (24000, 40000) +os.makedirs(os.path.dirname(os.path.abspath(OUT_GLB)), exist_ok=True) +os.makedirs(REVIEW, exist_ok=True) +bpy.context.preferences.filepaths.save_version = 0 + +bpy.ops.wm.read_factory_settings(use_empty=True) +bpy.ops.import_scene.gltf(filepath=SRC) +body = max((o for o in bpy.data.objects if o.type == "MESH"), key=lambda o: len(o.data.vertices)) +me = body.data +bpy.ops.object.select_all(action="DESELECT") +body.select_set(True) +bpy.context.view_layer.objects.active = body +bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + +bb = [body.matrix_world @ Vector(c) for c in body.bound_box] +mnz, mxz = min(v.z for v in bb), max(v.z for v in bb) +half_span = max(abs(v.x) for v in bb) +NECK_Z = mnz + 0.865 * (mxz - mnz) +HAND_X = 0.756 * half_span +print(f"[fullhead] height={mxz-mnz:.3f} half_span={half_span:.3f} neck_z={NECK_Z:.3f} hand_x={HAND_X:.3f}") + +# anti-facet shading fixes (shading only, geometry untouched) +try: bpy.ops.mesh.customdata_custom_splitnormals_clear() +except Exception: pass +if "sharp_edge" in me.attributes: + me.attributes.remove(me.attributes["sharp_edge"]) + +def is_head(v): return v.co.z > NECK_Z +def is_hand(v): return abs(v.co.x) > HAND_X + +def group(name, pred): + g = body.vertex_groups.new(name=name) + g.add([v.index for v in me.vertices if pred(v)], 1.0, "REPLACE") + return g + +head_tris0 = sum(1 for p in me.polygons if all(is_head(me.vertices[vi]) for vi in p.vertices)) +hand_tris0 = sum(1 for p in me.polygons if all(is_hand(me.vertices[vi]) for vi in p.vertices)) +print(f"[fullhead] regions: head_tris={head_tris0} hand_tris={hand_tris0} total={len(me.polygons)}") +if head_tris0 < 20000 or hand_tris0 < 20000: + raise SystemExit(f"[fullhead] region cut looks wrong (head={head_tris0}, hands={hand_tris0})") + +# Two passes only. The head is inside every protect group, so no pass ever touches it. +group("P_BODY", lambda v: is_head(v) or is_hand(v)) # pass decimates the body +group("P_HAND", lambda v: not is_hand(v)) # pass decimates the hands (head is not a hand) + +targets = {"P_BODY": T_BODY, "P_HAND": T_HAND} +region_now = {"P_BODY": len(me.polygons) - head_tris0 - hand_tris0, "P_HAND": hand_tris0} +for pg in ("P_BODY", "P_HAND"): + other_now = len(me.polygons) - region_now[pg] + dec = body.modifiers.new("dec", "DECIMATE") + dec.ratio = min(1.0, (targets[pg] + other_now) / max(1, len(me.polygons))) + dec.vertex_group = pg + dec.invert_vertex_group = True + dec.delimit = {"UV"} + bpy.ops.object.modifier_apply(modifier="dec") + me = body.data + region_now[pg] = targets[pg] + print(f"[fullhead] after {pg} pass: tris={len(me.polygons)}") + +# gate: the head must have survived bit-for-tri +head_tris1 = sum(1 for p in me.polygons if all(is_head(me.vertices[vi]) for vi in p.vertices)) +print(f"[fullhead] head tris {head_tris0} -> {head_tris1}") +if head_tris1 < head_tris0 * 0.999: + raise SystemExit(f"[fullhead] HEAD WAS DECIMATED ({head_tris0} -> {head_tris1}) — abort, do not use output") + +bpy.ops.object.shade_smooth() + +def render(name, loc, look_z): + cam = bpy.data.objects.get("cam") + if cam is None: + camd = bpy.data.cameras.new("cam"); cam = bpy.data.objects.new("cam", camd) + bpy.context.scene.collection.objects.link(cam) + bpy.context.scene.camera = cam + cam.location = loc + cam.rotation_euler = (math.radians(90), 0, 0) + cam.location.z = look_z + sc = bpy.context.scene + sc.render.engine = "BLENDER_WORKBENCH" + sc.display.shading.light = "STUDIO" + sc.display.shading.color_type = "TEXTURE" + sc.render.resolution_x = 800; sc.render.resolution_y = 800 + sc.render.filepath = os.path.join(REVIEW, name) + bpy.ops.render.render(write_still=True) + +h = mxz - mnz +render("qa_front.png", Vector((0, -2.0 * h, 0)), mnz + 0.5 * h) +render("qa_head.png", Vector((0, -0.45 * h, 0)), mnz + 0.93 * h) +render("qa_hand.png", Vector((HAND_X + 0.08 * h, -0.35 * h, 0)), mnz + 0.72 * h) + +bpy.ops.object.select_all(action="DESELECT"); body.select_set(True) +for g in list(body.vertex_groups): body.vertex_groups.remove(g) +bpy.ops.export_scene.gltf( + filepath=OUT_GLB, export_format="GLB", export_yup=True, export_apply=False, + export_animations=False, export_skins=False, export_morph=False) +print(f"[fullhead] wrote {OUT_GLB} ({os.path.getsize(OUT_GLB)/1e6:.1f} MB), " + f"final tris={len(me.polygons)} (head {head_tris1} + budgets {T_BODY}+{T_HAND})") +print("[fullhead] DONE") diff --git a/characters/work/mako/02_graft.py b/characters/work/mako/02_graft.py new file mode 100644 index 0000000..1e4586d --- /dev/null +++ b/characters/work/mako/02_graft.py @@ -0,0 +1,194 @@ +# Mako work lane, stage 02: graft AccuRig skeleton + weights onto the fullhead GLB. +# Direct adaptation of work/lena/46_graft.py — sole difference: the target is the GLB itself +# (the mako lane has no .blend master; the GLB is the source of truth). +# +# blender --background --python 02_graft.py -- +# +# blender --background --python 46_graft.py -- +# +# The lane's rule (`.agents/plans/rig-graft-lane-2026-08-04.md`): FBX is a disposable rig carrier, +# the GLB is the sole source of truth for mesh and materials, and it only ever GAINS bones and +# weights. So nothing here touches a vertex position, a UV, or a texture — the mesh that comes out +# is byte-for-byte v10's, and that is asserted at the end, not hoped for. +# +# INDEX-EXACT, not nearest-surface. The plan assumed a decimated bait would have to hand weights to +# a full-res mesh by proximity, which is where the July lane mangled the fingers (close-packed +# digits confuse a proximity search). AccuRig rigged the shipping mesh whole and returned the same +# 31,111 vertices in the same order, so weights copy index-to-index and the failure mode is gone. +# +# TWO CORRECTIONS, both SOLVED here rather than hardcoded. The carrier was exported at 1.700 m and +# AccuRig also shifted the body ~1.2 cm (the same family as the July lane's 4 cm offset trap). Both +# are recovered by least-squares fitting a uniform scale + translation between the two vertex sets, +# so this stays correct if either changes on a future re-rig. +import bpy, sys, os, time +import numpy as np +from mathutils import Vector, Matrix + +argv = sys.argv[sys.argv.index("--") + 1:] +GLB, FBX, OUTB, OUTG = argv[0], argv[1], os.path.abspath(argv[2]), os.path.abspath(argv[3]) +t0 = time.time() + + +def log(m): + print(f"[graft {time.time()-t0:6.1f}s] {m}", flush=True) + + +bpy.ops.wm.read_factory_settings(use_empty=True) +bpy.ops.import_scene.gltf(filepath=GLB) +for _o in bpy.data.objects: + if _o.type == "MESH": + bpy.context.view_layer.objects.active = _o; _o.select_set(True) +bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) +body = max([o for o in bpy.data.objects if o.type == 'MESH'], key=lambda o: len(o.data.vertices)) +me = body.data +n = len(me.vertices) +V = np.empty(n * 3); me.vertices.foreach_get("co", V); V = V.reshape(-1, 3) +V_ref = V.copy() +log(f"target: '{body.name}' {n}v {len(me.polygons)}f uv={[l.name for l in me.uv_layers]}") + +before = {o.name for o in bpy.data.objects} +bpy.ops.import_scene.fbx(filepath=FBX) +new = [o for o in bpy.data.objects if o.name not in before] +arm = next(o for o in new if o.type == 'ARMATURE') +src = max([o for o in new if o.type == 'MESH'], key=lambda o: len(o.data.vertices)) +log(f"carrier: armature '{arm.name}' {len(arm.data.bones)} bones, mesh {len(src.data.vertices)}v") + +# AccuRig ships a "T-Pose" take, and Blender's importer binds it as an ACTION. An action re-applies +# its pose on every evaluation and on every file load, so clearing pose values while it exists is +# futile — the in-session assert passes, the saved file comes back posed, and the body deforms +# 1.25 units into the air with every edge length unchanged. Drop the animation data first. +dropped = [] +for o in (arm, src): + if o.animation_data: + dropped.append(o.name) + o.animation_data_clear() + dat = o.data + if dat and getattr(dat, "animation_data", None): + dat.animation_data_clear() + sk = getattr(dat, "shape_keys", None) + if sk and sk.animation_data: + sk.animation_data_clear() +for act in list(bpy.data.actions): + if act.users == 0: + bpy.data.actions.remove(act) +log(f"animation data cleared on {dropped}; actions left: {[a.name for a in bpy.data.actions]}") + +assert len(src.data.vertices) == n, \ + f"vertex count differs ({len(src.data.vertices)} vs {n}) — index-exact graft is off the table" + +# ---- recover scale + translation between carrier and target (order is preserved) ---- +S = np.empty(n * 3); src.data.vertices.foreach_get("co", S); S = S.reshape(-1, 3) +M = np.array(src.matrix_world) +Sw = S @ M[:3, :3].T + M[:3, 3] +ca, cb = V.mean(axis=0), Sw.mean(axis=0) +A, B = V - ca, Sw - cb +s = float((A * B).sum() / (B * B).sum()) +t = ca - s * cb +res = np.linalg.norm(V - (s * Sw + t), axis=1) +UNITM = 1.750 / (V[:, 2].max() - V[:, 2].min()) +log(f"fit: scale {s:.6f} (1/{1/s:.6f}) translation {np.round(t, 6)} in target units") +log(f" carrier offset removed: {np.round(-t / s * 1000, 3)} mm in carrier space") +log(f" index-exact residual: mean {res.mean()*UNITM*1000:.4f} mm p99 " + f"{np.percentile(res,99)*UNITM*1000:.4f} mm max {res.max()*UNITM*1000:.4f} mm") + +# ---- move the skeleton into the target's space; the mesh never moves ---- +arm.matrix_world = Matrix.Translation(Vector(t)) @ Matrix.Diagonal( + Vector((s, s, s))).to_4x4() @ arm.matrix_world +bpy.ops.object.select_all(action='DESELECT') +arm.select_set(True) +bpy.context.view_layer.objects.active = arm +bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) +log(f"armature placed; scale now {tuple(round(v,5) for v in arm.scale)}") + +# Clear the carrier's POSE, and do it AFTER transform_apply. The mesh binds to the REST skeleton, +# so any pose riding along is pure displacement — and this FBX arrives with one. Clearing it by +# setting location/rotation/scale properties BEFORE the apply did not survive: transform_apply +# reintroduced non-identity rotations on the thigh and foot, the saved file deformed the body +# 1.25 units into the air, and every edge length stayed put — rigid motion, which reads as +# "weights are fine, model is gone". Use the pose operator, last, and assert it took. +bpy.ops.object.mode_set(mode='POSE') +bpy.ops.pose.select_all(action='SELECT') +bpy.ops.pose.transforms_clear() +bpy.ops.object.mode_set(mode='OBJECT') +bpy.context.view_layer.update() +worst_pb = max(np.abs(np.array(pb.matrix_basis) - np.eye(4)).max() for pb in arm.pose.bones) +log(f"pose cleared; largest deviation from identity across {len(arm.pose.bones)} bones: " + f"{worst_pb:.2e}") +assert worst_pb < 1e-6, "pose did not clear — the bind would be displaced" + +# ---- copy weights index-to-index ---- +for vg in list(body.vertex_groups): + body.vertex_groups.remove(vg) +name_of = {} +for g in src.vertex_groups: + name_of[g.index] = g.name + body.vertex_groups.new(name=g.name) +tgt = {g.name: g for g in body.vertex_groups} +copied = 0 +for v in src.data.vertices: + for g in v.groups: + w = g.weight + if w > 0.0: + tgt[name_of[g.group]].add([v.index], w, 'REPLACE') + copied += 1 +log(f"copied {copied} weights into {len(body.vertex_groups)} groups") + +# ---- bind ---- +for m_ in list(body.modifiers): + if m_.type == 'ARMATURE': + body.modifiers.remove(m_) +mod = body.modifiers.new("Armature", 'ARMATURE') +mod.object = arm +body.parent = arm +body.matrix_parent_inverse = arm.matrix_world.inverted() + +# ---- the carrier is disposable: nothing visual survives from the FBX ---- +carrier_mats = [m_ for m_ in src.data.materials if m_] +bpy.data.objects.remove(src, do_unlink=True) +for m_ in carrier_mats: + if m_.users == 0: + bpy.data.materials.remove(m_) +for im in list(bpy.data.images): + if im.users == 0 and im.name not in {"Render Result", "Viewer Node"}: + bpy.data.images.remove(im) + +# ---- verify ---- +V2 = np.empty(n * 3); me.vertices.foreach_get("co", V2); V2 = V2.reshape(-1, 3) +moved = np.linalg.norm(V2 - V_ref, axis=1).max() +tot = np.zeros(n) +for v in me.vertices: + tot[v.index] = sum(g.weight for g in v.groups) +print("\n=== VERIFY ===") +print(f" mesh vertices moved: {moved*UNITM*1000:.6f} mm (must be 0)") +print(f" height {V2[:,2].max()-V2[:,2].min():.5f} units, feet min-Z {V2[:,2].min():+.6f}") +print(f" weight sums: min {tot.min():.4f} mean {tot.mean():.4f} max {tot.max():.4f}") +print(f" unweighted vertices: {int((tot < 1e-6).sum())}") +print(f" bones {len(arm.data.bones)} vertex groups {len(body.vertex_groups)}") +print(f" uv layers {[l.name for l in me.uv_layers]} materials " + f"{[m_.name for m_ in me.materials if m_]}") +print(f" images {[i.name for i in bpy.data.images if i.size[0]]}") +assert moved < 1e-9, "the mesh moved — the graft must be additive only" + +# The rest mesh being right proves nothing: the armature modifier is what the engine will run. +# Evaluate it and require the deformed body to sit exactly where the undeformed one does. +dg = bpy.context.evaluated_depsgraph_get() +evo = body.evaluated_get(dg) +tmp = evo.to_mesh() +Dv = np.empty(len(tmp.vertices) * 3); tmp.vertices.foreach_get("co", Dv) +Dv = Dv.reshape(-1, 3) +evo.to_mesh_clear() +drift = np.linalg.norm(Dv - V_ref, axis=1).max() * UNITM * 1000 +print(f" DEFORMED at rest pose: z {Dv[:,2].min():.5f}..{Dv[:,2].max():.5f} " + f"max drift vs undeformed {drift:.6f} mm") +assert drift < 0.01, (f"the armature displaces the body by {drift:.3f} mm at rest — a leftover " + f"pose, not a weighting problem") + +bpy.ops.wm.save_as_mainfile(filepath=OUTB) +bpy.ops.object.select_all(action='DESELECT') +arm.select_set(True); body.select_set(True) +bpy.context.view_layer.objects.active = arm +bpy.ops.export_scene.gltf(filepath=OUTG, export_format='GLB', use_selection=True, + export_image_format='AUTO', export_jpeg_quality=95, + export_yup=True, export_apply=False, export_skins=True) +log(f"EXPORTED {OUTG} ({os.path.getsize(OUTG)/1e6:.2f} MB)") +print("GRAFT_DONE")