diff --git a/.claude/skills/character-pipeline/references/failure-modes.md b/.claude/skills/character-pipeline/references/failure-modes.md index e9b432c..1845972 100644 --- a/.claude/skills/character-pipeline/references/failure-modes.md +++ b/.claude/skills/character-pipeline/references/failure-modes.md @@ -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 diff --git a/assets/characters/skins/kiyoko.rig.json b/assets/characters/skins/kiyoko.rig.json index b40588d..6ddda8f 100644 --- a/assets/characters/skins/kiyoko.rig.json +++ b/assets/characters/skins/kiyoko.rig.json @@ -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": [ { diff --git a/assets/characters/skins/taila.glb b/assets/characters/skins/taila.glb index 6defd97..49d45d1 100644 Binary files a/assets/characters/skins/taila.glb and b/assets/characters/skins/taila.glb differ diff --git a/assets/characters/skins/taila.rig.json b/assets/characters/skins/taila.rig.json index 371cd06..1042be5 100644 --- a/assets/characters/skins/taila.rig.json +++ b/assets/characters/skins/taila.rig.json @@ -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 }, diff --git a/characters/skinned_player_model.gd b/characters/skinned_player_model.gd index aceca8f..a69bd28 100644 --- a/characters/skinned_player_model.gd +++ b/characters/skinned_player_model.gd @@ -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: diff --git a/tools/retarget.py b/tools/retarget.py index b79fc2f..af1a699 100644 --- a/tools/retarget.py +++ b/tools/retarget.py @@ -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)), diff --git a/tools/rig_map.py b/tools/rig_map.py index aa5174a..968b4a1 100644 --- a/tools/rig_map.py +++ b/tools/rig_map.py @@ -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):