From 682597a0d2fe0e98c7f388ca2239af441ea2f670 Mon Sep 17 00:00:00 2001 From: Nicholas Butzke <137417822+DottsGit@users.noreply.github.com> Date: Sun, 26 Jul 2026 19:02:11 -0400 Subject: [PATCH] feat(weapons): scale the gun to the arm, and close the hands around it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hold was wrong on every character in three independent ways, all of them a constant standing where a measurement belonged. MOUNT. set_weapon seated the weapon with 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 mounted the gun differently on every model. It never needed to be right — the pose layer aims by rotating the wrist until the weapon's forward lies on the aim line, so the identity means "forward is the hand bone's -Z", true on any rig, and the wrist absorbs the roll. SIZE. The set is modelled at real-world scale; an M4 is 0.84 m butt to muzzle and these characters have 0.47 m arms against an adult 0.52. That put the handguard 0.66 m from the support shoulder, 0.2 m past reach, so the support hand was slid back down the weapon until it fitted — on Taila from an authored 0.35 m to 0.083 m, which puts both fists together at the grip. Two hands on a pistol, not a rifle. Fixed by solving the support arm's triangle rather than picking a factor: its hand must reach stock+fore ahead of the pocket from a shoulder half a shoulder-width off the axis, so scale the gun to the largest that keeps the handguard inside that reach. Taila and Kiyoko now come out at their own scales (0.217 and 0.213 m of hand separation) with the support hand at its FULL authored handguard distance and the slide-back loop never firing. The loop also has a floor now: a straight support arm beats no handguard hold. FINGERS. Nothing posed them — every hand was flat and open, which is the loudest possible tell that a character is not holding anything. They close now, about an axis derived from each hand's own anatomy in the rest pose: along = wrist to middle knuckle, palm = middle knuckle to thumb tip (the thumb opposes the fingers, so it marks the palm side by construction), curl = along x palm. The trigger finger gets a much shallower curl than the rest, because it lies along the trigger. Finger bones resolve by ROLE across all three naming families met so far — Rigify DEF-f_index.01.L, VRoid J_Bip_L_Index1, Blender IndexFinger1_L — ordered by depth below the hand rather than by the number in the name, which is not consistent between them. Verified by render on Taila and by measurement on Kiyoko: stock at the shoulder, trigger hand on the grip, support hand out on the handguard, fingers wrapped, arms not crossing. Smoke 0 failures. Co-Authored-By: Claude Opus 5 --- .../references/failure-modes.md | 80 +++++--- assets/characters/skins/kiyoko.rig.json | 66 ++++++- assets/characters/skins/taila.glb | Bin 2886040 -> 2886040 bytes assets/characters/skins/taila.rig.json | 176 ++++++++++++------ characters/skinned_player_model.gd | 176 +++++++++++++++++- tools/retarget.py | 9 +- tools/rig_map.py | 47 +++++ 7 files changed, 452 insertions(+), 102 deletions(-) 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 6defd9770d8e25ef9df1aa7b4103aacc6ca7bf49..49d45d1f59f70c10e5467b807e1f688d53cdc82f 100644 GIT binary patch delta 462 zcmWN}Pe_wt90%}TeY0n3KIW!oF89aEa%&kFL~Os8g@vM`hd~|$=~6L8il80FQ1Hb@ zJCxISUJIdgBGjT3>Js>l6vB%Leh;7PN1Oa2X)vlvYLQy3%IZnA zL_M|4tO@ZhCd;sLt)yMy}Gyw9^~QJ&7UQGUTqzr?UUxXl(TxH&mR<-2i; zq#shdsYAF7=I7}-i;_(RXm$Ax9TaIIY_JKjmy#=mwZK}g?Ks!Snw_OvucqmG-D7X$ z!~(7NyrtTuE_(9GL$}8o6o2$SecsObqW=a&xPbHM<}Tfxh?D=XmqH~k^-O{7S8@H3 zO%Wrl6l%-)=AM0foiRChpqDE_7NfY`O#Q*^%Av{O;t$qplq1(TDJ|2!_g+o#L;34- z!)=06D1&l14Hdwl63##sn85%(ETThG(J1f>^dh`u(_q7u@(_t>iywd8RkVnIt*o|6A1V8={>DUDP$6_6E1@>Z8_Xs{G8UJu;woAFz1~|1~>D(`$ZO zG5BdeaZ|XH>^2V^%WAf}gKUmQ@`y1pm}F0L^E*vy;{FIpF`uxdSba@{5~CN@uSvc> zLh+2yt)Yy!^rh`3**>+9f7U~9ZaCGNsfRT4bJrW`y(3+EsYEWjZj&Vv;SK!!pn0vlWaJ6wcfa6k!^f)iYzz$GYy%WwtC Qp#rW#C0x^!Rql`RGmgH+%m4rY 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):