fix(weapons): the wrist turns the hand, and the gun stays on the aim line
The weapon is a child of a BoneAttachment3D on the trigger hand, so the two were welded by construction. Every degree `wrist_r` turned swung the barrel the same degree off the aim line — and took with it every control that could have corrected for it, because they are all expressed relative to that same hand. There was no combination of sliders that aligned a hand to a gun, which is the one thing the knob exists for. Both outcomes are now computed where the hand's local pose is set: the rotation the hand would take without the wrist offset, and the one it takes with it. The hand gets the second; the difference between them is exactly the counter-rotation the weapon mount needs, in the hand's own local frame, and `SkinnedPlayerModel._hold_weapon_still` applies it to the mount each frame. The gun ends up precisely where the solver put it. That also gives the two controls a clean split, which is what makes them usable together: TRIGGER / SUPPORT WRIST (hold) turns the HAND, gun stays on the aim line Grip roll / pitch / yaw (anchors) turns the GUN inside the hand Identity when `wrist_r` is untuned, so a character nobody has tuned mounts its weapon exactly as before. Applied in `_process` rather than inside the modifier pass on purpose. The gun's mount is not something the skeleton owns, and the compensated value only changes when a slider moves or the ADS blend travels, so one frame of lag is a fraction of a degree; reaching into the modifier to touch a scene node would be worse. wrist_gun_check asserts both halves, because only asserting the first is how this shipped broken: the hand must TURN, or the knob does nothing, and the gun must NOT, or the knob cannot be used. Across both poses and all three axes the hand turns 28.2-28.7 degrees for a 0.5 rad knob and the gun moves 0.1-0.6 — against the ~28 it would move if it were still following the wrist. The residue is the arm's own IK settling, since the hand's rotation feeds the chain that places the shoulder. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a97ccca13c
commit
e97de9aafd
@@ -106,6 +106,9 @@ var hold_tune: Dictionary = {}
|
||||
## Live anchor overrides — where the grip sits in the palm, and how the gun
|
||||
## rolls in the fingers. Same two sources as hold_tune. See RigAnchors.
|
||||
var anchors: Dictionary = {}
|
||||
## The weapon's local transform in the hand as `_measure_weapon` left it, before
|
||||
## the wrist counter-rotation. See `ShooterPoseModifier.wrist_comp_r`.
|
||||
var _weapon_seat: Transform3D = Transform3D.IDENTITY
|
||||
|
||||
# Animation blending: locomotion plays full-body through a Transition node;
|
||||
# gameplay one-shots (reload/throw/shoot/hit) play through an
|
||||
@@ -708,6 +711,7 @@ func _process(delta: float) -> void:
|
||||
_update_cloth_lod(delta)
|
||||
if not _pose_mod:
|
||||
return
|
||||
_hold_weapon_still()
|
||||
var t := 1.0 - exp(-POSE_SMOOTH * delta)
|
||||
# The body lean gets its own, much slower rate, and is scaled by how fast the
|
||||
# character is ACTUALLY moving rather than by which key is held.
|
||||
@@ -946,6 +950,29 @@ func set_anchors(a: Dictionary) -> void:
|
||||
_reseat_weapon()
|
||||
|
||||
|
||||
## Keep the weapon where the solver put it while the wrist turns under it.
|
||||
##
|
||||
## The two are welded by construction — the gun is a child of a BoneAttachment3D
|
||||
## on the trigger hand — so a wrist rotation swings the barrel off the aim line
|
||||
## and takes every control that could correct it along for the ride. The pose
|
||||
## layer works out the exact counter-rotation in the hand's own local frame;
|
||||
## this applies it.
|
||||
##
|
||||
## Deliberately in `_process` rather than inside the modifier pass. The gun's
|
||||
## mount is not something the skeleton owns, and the value being compensated
|
||||
## only changes when a slider moves or the ADS blend travels, so being one frame
|
||||
## behind is a rotation of a fraction of a degree that nothing can see. Reaching
|
||||
## into the modifier to touch a scene node would be worse.
|
||||
func _hold_weapon_still() -> void:
|
||||
if _weapon_attachment == null or _weapon_attachment.get_child_count() == 0:
|
||||
return
|
||||
var w := _weapon_attachment.get_child(0) as Node3D
|
||||
if w == null:
|
||||
return
|
||||
var comp: Quaternion = _pose_mod.wrist_comp_r
|
||||
w.transform = Transform3D(Basis(comp), Vector3.ZERO) * _weapon_seat
|
||||
|
||||
|
||||
## Re-apply the grip anchor and re-measure, after either table changed.
|
||||
##
|
||||
## `_measure_weapon` reads the weapon's transform to work out where its grip and
|
||||
@@ -1173,6 +1200,11 @@ func _measure_weapon(w: Node3D) -> void:
|
||||
if hold_tune.get("gun_stock", 0.0) > 0.0001:
|
||||
_pose_mod.gun_stock = float(hold_tune["gun_stock"])
|
||||
_pose_mod.tune = hold_tune
|
||||
# Where the weapon sits in the hand once everything derived and tuned has
|
||||
# been applied. Kept because `_process` re-derives the mount every frame from
|
||||
# it plus the wrist counter-rotation, and recomputing the seat instead would
|
||||
# re-run this whole measurement sixty times a second.
|
||||
_weapon_seat = w.transform
|
||||
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
@@ -1312,6 +1344,18 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
||||
## expressed in; without it, dragging left would mean something different at
|
||||
## every pitch of the weapon.
|
||||
var dbg_gun_basis: Basis = Basis.IDENTITY
|
||||
## What the weapon mount must be rotated by, in the trigger hand's own local
|
||||
## frame, to undo `wrist_r` — so the hand turns and the GUN does not.
|
||||
##
|
||||
## The weapon is parented to a BoneAttachment3D on that hand, so without this
|
||||
## the two are welded: rotating the wrist swings the barrel off the aim line,
|
||||
## and there is no second control that could bring it back, because every
|
||||
## control that moves the gun is expressed relative to the same hand. The
|
||||
## wrist knob was therefore unusable for the one thing it exists for.
|
||||
##
|
||||
## Identity when `wrist_r` is untuned, so a character nobody has touched
|
||||
## mounts its weapon exactly as before.
|
||||
var wrist_comp_r: Quaternion = Quaternion.IDENTITY
|
||||
|
||||
func _t(key: String, fallback: float) -> float:
|
||||
return float(tune.get(key, fallback))
|
||||
@@ -1772,8 +1816,22 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
||||
# the barrel has to lie on the aim line, which fixes two of the
|
||||
# hand's three freedoms, but nothing fixes how far the wrist is
|
||||
# cocked or broken, and those were unreachable.
|
||||
arc = _wrist("wrist_r", side, gun_up, aim_dir) * arc
|
||||
_set_global_rot(skel, hand, g_fa_r, arc, _hold_r)
|
||||
#
|
||||
# The gun hangs off THIS bone, so rotating it carries the gun
|
||||
# along and the barrel comes off the aim line — which made the
|
||||
# knob useless for its actual purpose, since there was then no
|
||||
# way to align the hand to a gun that had moved with it. Both
|
||||
# outcomes are computed, the hand takes the rotated one, and the
|
||||
# difference between them is published as the counter-rotation
|
||||
# the weapon mount needs to stay exactly where the solver put it.
|
||||
# See `wrist_comp_r`.
|
||||
var cur := skel.get_bone_pose_rotation(hand)
|
||||
var local_free := (g_fa_r.inverse() * arc).normalized()
|
||||
var wrist_q := _wrist("wrist_r", side, gun_up, aim_dir)
|
||||
var local_wrist := (g_fa_r.inverse() * (wrist_q * arc)).normalized()
|
||||
var applied := cur.slerp(local_wrist, _hold_r)
|
||||
wrist_comp_r = applied.inverse() * cur.slerp(local_free, _hold_r)
|
||||
skel.set_bone_pose_rotation(hand, applied)
|
||||
|
||||
# 6. Support hand: WRAP the handguard.
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user