feat(characters): four VRoid characters imported and selectable
Kiyoko, Hikari, Aria and Momo, all CC-BY from Sketchfab, licences recorded beside each skin. Seven GLB characters selectable now. kiyoko 13 meshes 20 cloth chains (62 bones) 0.0% cross-leg bleed aria 15 meshes 15 cloth chains (37 bones) 0.0% momo 5 meshes 9 cloth chains (35 bones) 0.0% hikari 13 meshes 10 cloth chains (37 bones) 0.2%, 12 twist bones These are the first characters imported that were NOT authored against the library's own bone spelling, and every one of them broke something that had been quietly wrong all along. All four failures were in code that guesses anatomy from names, which is exactly what tools/rig_map.py exists to stop doing: - LIMB ROLES went to the first role in LIMB_ORDER that matched at all, so shin's catch-all "leg" claimed UpperLeg before thigh's exact "upperleg" was ever consulted, and the thigh went unassigned. The result depended on the order bones arrived in. Claims are now granted longest-stem first. This also broke Mixamo (LeftUpLeg/LeftLeg) and had simply never been hit, because every character so far used Rigify DEF- names. - Names cannot settle thigh-vs-shin at all. A bare "leg" is the SHIN on Mixamo and the THIGH on a rig whose shin is "knee" — both common, same token, opposite bones. RigRoles now walks the leg from the foot upward and fills in whatever the names could not, stepping over twist bones. - verify_character.py looked for legs by the substrings "thigh"/"shin", which VRoid spells UpperLeg/LowerLeg. It declared every locomotion clip static while the legs animated perfectly, and the cross-leg bleed check found no leg vertex groups at all and passed vacuously. Two green- looking lies from one missing lookup; both now read the sidecar's resolved roles. - The cosmetic/spring classifier matched whole tokens only, so Momo's HairFL / HairFR / HairF_Top tokenised to "hairfl" and matched nothing. She imported with six chains, all bust, and no hair. Both classifiers now share one rule that also accepts a two-character positional suffix, which is short enough that "forearm" and "earring" are still untouched. Two more pipeline fixes: - A source model's own clips leaked into the export. Clearing bpy.data .actions before the library import is not enough — hikari carried two on a second armature's NLA tracks, and NLA_TRACKS export mode ships anything in a track anywhere in the file. They export as rest-pose statues. Now everything not retargeted is stripped from every object. - verify_character.py gained a check for cloth chains with no measurable extent. A chain whose bones are zero-length is dropped by the runtime and simulates nothing, while the sidecar still cheerfully reports it. Known: hikari's ten cloth chains are all zero-length and her collider fit found nothing, so her costume does not simulate — her rig has been through two toolchains and its cosmetic bones are empty terminators. She animates correctly otherwise. The new check now reports this instead of hiding it. tools/retarget.py also carries local working-tree changes that predate this session. Co-Authored-By: Claude Opus 5 <[email protected]>
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "Anime Girl Rigged Anime model",
|
||||||
|
"uid": "fbccf5c5a7b244e7ab04fa44da19c621",
|
||||||
|
"author": "dequeijospizza",
|
||||||
|
"author_url": "https://sketchfab.com/dequeijospizza",
|
||||||
|
"license": "CC Attribution",
|
||||||
|
"license_slug": "by",
|
||||||
|
"source_url": "https://sketchfab.com/3d-models/anime-girl-rigged-anime-model-fbccf5c5a7b244e7ab04fa44da19c621"
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 637 B |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 259 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "school anime girl (rigged)",
|
||||||
|
"uid": "6eaf44ff6ef7461583a5eccc301b88fc",
|
||||||
|
"author": "hikari",
|
||||||
|
"author_url": "https://sketchfab.com/hikari0135",
|
||||||
|
"license": "CC Attribution",
|
||||||
|
"license_slug": "by",
|
||||||
|
"source_url": "https://sketchfab.com/3d-models/school-anime-girl-rigged-6eaf44ff6ef7461583a5eccc301b88fc"
|
||||||
|
}
|
||||||
@@ -0,0 +1,731 @@
|
|||||||
|
{
|
||||||
|
"roles": {
|
||||||
|
"hips": "Hips",
|
||||||
|
"head": "Bone.003_end",
|
||||||
|
"neck": "Neck",
|
||||||
|
"spine": [
|
||||||
|
"Spine",
|
||||||
|
"Chest",
|
||||||
|
"Upper Chest",
|
||||||
|
"Neck",
|
||||||
|
"Head",
|
||||||
|
"Bone.003",
|
||||||
|
"Bone.003_end"
|
||||||
|
],
|
||||||
|
"shoulder.R": "Right shoulder",
|
||||||
|
"shoulder.L": "Left shoulder",
|
||||||
|
"foot.R": "Right ankle",
|
||||||
|
"foot.L": "Left ankle",
|
||||||
|
"hand.R": "Right wrist",
|
||||||
|
"hand.L": "Left wrist",
|
||||||
|
"forearm.R": "Right elbow",
|
||||||
|
"forearm.L": "Left elbow",
|
||||||
|
"shin.R": "Right knee",
|
||||||
|
"shin.L": "Left knee",
|
||||||
|
"toe.R": "Right toe",
|
||||||
|
"toe.L": "Left toe_end",
|
||||||
|
"upper_arm.R": "Right arm",
|
||||||
|
"upper_arm.L": "Left arm",
|
||||||
|
"thigh.L": "Left leg",
|
||||||
|
"thigh.R": "Right leg"
|
||||||
|
},
|
||||||
|
"chains": [
|
||||||
|
{
|
||||||
|
"class": "breast",
|
||||||
|
"root_parent": "Upper Chest",
|
||||||
|
"bones": [
|
||||||
|
"Breast_L",
|
||||||
|
"J_Sec_L_Bust2",
|
||||||
|
"J_Sec_L_Bust2_end"
|
||||||
|
],
|
||||||
|
"tips": [
|
||||||
|
[
|
||||||
|
-0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2e-05,
|
||||||
|
5e-05,
|
||||||
|
-0.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2e-05,
|
||||||
|
5e-05,
|
||||||
|
-0.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"hulls": [
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
"neighbours": [
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": "breast",
|
||||||
|
"root_parent": "Upper Chest",
|
||||||
|
"bones": [
|
||||||
|
"Breast_R",
|
||||||
|
"J_Sec_R_Bust2",
|
||||||
|
"J_Sec_R_Bust2_end"
|
||||||
|
],
|
||||||
|
"tips": [
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
-0.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-3e-05,
|
||||||
|
5e-05,
|
||||||
|
-1e-05
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-3e-05,
|
||||||
|
5e-05,
|
||||||
|
-1e-05
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"hulls": [
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
"neighbours": [
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": "hair",
|
||||||
|
"root_parent": "Head",
|
||||||
|
"bones": [
|
||||||
|
"J_Sec_Hair1_01",
|
||||||
|
"J_Sec_Hair2_01",
|
||||||
|
"J_Sec_Hair2_01_end"
|
||||||
|
],
|
||||||
|
"tips": [
|
||||||
|
[
|
||||||
|
-0.0,
|
||||||
|
-0.0,
|
||||||
|
-0.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3e-05,
|
||||||
|
3e-05,
|
||||||
|
-4e-05
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3e-05,
|
||||||
|
3e-05,
|
||||||
|
-4e-05
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"hulls": [
|
||||||
|
[
|
||||||
|
[
|
||||||
|
-0.0,
|
||||||
|
-0.0,
|
||||||
|
-0.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
-0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
"neighbours": [
|
||||||
|
{
|
||||||
|
"J_Sec_Hair2_01": 14.335576471341188
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"J_Sec_Hair1_01": 14.335576471341188
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": "hair",
|
||||||
|
"root_parent": "Head",
|
||||||
|
"bones": [
|
||||||
|
"J_Sec_Hair1_02",
|
||||||
|
"J_Sec_Hair2_02",
|
||||||
|
"J_Sec_Hair2_02_end"
|
||||||
|
],
|
||||||
|
"tips": [
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
-0.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
2e-05,
|
||||||
|
-5e-05
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
2e-05,
|
||||||
|
-5e-05
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"hulls": [
|
||||||
|
[
|
||||||
|
[
|
||||||
|
-0.0,
|
||||||
|
0.0,
|
||||||
|
-0.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
-0.0,
|
||||||
|
0.0,
|
||||||
|
-0.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
"neighbours": [
|
||||||
|
{
|
||||||
|
"J_Sec_Hair2_02": 13.644314260335037
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"J_Sec_Hair1_02": 13.644314260335037
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": "hair",
|
||||||
|
"root_parent": "Head",
|
||||||
|
"bones": [
|
||||||
|
"J_Sec_Hair1_03",
|
||||||
|
"J_Sec_Hair2_03",
|
||||||
|
"J_Sec_Hair2_03_end"
|
||||||
|
],
|
||||||
|
"tips": [
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
-0.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1e-05,
|
||||||
|
-0.0,
|
||||||
|
-6e-05
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1e-05,
|
||||||
|
-0.0,
|
||||||
|
-6e-05
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"hulls": [
|
||||||
|
[
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
-0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
"neighbours": [
|
||||||
|
{
|
||||||
|
"J_Sec_Hair2_03": 29.697010893706615
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"J_Sec_Hair1_03": 29.697010893706615
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": "hair",
|
||||||
|
"root_parent": "Head",
|
||||||
|
"bones": [
|
||||||
|
"J_Sec_Hair1_04",
|
||||||
|
"J_Sec_Hair2_04",
|
||||||
|
"J_Sec_Hair2_04_end"
|
||||||
|
],
|
||||||
|
"tips": [
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2e-05,
|
||||||
|
1e-05,
|
||||||
|
-6e-05
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2e-05,
|
||||||
|
1e-05,
|
||||||
|
-6e-05
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"hulls": [
|
||||||
|
[
|
||||||
|
[
|
||||||
|
-0.0,
|
||||||
|
-0.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
-0.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
"neighbours": [
|
||||||
|
{
|
||||||
|
"J_Sec_Hair2_04": 11.592366459380674
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"J_Sec_Hair1_04": 11.592366459380674
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": "hair",
|
||||||
|
"root_parent": "Head",
|
||||||
|
"bones": [
|
||||||
|
"J_Sec_Hair1_05",
|
||||||
|
"J_Sec_Hair2_05",
|
||||||
|
"J_Sec_Hair2_05_end"
|
||||||
|
],
|
||||||
|
"tips": [
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
-0.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1e-05,
|
||||||
|
2e-05,
|
||||||
|
-6e-05
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1e-05,
|
||||||
|
2e-05,
|
||||||
|
-6e-05
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"hulls": [
|
||||||
|
[
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
-0.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
-0.0,
|
||||||
|
0.0,
|
||||||
|
-0.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
"neighbours": [
|
||||||
|
{
|
||||||
|
"J_Sec_Hair2_05": 11.876289952662745
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"J_Sec_Hair1_05": 11.876289952662745
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": "hair",
|
||||||
|
"root_parent": "Head",
|
||||||
|
"bones": [
|
||||||
|
"J_Sec_Hair1_06",
|
||||||
|
"J_Sec_Hair2_06",
|
||||||
|
"J_Sec_Hair3_06",
|
||||||
|
"J_Sec_Hair4_06",
|
||||||
|
"J_Sec_Hair5_06",
|
||||||
|
"J_Sec_Hair5_06_end"
|
||||||
|
],
|
||||||
|
"tips": [
|
||||||
|
[
|
||||||
|
-0.0,
|
||||||
|
0.0,
|
||||||
|
-0.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
3e-05,
|
||||||
|
-5e-05
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
3e-05,
|
||||||
|
-5e-05
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"hulls": [
|
||||||
|
[
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
-0.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
-0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
-0.0,
|
||||||
|
0.0,
|
||||||
|
-0.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
-0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
-0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
"neighbours": [
|
||||||
|
{
|
||||||
|
"J_Sec_Hair2_06": 35.917357232526754
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"J_Sec_Hair3_06": 35.52913589468082,
|
||||||
|
"J_Sec_Hair1_06": 35.917357232526754
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"J_Sec_Hair4_06": 40.302343821289796,
|
||||||
|
"J_Sec_Hair2_06": 35.52913589468082
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"J_Sec_Hair3_06": 40.302343821289796,
|
||||||
|
"J_Sec_Hair5_06": 40.950213036078026
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"J_Sec_Hair4_06": 40.950213036078026
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": "hair",
|
||||||
|
"root_parent": "Head",
|
||||||
|
"bones": [
|
||||||
|
"J_Sec_Hair1_07",
|
||||||
|
"J_Sec_Hair2_07",
|
||||||
|
"J_Sec_Hair3_07",
|
||||||
|
"J_Sec_Hair4_07",
|
||||||
|
"J_Sec_Hair5_07",
|
||||||
|
"J_Sec_Hair5_07_end"
|
||||||
|
],
|
||||||
|
"tips": [
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-0.0,
|
||||||
|
0.0,
|
||||||
|
-0.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3e-05,
|
||||||
|
3e-05,
|
||||||
|
-3e-05
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3e-05,
|
||||||
|
3e-05,
|
||||||
|
-3e-05
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"hulls": [
|
||||||
|
[
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
-0.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
-0.0,
|
||||||
|
0.0,
|
||||||
|
-0.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
-0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
-0.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
"neighbours": [
|
||||||
|
{
|
||||||
|
"J_Sec_Hair2_07": 35.51566362438205
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"J_Sec_Hair3_07": 35.31215403867316,
|
||||||
|
"J_Sec_Hair1_07": 35.51566362438205
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"J_Sec_Hair4_07": 44.27519591590416,
|
||||||
|
"J_Sec_Hair2_07": 35.31215403867316
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"J_Sec_Hair3_07": 44.27519591590416,
|
||||||
|
"J_Sec_Hair5_07": 40.6256217681383
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"J_Sec_Hair4_07": 40.6256217681383
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": "hair",
|
||||||
|
"root_parent": "Head",
|
||||||
|
"bones": [
|
||||||
|
"J_Sec_Hair1_08",
|
||||||
|
"J_Sec_Hair2_08",
|
||||||
|
"J_Sec_Hair3_08",
|
||||||
|
"J_Sec_Hair3_08_end"
|
||||||
|
],
|
||||||
|
"tips": [
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4e-05,
|
||||||
|
-4e-05,
|
||||||
|
-1e-05
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4e-05,
|
||||||
|
-4e-05,
|
||||||
|
-1e-05
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"hulls": [
|
||||||
|
[
|
||||||
|
[
|
||||||
|
-0.0,
|
||||||
|
-0.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
-0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
-0.0,
|
||||||
|
-0.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
"neighbours": [
|
||||||
|
{
|
||||||
|
"J_Sec_Hair2_08": 2.5078807040660998
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"J_Sec_Hair1_08": 2.5078807040660998,
|
||||||
|
"J_Sec_Hair3_08": 2.222614413427433
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"J_Sec_Hair2_08": 2.222614413427433
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"twist": [
|
||||||
|
{
|
||||||
|
"bone": "Bone.004",
|
||||||
|
"parent": "Head",
|
||||||
|
"child": "Bone.004_end"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bone": "Bone.005",
|
||||||
|
"parent": "Head",
|
||||||
|
"child": "Bone.005_end"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bone": "Bone.006",
|
||||||
|
"parent": "Head",
|
||||||
|
"child": "Bone.006_end"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bone": "Bone.007",
|
||||||
|
"parent": "Head",
|
||||||
|
"child": "Bone.007_end"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bone": "Bone.008",
|
||||||
|
"parent": "Head",
|
||||||
|
"child": "Bone.008_end"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bone": "Bone.009",
|
||||||
|
"parent": "Head",
|
||||||
|
"child": "Bone.009_end"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bone": "Bone.010",
|
||||||
|
"parent": "Head",
|
||||||
|
"child": "Bone.010_end"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bone": "Bone.011",
|
||||||
|
"parent": "Head",
|
||||||
|
"child": "Bone.011_end"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bone": "Bone.012",
|
||||||
|
"parent": "Head",
|
||||||
|
"child": "Bone.012_end"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bone": "Bone.013",
|
||||||
|
"parent": "Head",
|
||||||
|
"child": "Bone.013_end"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bone": "Bone.001",
|
||||||
|
"parent": "Upper Chest",
|
||||||
|
"child": "Bone.001_end"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bone": "Bone.002",
|
||||||
|
"parent": "Upper Chest",
|
||||||
|
"child": "Bone.002_end"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"colliders": [],
|
||||||
|
"weights_authored": true,
|
||||||
|
"driven_bones": [
|
||||||
|
"Bone.003",
|
||||||
|
"Bone.003_end",
|
||||||
|
"Chest",
|
||||||
|
"Head",
|
||||||
|
"Hips",
|
||||||
|
"Left ankle",
|
||||||
|
"Left arm",
|
||||||
|
"Left elbow",
|
||||||
|
"Left knee",
|
||||||
|
"Left leg",
|
||||||
|
"Left shoulder",
|
||||||
|
"Left toe_end",
|
||||||
|
"Left wrist",
|
||||||
|
"Neck",
|
||||||
|
"Right ankle",
|
||||||
|
"Right arm",
|
||||||
|
"Right elbow",
|
||||||
|
"Right knee",
|
||||||
|
"Right leg",
|
||||||
|
"Right shoulder",
|
||||||
|
"Right toe",
|
||||||
|
"Right wrist",
|
||||||
|
"Spine",
|
||||||
|
"Upper Chest"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 345 KiB |
|
After Width: | Height: | Size: 337 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "Kiyoko School Girl",
|
||||||
|
"uid": "072667d7b2b3468e9baff483b27c3a09",
|
||||||
|
"author": "Kasujin",
|
||||||
|
"author_url": "https://sketchfab.com/Kasujin",
|
||||||
|
"license": "CC Attribution",
|
||||||
|
"license_slug": "by",
|
||||||
|
"source_url": "https://sketchfab.com/3d-models/kiyoko-school-girl-072667d7b2b3468e9baff483b27c3a09"
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 220 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 896 B |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 256 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 296 KiB |
|
After Width: | Height: | Size: 245 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 26 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "DANDADAN - Momo Ayase (3D Model) + DL",
|
||||||
|
"uid": "6ac6c3476a1f4b8da1c7de7e98a7c83c",
|
||||||
|
"author": "HiGuys920",
|
||||||
|
"author_url": "https://sketchfab.com/higuys920",
|
||||||
|
"license": "CC Attribution",
|
||||||
|
"license_slug": "by",
|
||||||
|
"source_url": "https://sketchfab.com/3d-models/dandadan-momo-ayase-3d-model-dl-6ac6c3476a1f4b8da1c7de7e98a7c83c"
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 562 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -20,6 +20,34 @@
|
|||||||
"description": "Quaternius reference mannequin (CC0)",
|
"description": "Quaternius reference mannequin (CC0)",
|
||||||
"model": "res://assets/characters/skins/mannequin.glb",
|
"model": "res://assets/characters/skins/mannequin.glb",
|
||||||
"unlocked": true
|
"unlocked": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "kiyoko",
|
||||||
|
"name": "Kiyoko",
|
||||||
|
"description": "Kiyoko School Girl \u2014 VRoid (CC-BY, Kasujin)",
|
||||||
|
"model": "res://assets/characters/skins/kiyoko.glb",
|
||||||
|
"unlocked": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "aria",
|
||||||
|
"name": "Aria",
|
||||||
|
"description": "Anime Girl Rigged \u2014 VRoid (CC-BY, dequeijospizza)",
|
||||||
|
"model": "res://assets/characters/skins/aria.glb",
|
||||||
|
"unlocked": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "momo",
|
||||||
|
"name": "Momo",
|
||||||
|
"description": "Momo Ayase, DANDADAN (CC-BY, HiGuys920)",
|
||||||
|
"model": "res://assets/characters/skins/momo.glb",
|
||||||
|
"unlocked": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "hikari",
|
||||||
|
"name": "Hikari",
|
||||||
|
"description": "School anime girl \u2014 VRoid (CC-BY, hikari)",
|
||||||
|
"model": "res://assets/characters/skins/hikari.glb",
|
||||||
|
"unlocked": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -106,6 +106,25 @@ SPRING_CLASSES = {"hair", "skirt", "cloth", "ribbon", "tail", "cape", "coat",
|
|||||||
"antenna", "wing"}
|
"antenna", "wing"}
|
||||||
|
|
||||||
|
|
||||||
|
def spring_class(name):
|
||||||
|
"""Which secondary-motion class this bone belongs to, or None.
|
||||||
|
|
||||||
|
Whole token first, then a SHORT positional suffix — the same rule as
|
||||||
|
rig_map.is_cosmetic, and it must be the same rule, because a bone that reads
|
||||||
|
as cosmetic there and as nothing here is left out of every chain and its
|
||||||
|
geometry never moves. DANDADAN's hair is HairFL / HairFR / HairF_Top, which
|
||||||
|
tokenise to "hairfl" and matched no class at all: she imported with six
|
||||||
|
chains, all of them her bust, and not one strand of hair.
|
||||||
|
"""
|
||||||
|
for t in tokens(name):
|
||||||
|
if t in SPRING_CLASSES:
|
||||||
|
return t
|
||||||
|
for c in SPRING_CLASSES:
|
||||||
|
if len(t) - len(c) <= 2 and t.startswith(c) and len(t) > len(c):
|
||||||
|
return c
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------- import
|
# --------------------------------------------------------------------- import
|
||||||
|
|
||||||
|
|
||||||
@@ -680,8 +699,7 @@ def _body_points(meshes):
|
|||||||
mw = m.matrix_world
|
mw = m.matrix_world
|
||||||
for v in m.data.vertices:
|
for v in m.data.vertices:
|
||||||
cloth_w = sum(g.weight for g in v.groups
|
cloth_w = sum(g.weight for g in v.groups
|
||||||
if any(t in SPRING_CLASSES
|
if spring_class(gname.get(g.group, "")))
|
||||||
for t in tokens(gname.get(g.group, ""))))
|
|
||||||
if cloth_w < 0.35:
|
if cloth_w < 0.35:
|
||||||
out.append(mw @ v.co)
|
out.append(mw @ v.co)
|
||||||
return out
|
return out
|
||||||
@@ -880,7 +898,7 @@ def describe_rig(arm, roles, mapping, meshes):
|
|||||||
chains = []
|
chains = []
|
||||||
springy = {b.name: b for b in arm.data.bones
|
springy = {b.name: b for b in arm.data.bones
|
||||||
if b.name not in driven
|
if b.name not in driven
|
||||||
and any(t in SPRING_CLASSES for t in tokens(b.name))}
|
and spring_class(b.name)}
|
||||||
neighbours = _cloth_neighbours(meshes, set(springy))
|
neighbours = _cloth_neighbours(meshes, set(springy))
|
||||||
|
|
||||||
for name, bone in springy.items():
|
for name, bone in springy.items():
|
||||||
@@ -902,8 +920,7 @@ def describe_rig(arm, roles, mapping, meshes):
|
|||||||
hulls.append(_hull_samples(arm, path[i], owned))
|
hulls.append(_hull_samples(arm, path[i], owned))
|
||||||
fallback = t
|
fallback = t
|
||||||
chains.append({
|
chains.append({
|
||||||
"class": next((t for t in tokens(path[0])
|
"class": spring_class(path[0]) or "cloth",
|
||||||
if t in SPRING_CLASSES), "cloth"),
|
|
||||||
"root_parent": bone.parent.name if bone.parent else None,
|
"root_parent": bone.parent.name if bone.parent else None,
|
||||||
"bones": path,
|
"bones": path,
|
||||||
"tips": tips,
|
"tips": tips,
|
||||||
@@ -1066,6 +1083,7 @@ def main():
|
|||||||
arm.animation_data.nla_tracks.remove(track)
|
arm.animation_data.nla_tracks.remove(track)
|
||||||
|
|
||||||
merged = 0
|
merged = 0
|
||||||
|
kept_clips = set()
|
||||||
for action in [a for a in bpy.data.actions if a not in before_actions]:
|
for action in [a for a in bpy.data.actions if a not in before_actions]:
|
||||||
clip = LIBRARY_CLIP_MAP.get(action.name.split(".")[0])
|
clip = LIBRARY_CLIP_MAP.get(action.name.split(".")[0])
|
||||||
if not clip:
|
if not clip:
|
||||||
@@ -1074,11 +1092,44 @@ def main():
|
|||||||
baked = retarget_clip(src_arm, src_roles, arm, roles, mapping, action,
|
baked = retarget_clip(src_arm, src_roles, arm, roles, mapping, action,
|
||||||
clip, yaw, scale)
|
clip, yaw, scale)
|
||||||
add_nla_clip(arm, baked, clip)
|
add_nla_clip(arm, baked, clip)
|
||||||
|
kept_clips.add(clip)
|
||||||
merged += 1
|
merged += 1
|
||||||
if merged == 0:
|
if merged == 0:
|
||||||
print("ERROR: no clips retargeted")
|
print("ERROR: no clips retargeted")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Drop anything animated that we did not put here.
|
||||||
|
#
|
||||||
|
# Clearing bpy.data.actions before the library import is not enough: a
|
||||||
|
# character can carry its own clip in an NLA track that survives, and it is
|
||||||
|
# then exported alongside ours. `hikari` shipped two — " Girl|Loli Anime
|
||||||
|
# GirlAction" — which are keyed for a rig that no longer exists after the
|
||||||
|
# retarget, so they export as frozen rest-pose statues and the build fails a
|
||||||
|
# check that is right to fail it.
|
||||||
|
# Across EVERY object, not just the armature we retargeted onto. The export
|
||||||
|
# runs in NLA_TRACKS mode, so anything sitting in an NLA track anywhere in
|
||||||
|
# the file ships — including a second armature the model happened to carry,
|
||||||
|
# which is where hikari's two were hiding.
|
||||||
|
def _strip_foreign(ad):
|
||||||
|
if ad is None:
|
||||||
|
return
|
||||||
|
for track in list(ad.nla_tracks):
|
||||||
|
if not any(st.action and st.action.name in kept_clips
|
||||||
|
for st in track.strips):
|
||||||
|
ad.nla_tracks.remove(track)
|
||||||
|
if ad.action is not None and ad.action.name not in kept_clips:
|
||||||
|
ad.action = None
|
||||||
|
|
||||||
|
for obj in list(bpy.data.objects):
|
||||||
|
_strip_foreign(obj.animation_data)
|
||||||
|
if obj.type == "MESH" and obj.data.shape_keys:
|
||||||
|
_strip_foreign(obj.data.shape_keys.animation_data)
|
||||||
|
for a in list(bpy.data.actions):
|
||||||
|
if a.name not in kept_clips:
|
||||||
|
a.use_fake_user = False
|
||||||
|
bpy.data.actions.remove(a)
|
||||||
|
print(f"Kept {len(kept_clips)} clips; dropped everything else")
|
||||||
|
|
||||||
for o in new_objects:
|
for o in new_objects:
|
||||||
bpy.data.objects.remove(o, do_unlink=True)
|
bpy.data.objects.remove(o, do_unlink=True)
|
||||||
|
|
||||||
@@ -1149,7 +1200,8 @@ def subdivide_cloth_panels(arm, meshes, roles, segments=4):
|
|||||||
for bone in arm.data.bones:
|
for bone in arm.data.bones:
|
||||||
if not is_cosmetic(bone.name):
|
if not is_cosmetic(bone.name):
|
||||||
continue
|
continue
|
||||||
if not any(t in SPRING_CLASSES and t != "hair" for t in tokens(bone.name)):
|
cls = spring_class(bone.name)
|
||||||
|
if cls is None or cls == "hair":
|
||||||
continue
|
continue
|
||||||
if bone.children:
|
if bone.children:
|
||||||
continue # already a chain — the artist's own topology wins
|
continue # already a chain — the artist's own topology wins
|
||||||
|
|||||||
@@ -97,8 +97,25 @@ def tokens(name: str):
|
|||||||
|
|
||||||
|
|
||||||
def is_cosmetic(name: str) -> bool:
|
def is_cosmetic(name: str) -> bool:
|
||||||
"""Whole-token match only — see the note on COSMETIC."""
|
"""Whole-token match, plus a short positional suffix — see COSMETIC.
|
||||||
return any(t in COSMETIC for t in tokens(name))
|
|
||||||
|
A bare token match misses the very common habit of gluing a position onto
|
||||||
|
the word: DANDADAN's hair bones are HairFL / HairFR / HairF_Top (front-left,
|
||||||
|
front-right, front-top), which tokenise to "hairfl" and match nothing, so
|
||||||
|
her hair was not detected as hair at all and none of it moved.
|
||||||
|
|
||||||
|
Only a SHORT remainder counts — two characters at most, which covers l/r/f/b
|
||||||
|
and the fl/fr/bl/br pairs and nothing else. That is what keeps the original
|
||||||
|
rule intact: "forearm" still does not begin with any cosmetic stem, and
|
||||||
|
"earring" has a four-character remainder and is not swept in by accident.
|
||||||
|
"""
|
||||||
|
for t in tokens(name):
|
||||||
|
if t in COSMETIC:
|
||||||
|
return True
|
||||||
|
for c in COSMETIC:
|
||||||
|
if len(t) - len(c) <= 2 and t.startswith(c) and len(t) > len(c):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def is_segment_of(name: str, all_names: set) -> bool:
|
def is_segment_of(name: str, all_names: set) -> bool:
|
||||||
@@ -131,11 +148,33 @@ class RigRoles:
|
|||||||
self.head = None
|
self.head = None
|
||||||
self.neck = None
|
self.neck = None
|
||||||
self._resolve_limbs()
|
self._resolve_limbs()
|
||||||
|
self._fill_limbs_by_anatomy()
|
||||||
self._resolve_spine()
|
self._resolve_spine()
|
||||||
|
|
||||||
# ---------------------------------------------------------------- limbs
|
# ---------------------------------------------------------------- limbs
|
||||||
|
|
||||||
def _resolve_limbs(self):
|
def _resolve_limbs(self):
|
||||||
|
"""Claim limb roles by the MOST SPECIFIC spelling that matches.
|
||||||
|
|
||||||
|
Not by walking LIMB_ORDER and taking the first role that matches at all.
|
||||||
|
That let a catch-all stem in an early role beat an exact one in a later
|
||||||
|
role, and the result depended on the order bones happened to arrive in:
|
||||||
|
|
||||||
|
J_Bip_L_UpperLeg canon "upperleg"
|
||||||
|
shin matches "leg" <- claimed it, because shin is first
|
||||||
|
thigh matches "upperleg" <- never consulted
|
||||||
|
J_Bip_L_LowerLeg canon "lowerleg"
|
||||||
|
shin already taken; nothing in thigh matches -> unclaimed
|
||||||
|
|
||||||
|
so a VRoid rig resolved with no thighs at all and the build stopped. The
|
||||||
|
same trap catches Mixamo, whose LeftUpLeg is a thigh and LeftLeg a shin —
|
||||||
|
it stayed hidden only because every character imported so far used the
|
||||||
|
Rigify DEF- spellings, where each role has an exact stem of its own.
|
||||||
|
|
||||||
|
Every plausible claim is collected, then granted longest-stem first, so
|
||||||
|
"upperleg" always beats "leg" whatever order the bones are in.
|
||||||
|
"""
|
||||||
|
claims = []
|
||||||
for name in self.names:
|
for name in self.names:
|
||||||
if is_cosmetic(name) or is_segment_of(name, self.names):
|
if is_cosmetic(name) or is_segment_of(name, self.names):
|
||||||
continue
|
continue
|
||||||
@@ -144,11 +183,56 @@ class RigRoles:
|
|||||||
continue
|
continue
|
||||||
stem = canon(name)
|
stem = canon(name)
|
||||||
for role in LIMB_ORDER:
|
for role in LIMB_ORDER:
|
||||||
if (role, side) in self.limb:
|
hits = [alt.replace("_", "") for alt in LIMB_ROLES[role]
|
||||||
|
if alt.replace("_", "") in stem]
|
||||||
|
if hits:
|
||||||
|
claims.append((len(max(hits, key=len)), role, side, name))
|
||||||
|
# Longest stem wins; LIMB_ORDER breaks ties, so a bone matching two roles
|
||||||
|
# equally well still resolves the way the list intends.
|
||||||
|
order = {role: i for i, role in enumerate(LIMB_ORDER)}
|
||||||
|
claims.sort(key=lambda c: (-c[0], order[c[1]]))
|
||||||
|
used = set()
|
||||||
|
for _, role, side, name in claims:
|
||||||
|
if (role, side) in self.limb or name in used:
|
||||||
continue
|
continue
|
||||||
if any(alt.replace("_", "") in stem for alt in LIMB_ROLES[role]):
|
|
||||||
self.limb[(role, side)] = name
|
self.limb[(role, side)] = name
|
||||||
|
used.add(name)
|
||||||
|
|
||||||
|
# A leg is thigh -> shin -> foot, whatever the bones are called. Walk it.
|
||||||
|
#
|
||||||
|
# Names alone cannot settle this and it is not a matter of adding more
|
||||||
|
# spellings. A bare "leg" is the SHIN on a Mixamo rig (whose thigh is
|
||||||
|
# "UpLeg") and the THIGH on a rig whose shin is called "knee" — the same
|
||||||
|
# token means opposite bones, and both rigs are common. What does not vary
|
||||||
|
# is the skeleton: the foot's parent is the shin and the shin's parent is
|
||||||
|
# the thigh.
|
||||||
|
#
|
||||||
|
# Only ever FILLS IN what the names could not resolve; a confident name
|
||||||
|
# match is never overridden. Twist bones are stepped over, since a rig may
|
||||||
|
# put one between the thigh and the shin.
|
||||||
|
def _fill_limbs_by_anatomy(self):
|
||||||
|
for side in ("L", "R"):
|
||||||
|
chain = []
|
||||||
|
b = self.bones.get(self.limb.get(("foot", side), ""))
|
||||||
|
while b is not None and len(chain) < 6:
|
||||||
|
b = b.parent
|
||||||
|
if b is None or b.name not in self.names:
|
||||||
break
|
break
|
||||||
|
if is_cosmetic(b.name) or is_segment_of(b.name, self.names):
|
||||||
|
continue # a twist segment is not a joint of its own
|
||||||
|
chain.append(b.name)
|
||||||
|
# chain is now [shin, thigh, hips, ...] going up from the foot.
|
||||||
|
for role, up in (("shin", 0), ("thigh", 1)):
|
||||||
|
if (role, side) in self.limb or up >= len(chain):
|
||||||
|
continue
|
||||||
|
name = chain[up]
|
||||||
|
# Never claim a bone another role already owns, and never claim
|
||||||
|
# the trunk — a two-bone leg would otherwise take the hips.
|
||||||
|
if name in self.limb.values():
|
||||||
|
continue
|
||||||
|
if side_of(name) != side:
|
||||||
|
continue
|
||||||
|
self.limb[(role, side)] = name
|
||||||
|
|
||||||
# ---------------------------------------------------------------- spine
|
# ---------------------------------------------------------------- spine
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,16 @@ PATH = argv[0]
|
|||||||
SIDECAR = argv[1] if len(argv) > 1 else os.path.splitext(PATH)[0] + ".rig.json"
|
SIDECAR = argv[1] if len(argv) > 1 else os.path.splitext(PATH)[0] + ".rig.json"
|
||||||
|
|
||||||
LEG_HINTS = ("thigh", "shin", "foot", "toe", "upleg", "calf")
|
LEG_HINTS = ("thigh", "shin", "foot", "toe", "upleg", "calf")
|
||||||
|
# Resolved bone roles from the sidecar — filled in below. Every check that needs
|
||||||
|
# to know "which bones are the legs" asks THIS, not the name hints.
|
||||||
|
#
|
||||||
|
# The hints are a fallback for a build with no sidecar, and they are only ever
|
||||||
|
# right for rigs that happen to spell things the way the library does. A VRoid
|
||||||
|
# rig calls its thigh J_Bip_L_UpperLeg: no hint matches it, so the cross-leg
|
||||||
|
# bleed check found no leg vertex groups at all and PASSED vacuously, and the
|
||||||
|
# leg-motion check declared every locomotion clip static while the legs were in
|
||||||
|
# fact animating perfectly. Two green-looking lies from one missing lookup.
|
||||||
|
ROLE_LEGS = {"L": set(), "R": set()}
|
||||||
failures = []
|
failures = []
|
||||||
warnings = []
|
warnings = []
|
||||||
# Set from the sidecar. A model whose source had no skeleton at all has to go
|
# Set from the sidecar. A model whose source had no skeleton at all has to go
|
||||||
@@ -71,6 +81,10 @@ def check(ok, label, detail="", needs_authored_weights=False, advisory=False):
|
|||||||
|
|
||||||
|
|
||||||
def side_of(name):
|
def side_of(name):
|
||||||
|
if name in ROLE_LEGS["L"]:
|
||||||
|
return -1
|
||||||
|
if name in ROLE_LEGS["R"]:
|
||||||
|
return 1
|
||||||
n = name.lower()
|
n = name.lower()
|
||||||
if not any(h in n for h in LEG_HINTS):
|
if not any(h in n for h in LEG_HINTS):
|
||||||
return 0
|
return 0
|
||||||
@@ -81,9 +95,17 @@ def side_of(name):
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
SIDECAR_INFO = {}
|
||||||
if os.path.exists(SIDECAR):
|
if os.path.exists(SIDECAR):
|
||||||
with open(SIDECAR, "r", encoding="utf-8") as f:
|
with open(SIDECAR, "r", encoding="utf-8") as f:
|
||||||
weights_authored = bool(json.load(f).get("weights_authored", True))
|
SIDECAR_INFO = json.load(f)
|
||||||
|
weights_authored = bool(SIDECAR_INFO.get("weights_authored", True))
|
||||||
|
_roles = SIDECAR_INFO.get("roles", {})
|
||||||
|
for _r in ("thigh", "shin", "foot", "toe"):
|
||||||
|
for _s in ("L", "R"):
|
||||||
|
_b = _roles.get(f"{_r}.{_s}")
|
||||||
|
if _b:
|
||||||
|
ROLE_LEGS[_s].add(_b)
|
||||||
|
|
||||||
bpy.ops.object.select_all(action="SELECT")
|
bpy.ops.object.select_all(action="SELECT")
|
||||||
bpy.ops.object.delete()
|
bpy.ops.object.delete()
|
||||||
@@ -193,6 +215,21 @@ if os.path.exists(SIDECAR):
|
|||||||
cloth.update(c["bones"])
|
cloth.update(c["bones"])
|
||||||
print(f" sidecar: {len(info.get('chains', []))} cloth chains "
|
print(f" sidecar: {len(info.get('chains', []))} cloth chains "
|
||||||
f"({len(cloth)} bones), {len(info.get('twist', []))} twist bones")
|
f"({len(cloth)} bones), {len(info.get('twist', []))} twist bones")
|
||||||
|
# A chain whose bones have no measurable extent is DEAD: the runtime drops
|
||||||
|
# any bone whose tip is shorter than a millimetre, so such a chain simulates
|
||||||
|
# nothing and the costume is welded solid — silently, because the sidecar
|
||||||
|
# still reports it. hikari shipped ten of them; her cosmetic bones are
|
||||||
|
# zero-length terminators from a rig that had been through two toolchains.
|
||||||
|
dead = 0
|
||||||
|
for c in info.get("chains", []):
|
||||||
|
ext = max((sum(v * v for v in t) ** 0.5 for t in c.get("tips", [])),
|
||||||
|
default=0.0)
|
||||||
|
if ext < 0.001:
|
||||||
|
dead += 1
|
||||||
|
if info.get("chains"):
|
||||||
|
check(dead == 0, "cloth chains have measurable extent",
|
||||||
|
f"{dead} of {len(info['chains'])} chains are zero-length and will "
|
||||||
|
f"not simulate", advisory=True)
|
||||||
else:
|
else:
|
||||||
warnings.append(f"no sidecar at {SIDECAR}")
|
warnings.append(f"no sidecar at {SIDECAR}")
|
||||||
|
|
||||||
@@ -202,6 +239,12 @@ else:
|
|||||||
LOCOMOTION = {"Idle", "Walk", "Run", "Sprint", "Jump", "Fall", "Land",
|
LOCOMOTION = {"Idle", "Walk", "Run", "Sprint", "Jump", "Fall", "Land",
|
||||||
"CrouchIdle", "CrouchWalk", "Dash"}
|
"CrouchIdle", "CrouchWalk", "Dash"}
|
||||||
|
|
||||||
|
# The thighs and shins by ROLE, so this works on any rig's spelling.
|
||||||
|
leg_bones = {b for s in ROLE_LEGS.values() for b in s}
|
||||||
|
if not leg_bones:
|
||||||
|
leg_bones = {b.name for b in arm.data.bones
|
||||||
|
if any(h in b.name.lower() for h in ("thigh", "shin"))}
|
||||||
|
|
||||||
frozen = []
|
frozen = []
|
||||||
legless = []
|
legless = []
|
||||||
keyed_cloth = set()
|
keyed_cloth = set()
|
||||||
@@ -217,8 +260,7 @@ for name, action in sorted(actions.items()):
|
|||||||
moved[b] = max(moved[b], max(vals) - min(vals))
|
moved[b] = max(moved[b], max(vals) - min(vals))
|
||||||
if max(moved.values(), default=0.0) < 0.005:
|
if max(moved.values(), default=0.0) < 0.005:
|
||||||
frozen.append(name)
|
frozen.append(name)
|
||||||
legs = max((v for b, v in moved.items()
|
legs = max((v for b, v in moved.items() if b in leg_bones), default=0.0)
|
||||||
if any(h in b.lower() for h in ("thigh", "shin"))), default=0.0)
|
|
||||||
if name in LOCOMOTION and legs < 0.01:
|
if name in LOCOMOTION and legs < 0.01:
|
||||||
legless.append(f"{name}({legs:.4f})")
|
legless.append(f"{name}({legs:.4f})")
|
||||||
|
|
||||||
|
|||||||