fix(weapons): orient the support hand by frame, not by arc plus a twist
The support hand was upside down on the handguard. Its rotation was built as a shortest arc from the hand's forearm line to the barrel, plus a constant 0.5 rad twist. A shortest arc is the MINIMAL rotation between two directions and says nothing at all about roll, so the entire roll of that hand came from the constant — and a constant is right only for the one rig it was tuned against. Orienting a hand onto something it grips is a frame-to-frame problem, and saying it that way leaves nothing free to guess. A hand wrapping a cylinder has its curl axis along that cylinder, or the fingers close across the handguard rather than around it; and its palm faces the cylinder, which for a hand supporting from underneath means up. The third axis falls out of the other two. Map the hand's rest anatomical frame onto that target and the roll is determined rather than chosen. The frame is the same one the finger curl already uses — along, palm, curl — now stored whole instead of just its curl axis. Verified by render on the mannequin (Rigify names, 0.524 m arm) and Kiyoko (VRoid names, 0.470 m): fingers wrap the handguard from below and over the top, stock at the shoulder, arms not crossing, consistent across idle, ADS and run. L_HAND_TWIST is deleted rather than retuned — it was the bug. Smoke 0 failures. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
daed7ab980
commit
2a6b321984
@@ -225,10 +225,33 @@ 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,
|
not by the number in the name — the numbering is not consistent between families,
|
||||||
but the hierarchy always runs knuckle to fingertip.
|
but the hierarchy always runs knuckle to fingertip.
|
||||||
|
|
||||||
|
**The support hand came out upside down**, because its orientation was built as
|
||||||
|
a shortest arc plus a constant twist: align the hand's forearm line to the barrel
|
||||||
|
(`Quaternion(fa_rest_dir, aim_dir)`), then add 0.5 rad of roll. A shortest arc
|
||||||
|
says NOTHING about roll — it is the minimal rotation between two directions — so
|
||||||
|
the entire roll came from that constant, and a constant is right only for the rig
|
||||||
|
it was tuned on.
|
||||||
|
|
||||||
|
Orienting a hand onto something it grips is a FRAME-TO-FRAME problem, and framing
|
||||||
|
it that way leaves nothing free to guess:
|
||||||
|
|
||||||
|
```
|
||||||
|
curl axis must lie along the object's axis, or the fingers close ACROSS the
|
||||||
|
handguard instead of around it
|
||||||
|
palm must face the object — up, for a hand supporting from underneath
|
||||||
|
along falls out of the other two (palm x curl)
|
||||||
|
```
|
||||||
|
|
||||||
|
Map the hand's rest anatomical frame onto that target and the roll is determined,
|
||||||
|
not chosen. Verified on both the Rigify-named mannequin and the VRoid-named
|
||||||
|
Kiyoko: fingers wrap the handguard from below, over the top.
|
||||||
|
|
||||||
**Rule:** anything expressed as a constant in a rig's local frame — a mount
|
**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
|
rotation, a grip offset, a curl axis, a weapon size, a wrist twist — is a guess
|
||||||
skeleton. Derive it from the skeleton, or hand it to a solver that already knows
|
about one skeleton. Derive it from the skeleton, or hand it to a solver that
|
||||||
the answer.
|
already knows the answer. And when a rotation needs a specific ROLL, never build
|
||||||
|
it from a shortest arc: that operator has no opinion about roll, so whatever you
|
||||||
|
add afterwards is doing all the work.
|
||||||
|
|
||||||
## 6. Some sources are not salvageable, and the gate should say so
|
## 6. Some sources are not salvageable, and the gate should say so
|
||||||
|
|
||||||
|
|||||||
@@ -1174,6 +1174,8 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
|||||||
var fingers: Dictionary = {}
|
var fingers: Dictionary = {}
|
||||||
var _fing: Dictionary = {} # same, resolved to bone indices
|
var _fing: Dictionary = {} # same, resolved to bone indices
|
||||||
var _curl: Dictionary = {} # "L"/"R" -> curl axis in the rest frame
|
var _curl: Dictionary = {} # "L"/"R" -> curl axis in the rest frame
|
||||||
|
## "L"/"R" -> Basis(along, palm, curl), the hand's anatomy in the rest pose.
|
||||||
|
var _hand_frame: Dictionary = {}
|
||||||
|
|
||||||
func _resolve() -> void:
|
func _resolve() -> void:
|
||||||
var skel := get_skeleton()
|
var skel := get_skeleton()
|
||||||
@@ -1255,7 +1257,19 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
|||||||
palm -= along * palm.dot(along)
|
palm -= along * palm.dot(along)
|
||||||
if palm.length() < 0.0001:
|
if palm.length() < 0.0001:
|
||||||
continue
|
continue
|
||||||
_curl[side] = along.cross(palm.normalized()).normalized()
|
palm = palm.normalized()
|
||||||
|
var curl := along.cross(palm).normalized()
|
||||||
|
_curl[side] = curl
|
||||||
|
# The whole hand as an ANATOMICAL FRAME, not just the curl axis.
|
||||||
|
#
|
||||||
|
# Orienting a hand onto something it is gripping is a frame-to-frame
|
||||||
|
# problem: the fingers have to wrap AROUND the object, so the curl
|
||||||
|
# axis must lie along the object's axis, and the palm has to face it.
|
||||||
|
# Both are answered at once by mapping this frame onto the target's.
|
||||||
|
# Building the rotation from a shortest arc plus a constant twist —
|
||||||
|
# which is what was here — leaves the roll about the barrel
|
||||||
|
# unspecified, so the support hand landed upside down.
|
||||||
|
_hand_frame[side] = Basis(along, palm, curl)
|
||||||
if OS.has_environment("HAND_DEBUG"):
|
if OS.has_environment("HAND_DEBUG"):
|
||||||
print("HANDS fingers=%d resolved=%d curl=%s" % [
|
print("HANDS fingers=%d resolved=%d curl=%s" % [
|
||||||
fingers.size(), _fing.size(), _curl])
|
fingers.size(), _fing.size(), _curl])
|
||||||
@@ -1467,7 +1481,6 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
|||||||
const POLE_L_HIP := Vector3(0.45, -0.90, -0.10)
|
const POLE_L_HIP := Vector3(0.45, -0.90, -0.10)
|
||||||
const POLE_L_ADS := Vector3(0.30, -0.95, -0.05)
|
const POLE_L_ADS := Vector3(0.30, -0.95, -0.05)
|
||||||
const R_HAND_TWIST := 0.0
|
const R_HAND_TWIST := 0.0
|
||||||
const L_HAND_TWIST := 0.5
|
|
||||||
|
|
||||||
func _apply_rifle_hold(skel: Skeleton3D) -> void:
|
func _apply_rifle_hold(skel: Skeleton3D) -> void:
|
||||||
var ua_r: int = _idx.get("DEF-upper_arm.R", -1)
|
var ua_r: int = _idx.get("DEF-upper_arm.R", -1)
|
||||||
@@ -1570,19 +1583,34 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
|||||||
arc = Quaternion(aim_dir, roll + R_HAND_TWIST) * arc
|
arc = Quaternion(aim_dir, roll + R_HAND_TWIST) * arc
|
||||||
_set_global_rot(skel, hand, g_fa_r, arc, _hold_r)
|
_set_global_rot(skel, hand, g_fa_r, arc, _hold_r)
|
||||||
|
|
||||||
# 6. Support hand: palm wraps the handguard, following its forearm.
|
# 6. Support hand: WRAP the handguard.
|
||||||
if _hold_l > 0.001 and g_fa_l != Quaternion.IDENTITY and ua_l >= 0:
|
#
|
||||||
|
# Built as a frame, not as an arc plus a twist. A hand gripping a
|
||||||
|
# cylinder has its fingers curling AROUND that cylinder, which fixes two
|
||||||
|
# things at once and leaves nothing free:
|
||||||
|
#
|
||||||
|
# curl axis must lie along the BARREL, or the fingers close across
|
||||||
|
# the handguard instead of around it
|
||||||
|
# palm must face the barrel — up, for a hand supporting from
|
||||||
|
# underneath
|
||||||
|
#
|
||||||
|
# The old version aligned the hand's forearm line to the barrel with a
|
||||||
|
# shortest arc and then added a constant 0.5 rad twist. A shortest arc
|
||||||
|
# says nothing about roll, so the roll came entirely from that constant,
|
||||||
|
# and a constant is only ever right for the one rig it was tuned on — the
|
||||||
|
# support hand came out upside down.
|
||||||
|
if _hold_l > 0.001 and g_fa_l != Quaternion.IDENTITY and ua_l >= 0 \
|
||||||
|
and _hand_frame.has("L"):
|
||||||
var hand_l: int = _idx.get("DEF-hand.L", -1)
|
var hand_l: int = _idx.get("DEF-hand.L", -1)
|
||||||
var fa_l_idx: int = _idx.get("DEF-forearm.L", -1)
|
if hand_l >= 0:
|
||||||
if hand_l >= 0 and fa_l_idx >= 0:
|
# -aim_dir so the hand comes at the handguard from the body side
|
||||||
var fa_o := skel.get_bone_global_rest(fa_l_idx).origin
|
# rather than reaching over it backwards.
|
||||||
var hand_o := skel.get_bone_global_rest(hand_l).origin
|
var want := Basis(gun_up.cross(-aim_dir).normalized(), gun_up,
|
||||||
var fa_rest_dir := (hand_o - fa_o).normalized()
|
-aim_dir)
|
||||||
var hand_rest_q := skel.get_bone_global_rest(hand_l).basis.get_rotation_quaternion()
|
var rest: Basis = _hand_frame["L"]
|
||||||
# Point the palm along the barrel so the fingers close over it.
|
var g_hand := (want * rest.inverse()).get_rotation_quaternion() \
|
||||||
var g_hand := Quaternion(aim_dir, L_HAND_TWIST) \
|
* skel.get_bone_global_rest(hand_l).basis.get_rotation_quaternion()
|
||||||
* Quaternion(fa_rest_dir, aim_dir) * hand_rest_q
|
_set_global_rot(skel, hand_l, g_fa_l, g_hand.normalized(), _hold_l)
|
||||||
_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.
|
# How far each segment of a finger closes, knuckle -> tip, in radians.
|
||||||
#
|
#
|
||||||
|
|||||||
Reference in New Issue
Block a user