diff --git a/.claude/skills/character-pipeline/references/failure-modes.md b/.claude/skills/character-pipeline/references/failure-modes.md index 1845972..e0ab278 100644 --- a/.claude/skills/character-pipeline/references/failure-modes.md +++ b/.claude/skills/character-pipeline/references/failure-modes.md @@ -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, 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 -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. +rotation, a grip offset, a curl axis, a weapon size, a wrist twist — is a guess +about one skeleton. Derive it from the skeleton, or hand it to a solver that +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 diff --git a/characters/skinned_player_model.gd b/characters/skinned_player_model.gd index a69bd28..e51f0c2 100644 --- a/characters/skinned_player_model.gd +++ b/characters/skinned_player_model.gd @@ -1174,6 +1174,8 @@ class ShooterPoseModifier extends SkeletonModifier3D: var fingers: Dictionary = {} var _fing: Dictionary = {} # same, resolved to bone indices 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: var skel := get_skeleton() @@ -1255,7 +1257,19 @@ class ShooterPoseModifier extends SkeletonModifier3D: palm -= along * palm.dot(along) if palm.length() < 0.0001: 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"): print("HANDS fingers=%d resolved=%d curl=%s" % [ 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_ADS := Vector3(0.30, -0.95, -0.05) const R_HAND_TWIST := 0.0 - const L_HAND_TWIST := 0.5 func _apply_rifle_hold(skel: Skeleton3D) -> void: 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 _set_global_rot(skel, hand, g_fa_r, arc, _hold_r) - # 6. Support hand: palm wraps the handguard, following its forearm. - if _hold_l > 0.001 and g_fa_l != Quaternion.IDENTITY and ua_l >= 0: + # 6. Support hand: WRAP the handguard. + # + # 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 fa_l_idx: int = _idx.get("DEF-forearm.L", -1) - if hand_l >= 0 and fa_l_idx >= 0: - var fa_o := skel.get_bone_global_rest(fa_l_idx).origin - var hand_o := skel.get_bone_global_rest(hand_l).origin - var fa_rest_dir := (hand_o - fa_o).normalized() - var hand_rest_q := skel.get_bone_global_rest(hand_l).basis.get_rotation_quaternion() - # Point the palm along the barrel so the fingers close over it. - var g_hand := Quaternion(aim_dir, L_HAND_TWIST) \ - * Quaternion(fa_rest_dir, aim_dir) * hand_rest_q - _set_global_rot(skel, hand_l, g_fa_l, g_hand, _hold_l) + if hand_l >= 0: + # -aim_dir so the hand comes at the handguard from the body side + # rather than reaching over it backwards. + var want := Basis(gun_up.cross(-aim_dir).normalized(), gun_up, + -aim_dir) + var rest: Basis = _hand_frame["L"] + var g_hand := (want * rest.inverse()).get_rotation_quaternion() \ + * skel.get_bone_global_rest(hand_l).basis.get_rotation_quaternion() + _set_global_rot(skel, hand_l, g_fa_l, g_hand.normalized(), _hold_l) # How far each segment of a finger closes, knuckle -> tip, in radians. #