feat(weapons): scale the gun to the arm, and close the hands around it

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 <[email protected]>
This commit is contained in:
Nicholas Butzke
2026-07-26 19:02:11 -04:00
co-authored by Claude Opus 5
parent c8b0337aa7
commit 682597a0d2
7 changed files with 452 additions and 102 deletions
@@ -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