Feat/outline thickness and tp weapon hold #22

Merged
Dotts merged 43 commits from feat/outline-thickness-and-tp-weapon-hold into main 2026-07-27 23:22:53 -07:00
7 changed files with 452 additions and 102 deletions
Showing only changes of commit 682597a0d2 - Show all commits
@@ -166,35 +166,69 @@ a bad fit degrades to "stiff" rather than to "torn".
---
## 5. A single-piece rig still needs its arms checked
## 5. A weapon has to be scaled to the arm that holds it
**Symptom:** the gun is held, but not convincingly — the stock is not in the
shoulder and the hands are not on the grip. **Hit:** mannequin.
**Confidence: uncertain**its hand bone resolves, so this is not #2.
**Symptom:** hands flat and open, both fists bunched together at the grip, the
stock nowhere near the shoulder. **Hit:** every model.
**Confidence: certain**measured and fixed.
The rifle hold places the weapon from the shoulder joint and the aim direction,
then solves both arms onto the grip and foregrip with two-bone IK. It is tuned
against proportions like Taila's. A rig with different arm lengths, a different
rest pose (A-pose vs T-pose) or a different bone roll will put the hands
somewhere plausible for the maths and wrong for the eye.
Three separate causes, all of them "a constant where a measurement belonged".
**The real cause turned out to be #2's sibling, and it affected every model.**
`set_weapon` mounted the gun with a fixed `rotation_degrees = (0, 90, -90)`. A
bone attachment is expressed in the BONE's axes, and no two rigs agree on those,
so a constant there mounts the weapon differently on every character.
**The gun was mounted with a constant rotation.** `set_weapon` used
`rotation_degrees = (0, 90, -90)`. A bone attachment is expressed in the BONE's
axes and no two rigs agree on those, so one constant mounts the weapon
differently on every character. It never needed to be right: the pose layer aims
the gun by rotating the WRIST until the weapon's forward lies on the aim line, so
handing it the IDENTITY means "forward is the hand bone's -Z" — true by
construction on any rig — and the wrist absorbs the roll.
It does not need to be right at all. The pose layer aims the gun by rotating the
WRIST until the weapon's forward axis lies on the aim line, so handing it the
IDENTITY means "the gun's forward is the hand bone's -Z" — true by construction
on any rig — and the wrist absorbs whatever roll that bone happens to have. The
grip is placed at the bone's origin, so the gun sits *in* the hand instead of at
a fixed offset from a differently-oriented bone.
**The gun was full size on a stylised character.** The set is modelled at
real-world scale (an M4 is 0.84 m butt to muzzle); these characters have 0.47 m
arms against an adult 0.52. That puts the handguard 0.66 m from the support
shoulder — 0.2 m beyond reach — so a loop slid the support hand back down the
weapon until it fitted. On Taila a support offset authored at 0.35 m collapsed to
**0.083 m**: two fists together at the grip, which reads as a two-handed pistol
grip, not a rifle.
**Rule:** never express a mount as a constant in a bone's local frame. Either
derive the frame from the skeleton, or hand the downstream solver an identity and
let it do the work — it already knows where the gun must point.
The fix is not a fixed scale factor. The binding constraint is the SUPPORT arm:
its hand must reach `stock + fore` in front of the pocket, from a shoulder half a
shoulder-width off the weapon axis. Solve that triangle for the largest gun whose
handguard still lands inside the arm's reach. Taila and Kiyoko come out at
different scales from the same code, both with the support hand at its full
authored handguard distance and no sliding at all.
---
Also give the slide-back loop a FLOOR. A slightly straight support arm looks far
better than no handguard hold.
**Nothing posed the fingers.** Every hand was flat and open — the single loudest
tell that a character is not really holding anything. Fingers are now closed by
the pose layer, using an axis derived from each hand's OWN anatomy in the rest
pose, because no two rigs agree on finger bone orientation:
```
along wrist -> middle knuckle the length of the hand
palm middle knuckle -> thumb tip across it; the thumb OPPOSES the
fingers, so it is on the palm side by
construction — a fact about hands, not
a rig convention
curl along x palm turning about this swings the fingers
into the palm, not sideways
```
The trigger hand's index finger gets a much shallower curl than the rest — it
lies along the trigger. Curling it with the others is what makes a character look
like they are squeezing a bar of soap.
Finger bones now resolve by role too (`rig_map.DIGITS`), across all three naming
families met so far: Rigify `DEF-f_index.01.L`, VRoid `J_Bip_L_Index1`, and
Blender-export `IndexFinger1_L`. Segments are ordered by DEPTH BELOW THE HAND,
not by the number in the name — the numbering is not consistent between families,
but the hierarchy always runs knuckle to fingertip.
**Rule:** anything expressed as a constant in a rig's local frame — a mount
rotation, a grip offset, a curl axis, a weapon size — is a guess about one
skeleton. Derive it from the skeleton, or hand it to a solver that already knows
the answer.
## 6. Some sources are not salvageable, and the gate should say so
+59 -7
View File
@@ -13,20 +13,72 @@
],
"shin.R": "J_Bip_R_LowerLeg",
"shin.L": "J_Bip_L_LowerLeg",
"thigh.R": "J_Bip_R_UpperLeg",
"thigh.L": "J_Bip_L_UpperLeg",
"shoulder.L": "J_Bip_L_Shoulder",
"thigh.R": "J_Bip_R_UpperLeg",
"shoulder.R": "J_Bip_R_Shoulder",
"forearm.R": "J_Bip_R_LowerArm",
"shoulder.L": "J_Bip_L_Shoulder",
"forearm.L": "J_Bip_L_LowerArm",
"upper_arm.L": "J_Bip_L_UpperArm",
"forearm.R": "J_Bip_R_LowerArm",
"upper_arm.R": "J_Bip_R_UpperArm",
"toe.L": "J_Bip_L_ToeBase",
"upper_arm.L": "J_Bip_L_UpperArm",
"toe.R": "J_Bip_R_ToeBase",
"toe.L": "J_Bip_L_ToeBase",
"foot.L": "J_Bip_L_Foot",
"foot.R": "J_Bip_R_Foot",
"hand.L": "J_Bip_L_Hand",
"hand.R": "J_Bip_R_Hand"
"hand.R": "J_Bip_R_Hand",
"hand.L": "J_Bip_L_Hand"
},
"fingers": {
"thumb.L": [
"J_Bip_L_Thumb1",
"J_Bip_L_Thumb2",
"J_Bip_L_Thumb3"
],
"index.L": [
"J_Bip_L_Index1",
"J_Bip_L_Index2",
"J_Bip_L_Index3"
],
"middle.L": [
"J_Bip_L_Middle1",
"J_Bip_L_Middle2",
"J_Bip_L_Middle3"
],
"ring.L": [
"J_Bip_L_Ring1",
"J_Bip_L_Ring2",
"J_Bip_L_Ring3"
],
"pinky.L": [
"J_Bip_L_Little1",
"J_Bip_L_Little2",
"J_Bip_L_Little3"
],
"thumb.R": [
"J_Bip_R_Thumb1",
"J_Bip_R_Thumb2",
"J_Bip_R_Thumb3"
],
"index.R": [
"J_Bip_R_Index1",
"J_Bip_R_Index2",
"J_Bip_R_Index3"
],
"middle.R": [
"J_Bip_R_Middle1",
"J_Bip_R_Middle2",
"J_Bip_R_Middle3"
],
"ring.R": [
"J_Bip_R_Ring1",
"J_Bip_R_Ring2",
"J_Bip_R_Ring3"
],
"pinky.R": [
"J_Bip_R_Little1",
"J_Bip_R_Little2",
"J_Bip_R_Little3"
]
},
"chains": [
{
Binary file not shown.
+114 -62
View File
@@ -15,8 +15,8 @@
"shoulder.L": "DEF-shoulder.L",
"upper_arm.L": "DEF-upper_arm.L",
"upper_arm.R": "DEF-upper_arm.R",
"forearm.R": "DEF-forearm.R",
"forearm.L": "DEF-forearm.L",
"forearm.R": "DEF-forearm.R",
"thigh.R": "DEF-thigh.R",
"thigh.L": "DEF-thigh.L",
"foot.R": "DEF-foot.R",
@@ -28,6 +28,58 @@
"toe.L": "DEF-toe.L",
"toe.R": "DEF-toe.R"
},
"fingers": {
"thumb.L": [
"DEF-thumb.01.L",
"DEF-thumb.02.L",
"DEF-thumb.03.L"
],
"index.L": [
"DEF-f_index.01.L",
"DEF-f_index.02.L",
"DEF-f_index.03.L"
],
"middle.L": [
"DEF-f_middle.01.L",
"DEF-f_middle.02.L",
"DEF-f_middle.03.L"
],
"ring.L": [
"DEF-f_ring.01.L",
"DEF-f_ring.02.L",
"DEF-f_ring.03.L"
],
"pinky.L": [
"DEF-f_pinky.01.L",
"DEF-f_pinky.02.L",
"DEF-f_pinky.03.L"
],
"thumb.R": [
"DEF-thumb.01.R",
"DEF-thumb.02.R",
"DEF-thumb.03.R"
],
"index.R": [
"DEF-f_index.01.R",
"DEF-f_index.02.R",
"DEF-f_index.03.R"
],
"middle.R": [
"DEF-f_middle.01.R",
"DEF-f_middle.02.R",
"DEF-f_middle.03.R"
],
"ring.R": [
"DEF-f_ring.01.R",
"DEF-f_ring.02.R",
"DEF-f_ring.03.R"
],
"pinky.R": [
"DEF-f_pinky.01.R",
"DEF-f_pinky.02.R",
"DEF-f_pinky.03.R"
]
},
"chains": [
{
"class": "hair",
@@ -3414,10 +3466,10 @@
"DEF-skirt": 1.8893422721806523,
"DEF-skirt.003.seg2": 3.097181898448024,
"DEF-skirt.003.seg1": 1.512647745279899,
"DEF-skirt.003.seg3": 5.170624213956808,
"DEF-skirt.003.seg3": 5.169104631170351,
"DEF-skirt.007.seg3": 0.12245594475491542,
"DEF-skirt.005": 0.16033306364800098,
"DEF-skirt.seg2": 2.792140468584722,
"DEF-skirt.seg2": 2.786772327315499,
"DEF-skirt.009": 0.09372445010591768,
"DEF-skirt.L.seg1": 2.1407108254297182,
"DEF-skirt.007": 0.04866960387074015,
@@ -3429,8 +3481,8 @@
"DEF-skirt.L": 0.12044016764635926
},
{
"DEF-skirt.003.seg3": 5.211532068417141,
"DEF-skirt.seg3": 2.1638592614301637,
"DEF-skirt.003.seg3": 5.207492995592394,
"DEF-skirt.seg3": 2.161155452056163,
"DEF-skirt.007.seg2": 0.05601081196344071,
"DEF-skirt.L.seg3": 2.7192597439563233,
"DEF-skirt.007.seg1": 0.09495247018795672,
@@ -3438,7 +3490,7 @@
"DEF-skirt.005": 0.4334825864653958,
"DEF-skirt.L.004": 0.14029811314042995,
"DEF-skirt.009": 0.16869629844680606,
"DEF-skirt.seg1": 2.792140468584722,
"DEF-skirt.seg1": 2.786772327315499,
"DEF-skirt.011": 0.15320185432908007,
"DEF-skirt.007.seg3": 0.1889945161820596,
"DEF-skirt.009.seg1": 0.0714384640111776,
@@ -3452,14 +3504,14 @@
},
{
"DEF-skirt.002": 0.25142821495506595,
"DEF-skirt.011.seg1": 5.544001124575805,
"DEF-skirt.011.seg1": 5.570096570820647,
"DEF-skirt.L.001": 0.4132729913690881,
"DEF-skirt.011": 4.6961528319649295,
"DEF-skirt.003.seg3": 8.365425825133514,
"DEF-skirt.005.seg2": 4.61498402848288,
"DEF-skirt.009": 0.7582600522037454,
"DEF-skirt.009.seg1": 0.9144382488613035,
"DEF-skirt.seg2": 2.1638592614301637,
"DEF-skirt.seg2": 2.161155452056163,
"DEF-skirt.L.seg3": 4.3393369779026685,
"DEF-skirt.011.seg2": 2.8357110381653126,
"DEF-skirt.006": 0.1217170026805725,
@@ -3760,14 +3812,14 @@
"DEF-skirt.009": 1.1286794898007495,
"DEF-skirt.009.seg1": 0.8940016644600399,
"DEF-skirt.011.seg1": 1.3719167847603355,
"DEF-skirt.seg2": 5.211532068417141,
"DEF-skirt.seg2": 5.207492995592394,
"DEF-skirt.007.seg2": 1.980115868442557,
"DEF-skirt.011": 1.6883411692987111,
"DEF-skirt.007.seg1": 2.9822736639827294,
"DEF-skirt.L.seg3": 0.5467618653323603,
"DEF-skirt": 1.595806262183024,
"DEF-skirt.003.seg2": 2.4862878647459423,
"DEF-skirt.seg1": 5.170624213956808,
"DEF-skirt.seg1": 5.169104631170351,
"DEF-skirt.007.seg3": 2.7651239767046945,
"DEF-skirt.009.seg2": 0.18218562382336984,
"DEF-skirt.R.seg2": 0.02413577489838903,
@@ -3809,9 +3861,9 @@
-0.0
],
[
-0.01201,
0.14512,
0.03219
-0.01353,
0.14671,
0.03405
]
],
"hulls": [
@@ -3973,9 +4025,9 @@
],
[
[
-0.07886,
0.14877,
0.10408
-0.07888,
0.14866,
0.10517
],
[
0.02098,
@@ -4072,21 +4124,21 @@
"DEF-skirt.L.002": 0.10102962666415694,
"DEF-skirt.L.001.seg1": 0.19093190472623134,
"DEF-skirt.L.005": 0.04318668795316161,
"DEF-skirt.L.004": 3.7163001012175743,
"DEF-skirt.L.004": 3.8285142497834164,
"DEF-skirt.011.seg3": 4.602221938080493,
"DEF-skirt.011.seg2": 3.703707791421389,
"DEF-skirt.L.004.seg1": 0.9578381959761514,
"DEF-skirt.011.seg2": 3.722823119417367,
"DEF-skirt.L.004.seg1": 0.9764221426855458,
"DEF-skirt.L.003": 3.6402760699503562,
"DEF-skirt.L.seg2": 3.686576538966275,
"DEF-skirt.L.001": 1.7430155018692777,
"DEF-skirt.seg3": 4.3393369779026685,
"DEF-skirt.seg2": 2.7192597439563233,
"DEF-skirt.003.seg3": 0.5467618653323603,
"DEF-skirt.L.003.seg3": 1.4767131363706594,
"DEF-skirt.L.003.seg2": 0.739566607477287,
"DEF-skirt.L.003.seg3": 1.533915214427354,
"DEF-skirt.L.003.seg2": 0.7716510583968516,
"DEF-skirt.005.seg2": 0.03759796665737425,
"DEF-skirt.011.seg1": 0.6000672181866031,
"DEF-skirt.L.003.seg1": 0.7168149637479433,
"DEF-skirt.011.seg1": 0.6236902469398551,
"DEF-skirt.L.003.seg1": 0.7488992127140781,
"DEF-skirt.011": 0.13239584627096912,
"DEF-skirt.003.seg2": 0.024977540935678944
}
@@ -4118,9 +4170,9 @@
0.0
],
[
0.03615,
0.14155,
-0.03115
0.0279,
0.14363,
-0.02295
]
],
"hulls": [
@@ -4287,9 +4339,9 @@
0.17881
],
[
0.19114,
0.03703,
-0.18664
0.18049,
0.03004,
-0.17856
],
[
-0.044,
@@ -4297,9 +4349,9 @@
0.02135
],
[
0.06809,
0.11583,
-0.07723
0.06877,
0.11585,
-0.07637
],
[
0.08415,
@@ -4356,10 +4408,10 @@
{
"DEF-skirt.L.003": 6.056427661191389,
"DEF-skirt.R.003": 3.5362091427452844,
"DEF-skirt.L.003.seg2": 1.4336744309317115,
"DEF-skirt.L.003.seg2": 1.4465578586045362,
"DEF-skirt.R.003.seg1": 0.0075091120423149416,
"DEF-skirt.L.004": 0.28033761075145813,
"DEF-skirt.L.seg3": 0.7168149637479433,
"DEF-skirt.L.004": 0.3003293425343978,
"DEF-skirt.L.seg3": 0.7488992127140781,
"DEF-skirt.L.seg2": 0.1957218245564052,
"DEF-skirt.L.seg1": 0.6595836843532097,
"DEF-skirt.L": 1.5219704851834268,
@@ -4368,10 +4420,10 @@
{
"DEF-skirt.L.003.seg3": 1.7624733424013248,
"DEF-skirt.R.003": 5.005937962569201,
"DEF-skirt.L.004": 0.6573735970229051,
"DEF-skirt.L.seg3": 0.739566607477287,
"DEF-skirt.L.004": 0.677365454643255,
"DEF-skirt.L.seg3": 0.7716510583968516,
"DEF-skirt.L.seg2": 0.19439024437881924,
"DEF-skirt.L.003.seg1": 1.4336744309317115,
"DEF-skirt.L.003.seg1": 1.4465578586045362,
"DEF-skirt.R.003.seg1": 0.1158529500045482,
"DEF-skirt.L": 0.9837299947917074,
"DEF-skirt.L.seg1": 0.322305533592365,
@@ -4389,8 +4441,8 @@
"DEF-skirt.R.003.seg3": 5.539891594630126,
"DEF-skirt.L.004.seg1": 7.552187895272386,
"DEF-skirt.R.004": 1.6662561376102858,
"DEF-skirt.L.004": 5.172512625961911,
"DEF-skirt.L.seg3": 1.4767131363706594,
"DEF-skirt.L.004": 5.274030080809885,
"DEF-skirt.L.seg3": 1.533915214427354,
"DEF-skirt.L.seg2": 0.009721631050302243,
"DEF-skirt.R.004.seg1": 0.2699483450459348,
"DEF-skirt.R.003.seg2": 1.2716413510540925,
@@ -4650,7 +4702,7 @@
"DEF-skirt.007.seg1": 0.7598426687282531,
"DEF-skirt.R.seg1": 3.2886585052114654,
"DEF-skirt.007.seg3": 0.5922937660442745,
"DEF-skirt.R.003.seg1": 0.9644485221168745,
"DEF-skirt.R.003.seg1": 0.958425160444825,
"DEF-skirt.003.seg3": 0.058546876452437446,
"DEF-skirt.007.seg2": 0.17227260051985324,
"DEF-skirt.L": 0.7833441613586325,
@@ -4665,7 +4717,7 @@
"DEF-skirt.R.seg2": 3.9264200827331246,
"DEF-skirt.007.seg3": 5.990418387665851,
"DEF-skirt.003.seg3": 0.07982489265778714,
"DEF-skirt.R.003.seg1": 2.6460365216903683,
"DEF-skirt.R.003.seg1": 2.624934006784733,
"DEF-skirt.R": 3.2886585052114654,
"DEF-skirt.R.003.seg2": 0.26822713323056313,
"DEF-skirt.R.003": 1.2574131429974016,
@@ -4930,9 +4982,9 @@
"DEF-skirt.L.003.seg2": 0.1158529500045482,
"DEF-skirt.L.003.seg1": 0.0075091120423149416,
"DEF-skirt.L.003.seg3": 1.7362462282224314,
"DEF-skirt.R.003.seg2": 2.396364732019504,
"DEF-skirt.R.seg1": 2.6460365216903683,
"DEF-skirt.R": 0.9644485221168745,
"DEF-skirt.R.003.seg2": 2.390378788205351,
"DEF-skirt.R.seg1": 2.624934006784733,
"DEF-skirt.R": 0.958425160444825,
"DEF-skirt.R.003": 1.0807317991880538,
"DEF-skirt.R.seg2": 0.04449534258909127
},
@@ -4941,7 +4993,7 @@
"DEF-skirt.R.seg2": 0.7907999176082137,
"DEF-skirt.R.001": 0.15603431496875952,
"DEF-skirt.L.003.seg3": 1.2716413510540925,
"DEF-skirt.R.003.seg1": 2.396364732019504,
"DEF-skirt.R.003.seg1": 2.390378788205351,
"DEF-skirt.R.004": 0.016111401937035363,
"DEF-skirt.R.seg1": 0.26822713323056313,
"DEF-skirt.R": 0.0144911590928406
@@ -5629,31 +5681,31 @@
},
{
"DEF-skirt.011": 4.58106049623973,
"DEF-skirt.011.seg2": 7.19317806733265,
"DEF-skirt.011.seg2": 7.206079765921628,
"DEF-skirt.L.002": 0.02658579166615116,
"DEF-skirt.002.seg2": 0.012493393667064645,
"DEF-skirt.002.seg1": 0.07562430091019157,
"DEF-skirt.L.001": 1.879150088933239,
"DEF-skirt.L.001.seg1": 0.060376459628942836,
"DEF-skirt.002": 1.0432309729624893,
"DEF-skirt.seg3": 5.544001124575805,
"DEF-skirt.seg3": 5.570096570820647,
"DEF-skirt.003.seg3": 1.3719167847603355,
"DEF-skirt.005.seg2": 3.0893325526066313,
"DEF-skirt.005.seg1": 1.0448083213115291,
"DEF-skirt.L.seg3": 0.6000672181866031,
"DEF-skirt.L.seg3": 0.6236902469398551,
"DEF-skirt.005.seg3": 0.031181913198806976
},
{
"DEF-skirt.011.seg1": 7.19317806733265,
"DEF-skirt.011.seg1": 7.206079765921628,
"DEF-skirt.L.002": 0.1453999495240184,
"DEF-skirt.002.seg3": 0.11034352255838153,
"DEF-skirt.011.seg3": 2.7509932421896424,
"DEF-skirt.011.seg3": 2.7529092791157663,
"DEF-skirt.002.seg2": 0.09621669328872662,
"DEF-skirt.L.001.seg1": 0.8697016458385258,
"DEF-skirt.002.seg1": 0.5566789920191999,
"DEF-skirt.002": 0.6140337423742315,
"DEF-skirt.L.001": 1.0570456406771491,
"DEF-skirt.L.seg3": 3.703707791421389,
"DEF-skirt.L.seg3": 3.722823119417367,
"DEF-skirt.seg3": 2.8357110381653126,
"DEF-skirt.005.seg2": 1.4663347373091402,
"DEF-skirt.005.seg1": 0.06153290296288072,
@@ -5666,7 +5718,7 @@
"DEF-skirt.L.002": 0.2979373224340631,
"DEF-skirt.002.seg3": 0.8674111035481489,
"DEF-skirt.L.001.seg1": 0.43628207869348157,
"DEF-skirt.011.seg2": 2.7509932421896424,
"DEF-skirt.011.seg2": 2.7529092791157663,
"DEF-skirt.L.seg3": 4.602221938080493,
"DEF-skirt.seg3": 0.14448731559887507,
"DEF-skirt.005.seg2": 0.06419735157119555,
@@ -6389,7 +6441,7 @@
"DEF-skirt.R.003.seg3": 0.1715765989223872,
"DEF-skirt.R.004": 0.030876031930140435,
"DEF-skirt.R.004.seg1": 0.020803836908132034,
"DEF-skirt.L.004": 0.22907211825039514,
"DEF-skirt.L.004": 0.22954852875263643,
"DEF-skirt.L.004.seg1": 1.117666014626619,
"DEF-skirt.R.005": 0.00681448318365796
},
@@ -6609,32 +6661,32 @@
],
"neighbours": [
{
"DEF-skirt.L.004.seg1": 4.0505432754151975,
"DEF-skirt.L.005": 0.22907211825039514,
"DEF-skirt.L.004.seg1": 4.0678154269769955,
"DEF-skirt.L.005": 0.22954852875263643,
"DEF-skirt.L.001.seg2": 0.6736723993011002,
"DEF-skirt.L.002": 0.808795880142356,
"DEF-skirt.L.001.seg1": 0.8051463519527622,
"DEF-skirt.L.seg3": 3.7163001012175743,
"DEF-skirt.L.seg3": 3.8285142497834164,
"DEF-skirt.L.003": 0.21646333191305667,
"DEF-skirt.L.seg2": 0.06935733543747569,
"DEF-skirt.L.003.seg3": 5.172512625961911,
"DEF-skirt.L.003.seg3": 5.274030080809885,
"DEF-skirt.R.003.seg3": 0.845388225157766,
"DEF-skirt.L.003.seg2": 0.6573735970229051,
"DEF-skirt.L.003.seg2": 0.677365454643255,
"DEF-skirt.seg3": 0.8155590405608446,
"DEF-skirt": 0.19603096959511881,
"DEF-skirt.seg2": 0.14029811314042995,
"DEF-skirt.L.003.seg1": 0.28033761075145813,
"DEF-skirt.L.003.seg1": 0.3003293425343978,
"DEF-skirt.R.004": 0.13967539384350525,
"DEF-skirt.seg1": 0.20676807425859
},
{
"DEF-skirt.L.004": 4.0505432754151975,
"DEF-skirt.L.004": 4.0678154269769955,
"DEF-skirt.L.005": 1.117666014626619,
"DEF-skirt.R.003.seg3": 1.9171418898471628,
"DEF-skirt.L.004.seg2": 4.7309382362110455,
"DEF-skirt.L.003.seg3": 7.552187895272386,
"DEF-skirt.R.004": 1.5419089252454603,
"DEF-skirt.L.seg3": 0.9578381959761514,
"DEF-skirt.L.seg3": 0.9764221426855458,
"DEF-skirt.R.004.seg1": 0.07202823700664285,
"DEF-skirt.seg3": 0.029836616482513456
},
+167 -9
View File
@@ -216,6 +216,7 @@ func load_model(path: String) -> void:
_pose_mod = ShooterPoseModifier.new()
_pose_mod.joint_helpers = _joint_helpers
_pose_mod.roles = _rig_info.get("roles", {})
_pose_mod.fingers = _rig_info.get("fingers", {})
_pose_mod.name = "ShooterPose"
skeleton.add_child(_pose_mod)
# Cloth and hair last, so the springs react to the FINAL body pose —
@@ -976,14 +977,52 @@ func get_muzzle_node() -> Node3D:
## viewmodel's hands at GRIP_LOCAL and SUPPORT_LOCAL in weapon space, and every
## weapon sets muzzle_flash.position at its barrel tip. Third person simply
## reaches for the same points the viewmodel does.
## How far a weapon may be scaled down before it reads as a toy.
const MIN_WEAPON_SCALE := 0.55
## Forward offset of the stock pocket from the shoulder joint — see POCKET_HIP.
const POCKET_FORWARD := 0.06
## Scale the weapon so this character can actually reach its handguard.
##
## Not against a fixed reference size: against the arm that has to hold it. The
## set is modelled at real-world scale — an M4 is 0.84 m butt to muzzle — and
## these characters are stylised, with arms around 0.47 m against an adult 0.52.
##
## The binding constraint is the SUPPORT arm. Its hand has to reach a point
## (stock + fore) in front of the shoulder pocket, from a shoulder half the
## shoulder-width off the weapon's axis. Solve that triangle for the largest gun
## whose handguard still lands inside the arm's reach, and the support hand stops
## being dragged back down the barrel.
##
## Measured on Taila before this: a support offset authored at 0.35 m collapsed
## to 0.083 m, which puts the support fist on top of the trigger fist — a
## two-handed pistol grip, not a rifle. That is what "the hands are not on the
## rails" was.
func _weapon_scale(span: float) -> float:
if not _pose_mod or not skeleton or span <= 0.01:
return 1.0
var reach: float = _pose_mod._arm_reach(skeleton, "DEF-upper_arm.L",
"DEF-forearm.L", "DEF-hand.L") * 0.94
if reach <= 0.01:
return 1.0
var ua_l: int = _pose_mod._idx.get("DEF-upper_arm.L", -1)
var ua_r: int = _pose_mod._idx.get("DEF-upper_arm.R", -1)
var lateral := 0.14
if ua_l >= 0 and ua_r >= 0:
# The weapon rides near the right shoulder, so the support arm crosses
# about half the shoulder width to get to it.
lateral = skeleton.get_bone_global_rest(ua_l).origin.distance_to(
skeleton.get_bone_global_rest(ua_r).origin) * 0.5
var forward := sqrt(maxf(reach * reach - lateral * lateral, 0.01))
return clampf((forward - POCKET_FORWARD) / span, MIN_WEAPON_SCALE, 1.0)
func _measure_weapon(w: Node3D) -> void:
var grip: Vector3 = WeaponGrips.GRIP
var support: Vector3 = WeaponGrips.SUPPORT
var fwd := Vector3(0, 0, -1) # the weapon's own muzzle axis
# Put the GRIP — not the model origin — in the fist.
w.position -= w.transform.basis * grip
# Barrel length, grip to muzzle. Every weapon marks its own barrel tip.
var muzzle_dist: float = WeaponGrips.DEFAULT_MUZZLE_DIST
if "muzzle_flash" in w and w.muzzle_flash:
@@ -995,12 +1034,20 @@ func _measure_weapon(w: Node3D) -> void:
# front of the barrel with nothing to hold, which is why some weapons did
# not look supported by the arms.
var fore: float = absf((support - grip).dot(fwd))
_pose_mod.gun_fore = clampf(minf(fore, muzzle_dist * 0.8), 0.12, 0.45)
var fore_u: float = clampf(minf(fore, muzzle_dist * 0.8), 0.12, 0.45)
var stock_u: float = clampf(muzzle_dist * 0.5, 0.10, 0.40)
# Size the gun to the arm that has to hold it, then seat the grip in the fist.
var k := _weapon_scale(stock_u + fore_u)
w.scale = Vector3.ONE * k
w.position -= w.transform.basis * grip
_pose_mod.gun_fore = fore_u * k
# Stock: not authored anywhere, so derive it from the barrel. Half the
# grip-to-muzzle distance behind the grip lands the butt in the shoulder
# pocket for every gun in the set.
_pose_mod.gun_stock = clampf(muzzle_dist * 0.5, 0.10, 0.40)
_pose_mod.gun_stock = stock_u * k
# ── Helpers ───────────────────────────────────────────────────────────────────
@@ -1123,6 +1170,10 @@ class ShooterPoseModifier extends SkeletonModifier3D:
## the model was rebound onto the library skeleton, where the names below
## already match.
var roles: Dictionary = {}
## "index.L" -> its bone names, knuckle to fingertip, from the same sidecar.
var fingers: Dictionary = {}
var _fing: Dictionary = {} # same, resolved to bone indices
var _curl: Dictionary = {} # "L"/"R" -> curl axis in the rest frame
func _resolve() -> void:
var skel := get_skeleton()
@@ -1153,8 +1204,62 @@ class ShooterPoseModifier extends SkeletonModifier3D:
if b < 0:
b = skel.find_bone(n)
_idx[n] = b
_resolve_hands(skel)
_resolved = true
## Finger bones, and the axis a finger closes about, per hand.
##
## The axis is derived from the hand's OWN ANATOMY in the rest pose, because
## no two rigs agree on finger bone orientation and a constant would close
## one character's hand and splay another's:
##
## along wrist -> middle knuckle, the length of the hand
## palm middle knuckle -> thumb tip, across it. The thumb opposes the
## fingers, so it is on the palm side by construction — which is
## true of a hand, not of a rig convention.
## curl along x palm, so turning about it swings the fingers into the
## palm rather than sideways or backwards.
func _resolve_hands(skel: Skeleton3D) -> void:
for key in fingers:
var list := PackedInt32Array()
for n in fingers[key]:
var b := skel.find_bone(String(n))
if b >= 0:
list.append(b)
if not list.is_empty():
_fing[key] = list
for side in ["L", "R"]:
var hand: int = _idx.get("DEF-hand." + side, -1)
var mid: PackedInt32Array = _fing.get("middle." + side, PackedInt32Array())
if hand < 0 or mid.is_empty():
continue
var wrist := skel.get_bone_global_rest(hand).origin
var knuckle := skel.get_bone_global_rest(mid[0]).origin
var along := knuckle - wrist
if along.length() < 0.0001:
continue
along = along.normalized()
var palm := Vector3.ZERO
var thumb: PackedInt32Array = _fing.get("thumb." + side, PackedInt32Array())
if not thumb.is_empty():
palm = skel.get_bone_global_rest(thumb[thumb.size() - 1]).origin - knuckle
else:
# No thumb on this rig: fall back to the knuckle line, which
# gives a plane but not a side, so the sign may be wrong.
var ix: PackedInt32Array = _fing.get("index." + side, PackedInt32Array())
var pk: PackedInt32Array = _fing.get("pinky." + side, PackedInt32Array())
if ix.is_empty() or pk.is_empty():
continue
palm = (skel.get_bone_global_rest(pk[0]).origin
- skel.get_bone_global_rest(ix[0]).origin).cross(along)
palm -= along * palm.dot(along)
if palm.length() < 0.0001:
continue
_curl[side] = along.cross(palm.normalized()).normalized()
if OS.has_environment("HAND_DEBUG"):
print("HANDS fingers=%d resolved=%d curl=%s" % [
fingers.size(), _fing.size(), _curl])
func _process_modification() -> void:
var skel := get_skeleton()
if not skel:
@@ -1179,6 +1284,7 @@ class ShooterPoseModifier extends SkeletonModifier3D:
_apply_grapple(skel)
if _hold_r > 0.01 or _hold_l > 0.01:
_apply_rifle_hold(skel)
_close_hands(skel)
if recoil > 0.01:
_apply_recoil(skel)
recoil = lerpf(recoil, 0.0, 0.25)
@@ -1398,12 +1504,20 @@ class ShooterPoseModifier extends SkeletonModifier3D:
var l_sh := skel.get_bone_global_pose(ua_l).origin
var reach_l := _arm_reach(skel, "DEF-upper_arm.L", "DEF-forearm.L",
"DEF-hand.L") * 0.94
for _i in 5:
# Slide the support hand back down the handguard until the arm can
# reach it — but NEVER past this floor. Without one it collapsed to
# 0.083 m on Taila, which puts the support fist on top of the trigger
# fist: that reads as a two-handed pistol grip, not a rifle. A support
# arm that is nearly straight looks far better than no handguard hold.
var floor_fore: float = gun_fore * 0.55
for _i in 6:
if grip_pos.distance_to(l_sh) > reach_l:
break # even the grip is out of reach; nothing to slide to
if (grip_pos + aim_dir * fore_dist).distance_to(l_sh) <= reach_l:
break
fore_dist *= 0.75
fore_dist = maxf(fore_dist * 0.8, floor_fore)
if fore_dist <= floor_fore:
break
var fore_pos := grip_pos + aim_dir * fore_dist
# 3. Support hand goes to the mag well during a reload (under the
@@ -1432,8 +1546,9 @@ class ShooterPoseModifier extends SkeletonModifier3D:
if OS.has_environment("GUN_POSE_DEBUG"):
var hr: int = _idx.get("DEF-hand.R", -1)
var hl: int = _idx.get("DEF-hand.L", -1)
print("HOLD ads=%.2f holdR=%.2f holdL=%.2f rl=%.2f fore=%.3f stock=%.3f foredist=%.3f" % [
ads, _hold_r, _hold_l, reload_phase, gun_fore, gun_stock, fore_dist],
print("HOLD st=%s tgtL=%.2f ads=%.2f holdR=%.2f holdL=%.2f rl=%.2f fore=%.3f stock=%.3f foredist=%.3f" % [
state, hold_l_target, ads, _hold_r, _hold_l, reload_phase,
gun_fore, gun_stock, fore_dist],
" sh=", shoulder, " grip=", grip_pos, " fore=", fore_pos,
" handR=", skel.get_bone_global_pose(hr).origin if hr >= 0 else "-",
" handL=", skel.get_bone_global_pose(hl).origin if hl >= 0 else "-",
@@ -1469,6 +1584,49 @@ class ShooterPoseModifier extends SkeletonModifier3D:
* Quaternion(fa_rest_dir, aim_dir) * hand_rest_q
_set_global_rot(skel, hand_l, g_fa_l, g_hand, _hold_l)
# How far each segment of a finger closes, knuckle -> tip, in radians.
#
# Two different grips. The SUPPORT hand wraps a handguard, so all four
# fingers close hard and evenly. The TRIGGER hand wraps a pistol grip with
# three fingers while the index lies along the trigger, nearly straight —
# curling it with the rest is the single thing that most makes a game
# character look like they are squeezing a bar of soap rather than holding a
# rifle.
const CURL_WRAP := [0.85, 1.15, 0.85]
const CURL_TRIGGER := [0.42, 0.55, 0.35]
# The thumb opposes rather than curls, so it closes less and it is the one
# digit whose middle joint barely bends.
const CURL_THUMB := [0.45, 0.30, 0.25]
const DIGITS := ["index", "middle", "ring", "pinky"]
## Close both hands around whatever the IK put them on.
##
## Applied AFTER the hold, so the fingers follow wherever the wrists ended up.
## Weighted by the per-arm hold blend, so a hand the clip has taken back opens
## again instead of staying clamped shut.
func _close_hands(skel: Skeleton3D) -> void:
_close_hand(skel, "R", _hold_r)
_close_hand(skel, "L", _hold_l)
func _close_hand(skel: Skeleton3D, side: String, w: float) -> void:
if w <= 0.01 or not _curl.has(side):
return
var axis: Vector3 = _curl[side]
for digit in DIGITS:
var bones: PackedInt32Array = _fing.get(digit + "." + side,
PackedInt32Array())
if bones.is_empty():
continue
# The right index rides the trigger; everything else wraps.
var amount: Array = CURL_TRIGGER if (side == "R" and digit == "index") else CURL_WRAP
for i in bones.size():
var a: float = amount[i] if i < amount.size() else amount[-1]
_add_space(skel, bones[i], Quaternion(axis, a * w))
var thumb: PackedInt32Array = _fing.get("thumb." + side, PackedInt32Array())
for i in thumb.size():
var a: float = CURL_THUMB[i] if i < CURL_THUMB.size() else CURL_THUMB[-1]
_add_space(skel, thumb[i], Quaternion(axis, a * w))
## Straight-arm length of an arm chain, from the rest pose.
func _arm_reach(skel: Skeleton3D, ua_name: String, fa_name: String,
hand_name: String) -> float:
+8 -1
View File
@@ -1033,7 +1033,14 @@ def describe_rig(arm, roles, mapping, meshes):
if b.parent is not None:
twist.append({"bone": b.name, "parent": b.parent.name,
"child": b.children[0].name if b.children else None})
return {"roles": roles_out, "chains": chains, "twist": twist,
# Fingers, so the runtime can close a hand around a grip without knowing
# how this rig spells "index". Ordered knuckle -> fingertip.
finger_out = {}
for (digit, side), bones in roles.fingers.items():
finger_out["%s.%s" % (digit, side)] = bones
return {"roles": roles_out, "fingers": finger_out,
"chains": chains, "twist": twist,
"colliders": _leg_colliders(
arm, roles, _dominant_vertices(meshes, arm, min_weight=0.6),
_body_points(meshes)),
+47
View File
@@ -58,6 +58,20 @@ LIMB_ROLES = {
# Longest-first inside each role, so "upperarm" is tried before "arm".
LIMB_ORDER = ["toe", "foot", "shin", "thigh", "shoulder", "hand", "forearm", "upper_arm"]
# digit -> spellings. Every rig met so far uses one of three families:
# Rigify DEF-f_index.01.L canon "findex01l"
# VRoid J_Bip_L_Index1 canon "index1"
# Blender IndexFinger1_L canon "indexfinger1l"
# so a substring test on the canon form covers all of them. The pinky is called
# "little" on VRoid and half the Blender exports.
DIGITS = {
"thumb": ("thumb",),
"index": ("index",),
"middle": ("middle",),
"ring": ("ring",),
"pinky": ("pinky", "little"),
}
_PREFIXES = re.compile(
r"^(def[-_]|org[-_]|mch[-_]|ctrl[-_]|mixamorig\d*[:_]|bip\d*[-_ ]|"
r"j_bip_[clr]_|j_sec_[clr]_|valvebiped\.|bone_|b_)", re.I)
@@ -143,6 +157,7 @@ class RigRoles:
self.bones = {b.name: b for b in arm.data.bones}
self.names = set(self.bones)
self.limb = {} # ("thigh", "L") -> bone name
self.fingers = {} # ("index", "L") -> [proximal, middle, distal]
self.spine = [] # hips-exclusive chain, ordered, ending on the head
self.hips = None
self.head = None
@@ -150,6 +165,7 @@ class RigRoles:
self._resolve_limbs()
self._fill_limbs_by_anatomy()
self._resolve_spine()
self._resolve_fingers()
# ---------------------------------------------------------------- limbs
@@ -234,6 +250,37 @@ class RigRoles:
continue
self.limb[(role, side)] = name
# -------------------------------------------------------------- fingers
def _resolve_fingers(self):
"""digit+side -> its bones, ordered from the knuckle outwards.
Ordered by DEPTH BELOW THE HAND rather than by the number in the name.
The numbering is not consistent — Rigify counts .01/.02/.03, VRoid counts
1/2/3, and some exports number from the tip — but the hierarchy always
runs knuckle to fingertip, so walking it is the only spelling-independent
way to know which segment is which.
"""
for side in ("L", "R"):
hand = self.limb.get(("hand", side))
if not hand or hand not in self.bones:
continue
depth = {}
def walk(b, d):
depth[b.name] = d
for c in b.children:
walk(c, d + 1)
for c in self.bones[hand].children:
walk(c, 0)
for digit, stems in DIGITS.items():
got = [n for n in depth
if any(st in canon(n) for st in stems)
and side_of(n) == side]
if got:
self.fingers[(digit, side)] = sorted(got, key=lambda n: depth[n])
# ---------------------------------------------------------------- spine
def _resolve_spine(self):