fix: third-person gun actually held — IK to the weapon, real recoil, no flip
Four separate third-person defects, one shared root: the arms were posed at art-directed ANGLES that merely approximated the gun, and a stuck one-shot flag kept handing them back to the animation clip. - The rifle hold was silently DEAD after the first reload or throw. The gate trusted AnimationNodeOneShot's `active` parameter, which never clears, so clip_owns_arms stayed true forever and the clip drove the arms while the gun hung off the hand. `_upper_lock` (a timer we own) is now the authority, and the one-shot is explicitly faded out when it expires. This alone is why the gun was never held correctly. - Arms are now solved with real two-bone IK (`_ik_arm`) onto points derived from the WEAPON: the stock is anchored in the shoulder pocket, the grip and foregrip fall out along the barrel, and the support hand is placed on the actual handguard (sliding inboard if the model is too long to reach). Verified numerically: the hand lands within 2mm of its target. - `_measure_weapon` measures the model's AABB along its barrel and re-seats it so the hand sits at a realistic pistol-grip point. Weapon models put their origin anywhere — the M4's was 10cm from the muzzle end, so parking its stock in the shoulder drove the hand INTO the shoulder and folded the arm up behind the head. - Recoil now shows for the OWNER in third person: server_play_fire_effects only ever fired for remote shooters, so the local player saw nothing. The ammo counter dropping now kicks the model. The kick rides in the gun's aim direction so the IK carries BOTH hands up with it, instead of rotating the arms and shoving the support hand off the gun. - Reload no longer flips the gun. The library's pistol-reload rotates the wrist the gun is parented to, which turned the rifle upside-down (mag to the sky) while the hand reached down for it. The hold now keeps the right arm through a reload and the support hand does the magazine work at the real mag well, under the receiver. anim_capture gains recoil shots and lets ADS settle before framing; debug/dump_bones.gd prints a skin's bone hierarchy. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
df0bf18e0f
commit
0d125dc03f
@@ -90,6 +90,8 @@ var _anim_tree: AnimationTree
|
||||
var _loco_trans: AnimationNodeTransition
|
||||
var _upper_anim: AnimationNodeAnimation
|
||||
var _upper_lock: float = 0.0 # seconds the one-shot owns the ARMS
|
||||
var _upper_total: float = 0.0 # its full duration, for progress 0..1
|
||||
var _upper_action: String = "" # which ACTIONS entry is playing
|
||||
## Bone-name fragments that belong to the upper-body one-shot layer.
|
||||
const UPPER_BONE_HINTS := ["shoulder", "upper_arm", "forearm", "hand", "thumb",
|
||||
"f_index", "f_middle", "f_ring", "f_pinky", "spine.002", "spine.003",
|
||||
@@ -314,6 +316,7 @@ func play_oneshot(canonical: String, lock_time: float = 0.35) -> void:
|
||||
_play_clip(canonical, true)
|
||||
return
|
||||
_upper_lock = lock_time
|
||||
_upper_total = lock_time
|
||||
_upper_anim.animation = _resolved_clips[canonical]
|
||||
_anim_tree.set("parameters/upper/request", AnimationNodeOneShot.ONE_SHOT_REQUEST_FIRE)
|
||||
|
||||
@@ -327,6 +330,7 @@ func set_dancing(on: bool) -> void:
|
||||
## Play a named gameplay action (reload / throw / shoot) as a one-shot.
|
||||
func play_action(action: String) -> void:
|
||||
if ACTIONS.has(action):
|
||||
_upper_action = action
|
||||
play_oneshot(ACTIONS[action][0], ACTIONS[action][1])
|
||||
|
||||
|
||||
@@ -458,6 +462,15 @@ func _process(delta: float) -> void:
|
||||
# direction to the anchor in skeleton space.
|
||||
if _upper_lock > 0.0:
|
||||
_upper_lock -= delta
|
||||
if _upper_lock <= 0.0:
|
||||
# Explicitly retire the one-shot. Its `active` parameter does NOT
|
||||
# reliably clear on its own, and anything still treating the shot
|
||||
# as live permanently disables the rifle hold — after one reload
|
||||
# the character would hold the gun with clip arms forever.
|
||||
_upper_action = ""
|
||||
if _anim_tree:
|
||||
_anim_tree.set("parameters/upper/request",
|
||||
AnimationNodeOneShot.ONE_SHOT_REQUEST_FADE_OUT)
|
||||
var grapple_target := 1.0 if _pose_mod.state == "grapple" else 0.0
|
||||
_cur_grapple = lerpf(_cur_grapple, grapple_target, t)
|
||||
_pose_mod.grapple = _cur_grapple
|
||||
@@ -473,10 +486,17 @@ func _process(delta: float) -> void:
|
||||
# dance, death) needs the authored animation to read through. The upper
|
||||
# one-shot's own `active` flag is the truth for how long it owns the arms.
|
||||
var st: String = _pose_mod.state
|
||||
var upper_active: bool = _anim_tree != null \
|
||||
and bool(_anim_tree.get("parameters/upper/active"))
|
||||
var clip_owns_arms := _oneshot_lock > 0.0 or _upper_lock > 0.0 \
|
||||
or upper_active or _dancing or st == "death"
|
||||
# `_upper_lock` (a timer we own) is the authority on how long the one-shot
|
||||
# owns the arms — NOT the OneShot node's `active` flag, which can stay
|
||||
# true indefinitely and would strand the arms on the clip forever.
|
||||
var reloading := _upper_action == "reload" and _upper_lock > 0.0
|
||||
# A RELOAD must never hand the right arm to the clip: the gun is parented
|
||||
# to that hand, and the library's pistol-reload rotates the wrist — which
|
||||
# flipped the rifle upside-down (mag pointing at the sky) while the hand
|
||||
# reached "down" for it. During a reload the hold keeps the gun steady and
|
||||
# the support hand does the magazine work at the real mag well instead.
|
||||
var clip_owns_arms := (_oneshot_lock > 0.0 or _upper_lock > 0.0 \
|
||||
or _dancing or st == "death") and not reloading
|
||||
var hold_r := 0.0
|
||||
var hold_l := 0.0
|
||||
if is_holding_weapon and not clip_owns_arms:
|
||||
@@ -496,6 +516,12 @@ func _process(delta: float) -> void:
|
||||
_pose_mod.hold_r_target = hold_r
|
||||
_pose_mod.hold_l_target = hold_l
|
||||
|
||||
# Reload progress drives the support hand's trip to the mag well.
|
||||
var rl_target := 0.0
|
||||
if reloading and _upper_total > 0.0:
|
||||
rl_target = clampf(1.0 - (_upper_lock / _upper_total), 0.0, 1.0)
|
||||
_pose_mod.reload_phase = rl_target
|
||||
|
||||
|
||||
func _play_clip(canonical: String, restart: bool = false) -> void:
|
||||
if not _anim_tree or not _resolved_clips.has(canonical):
|
||||
@@ -547,13 +573,14 @@ func set_weapon(script_path: String) -> void:
|
||||
# lie along the hand's grip, scaled down to character proportions.
|
||||
w.position = Vector3(-0.02, 0.07, 0.0)
|
||||
w.rotation_degrees = Vector3(0, 90, -90)
|
||||
w.scale = Vector3(0.75, 0.75, 0.75)
|
||||
w.scale = Vector3(1.0, 1.0, 1.0)
|
||||
# Tell the pose layer the gun's axes in hand-bone space so it can
|
||||
# aim the wrist to point the muzzle exactly where the player looks.
|
||||
if _pose_mod:
|
||||
var b: Basis = w.transform.basis.orthonormalized()
|
||||
_pose_mod.gun_fwd_hand = b * Vector3(0, 0, -1)
|
||||
_pose_mod.gun_up_hand = b * Vector3(0, 1, 0)
|
||||
_measure_weapon(w)
|
||||
)
|
||||
|
||||
var hand_idx := _find_bone(["RightHand", "Hand_R", "hand.R"])
|
||||
@@ -572,6 +599,45 @@ func set_weapon(script_path: String) -> void:
|
||||
add_child(w)
|
||||
|
||||
|
||||
## Measure the held weapon along its own barrel axis so the pose layer knows
|
||||
## where the real foregrip and stock butt are, instead of guessing. Distances
|
||||
## are from the GRIP (the weapon node's origin, which sits in the hand), in
|
||||
## metres of character space.
|
||||
func _measure_weapon(w: Node3D) -> void:
|
||||
var local_fwd := Vector3(0, 0, -1) # the weapon's own muzzle axis
|
||||
var min_t := INF # most negative = stock end
|
||||
var max_t := -INF # most positive = muzzle end
|
||||
for mi in w.find_children("*", "MeshInstance3D", true, false):
|
||||
if not mi.mesh:
|
||||
continue
|
||||
var xf: Transform3D = w.global_transform.affine_inverse() * mi.global_transform
|
||||
var aabb: AABB = mi.mesh.get_aabb()
|
||||
for i in 8:
|
||||
var t: float = (xf * aabb.get_endpoint(i)).dot(local_fwd)
|
||||
min_t = minf(min_t, t)
|
||||
max_t = maxf(max_t, t)
|
||||
if min_t > max_t:
|
||||
return
|
||||
var s: float = absf(w.scale.z)
|
||||
var total := max_t - min_t
|
||||
if total < 0.0001:
|
||||
return
|
||||
# Weapon models put their origin wherever the artist left it — for the M4
|
||||
# that is barely 10 cm behind the muzzle end, so hanging the hand there
|
||||
# and then parking the stock in the shoulder shoved the hand INTO the
|
||||
# shoulder and the arm folded up behind the head. Re-seat the weapon so
|
||||
# the hand sits at a realistic pistol-grip point (~a third back from the
|
||||
# muzzle), which puts real length of gun behind the hand to reach the
|
||||
# shoulder with.
|
||||
var grip_at := min_t + total * 0.32
|
||||
w.position -= _pose_mod.gun_fwd_hand * (grip_at * s)
|
||||
var back := (grip_at - min_t) * s # butt of the stock, behind the grip
|
||||
var front := (max_t - grip_at) * s # muzzle, ahead of the grip
|
||||
_pose_mod.gun_stock = clampf(back, 0.10, 0.40)
|
||||
# Support hand rides partway out the handguard, never past the muzzle.
|
||||
_pose_mod.gun_fore = clampf(front * 0.55, 0.14, 0.45)
|
||||
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
func _find_bone(name_parts: Array) -> int:
|
||||
@@ -632,6 +698,12 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
||||
# The attached gun's forward/up axes in hand-bone space (set on set_weapon).
|
||||
var gun_fwd_hand: Vector3 = Vector3.ZERO
|
||||
var gun_up_hand: Vector3 = Vector3.UP
|
||||
# Measured gun geometry (metres from the grip): how far out the support
|
||||
# hand rides, and how far back the stock butt reaches. See _measure_weapon.
|
||||
var gun_fore: float = 0.26
|
||||
var gun_stock: float = 0.20
|
||||
# 0..1 through a reload — drives the support hand to the mag well and back.
|
||||
var reload_phase: float = 0.0
|
||||
|
||||
# Tuning (radians). Positive pitch leans forward; positive roll leans right.
|
||||
const LEAN_ROLL := 0.30
|
||||
@@ -701,16 +773,15 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
||||
_add_space(skel, _idx.get("DEF-head", -1), Quaternion.IDENTITY.slerp(head_q, 0.5))
|
||||
|
||||
|
||||
# Shot kick: shoulders snap back and up, forearms rise; decays fast.
|
||||
# Shot kick: the torso absorbs it. The MUZZLE rise is not applied here —
|
||||
# it rides in the hold's aim direction (see `kick` in _apply_rifle_hold),
|
||||
# so the IK carries BOTH hands up with the gun. Rotating the arms here
|
||||
# instead would shove the support hand straight off the handguard.
|
||||
func _apply_recoil(skel: Skeleton3D) -> void:
|
||||
var k := recoil
|
||||
var back := Quaternion(Vector3(1, 0, 0), -0.12 * k)
|
||||
var back := Quaternion(Vector3(1, 0, 0), -0.05 * k)
|
||||
for n in ["DEF-spine.002", "DEF-spine.003"]:
|
||||
_add_space(skel, _idx.get(n, -1), back)
|
||||
var arm_up := Quaternion(Vector3(1, 0, 0), -0.3 * k)
|
||||
_add_space(skel, _idx.get("DEF-upper_arm.R", -1), arm_up)
|
||||
_add_space(skel, _idx.get("DEF-upper_arm.L", -1), arm_up)
|
||||
_add_space(skel, _idx.get("DEF-forearm.R", -1), Quaternion(Vector3(1, 0, 0), -0.22 * k))
|
||||
|
||||
# Distribute a skeleton-space lean across the spine bones.
|
||||
func _apply_lean(skel: Skeleton3D) -> void:
|
||||
@@ -765,7 +836,7 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
||||
var hand_o := skel.get_bone_global_rest(hand_l).origin
|
||||
var fa_rest_dir := (hand_o - fa_o).normalized()
|
||||
var g_hand := Quaternion(d, 0.4) * Quaternion(fa_rest_dir, d) * hand_rest_q
|
||||
_blend_local(skel, hand_l, g_fa.inverse() * g_hand, grapple)
|
||||
_set_global_rot(skel, hand_l, g_fa, g_hand, grapple)
|
||||
|
||||
# Wall run: roll into the wall, drive forward, inner arm reaches the wall.
|
||||
func _apply_wall_lean(skel: Skeleton3D) -> void:
|
||||
@@ -807,88 +878,207 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
||||
_add_space(skel, _idx.get("DEF-forearm.L", -1), Quaternion(Vector3(1, 0, 0), 0.35 * slide))
|
||||
|
||||
# ── Two-hand rifle hold ──────────────────────────────────────────────────
|
||||
# REPLACES the arm-chain rotations from the base clip with a deterministic
|
||||
# FK pose: each arm bone is aimed along an art-directed skeleton-space
|
||||
# DIRECTION (low-ready at the hip, shouldered on ADS), so the hold looks
|
||||
# identical in every locomotion state — no unarmed jog arms flailing
|
||||
# around a floating gun. Directions are in skeleton space: character faces
|
||||
# +Z, up +Y, character-right -X.
|
||||
# The gun is parented to the RIGHT HAND bone, so where the hands go decides
|
||||
# where the gun goes. We therefore place the WEAPON first — stock in the
|
||||
# shoulder pocket, barrel down the aim line — then solve both arms with
|
||||
# two-bone IK to the resulting grip and foregrip points. That is what makes
|
||||
# the stock actually meet the shoulder and the support hand actually touch
|
||||
# the handguard, instead of both arms waving at art-directed angles near it.
|
||||
# Skeleton space: character faces +Z, up +Y, character-right -X.
|
||||
|
||||
# Right arm: gun hand — elbow at the ribs, hand ahead of the right hip,
|
||||
# rifle line pointing forward-down.
|
||||
const R_UA_HIP := Vector3(-0.30, -0.90, 0.28)
|
||||
const R_FA_HIP := Vector3(0.25, 0.15, 0.95)
|
||||
# Left arm: support hand crosses to the foregrip ahead of the belly.
|
||||
const L_UA_HIP := Vector3(0.32, -0.80, 0.42)
|
||||
const L_FA_HIP := Vector3(-0.25, 0.10, 0.96)
|
||||
# ADS: both arms rise with bent elbows, hands stack along the eye line.
|
||||
const R_UA_ADS := Vector3(-0.30, -0.60, 0.70)
|
||||
const R_FA_ADS := Vector3(0.35, 0.45, 0.85)
|
||||
const L_UA_ADS := Vector3(0.28, -0.50, 0.80)
|
||||
const L_FA_ADS := Vector3(-0.25, 0.45, 0.88)
|
||||
# Twist about the bone line (radians) to keep elbows/palms natural.
|
||||
const R_UA_TWIST := 0.3; const L_UA_TWIST := -0.3
|
||||
const L_HAND_TWIST := 0.5
|
||||
const GUN_PITCH_HIP := 0.38 # muzzle tilts down this much at low-ready
|
||||
# Where the butt of the stock sits, relative to the right shoulder joint.
|
||||
# The pocket is on the FRONT of the shoulder, slightly inboard of the joint.
|
||||
const POCKET_ADS := Vector3(0.05, 0.01, 0.07) # in the shoulder pocket
|
||||
const POCKET_HIP := Vector3(0.02, -0.20, 0.05) # tucked down at the ribs
|
||||
# Elbow bend hints (skeleton space). At the hip the firing elbow rides
|
||||
# down by the ribs; shouldered it flares OUT and level (the classic
|
||||
# "chicken wing"), which is what keeps the tight fold from folding the
|
||||
# arm up behind the head. The support elbow always tucks under the gun.
|
||||
const POLE_R_HIP := Vector3(-0.55, -0.85, -0.20)
|
||||
const POLE_R_ADS := Vector3(-1.0, -0.25, -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 R_HAND_TWIST := 0.0
|
||||
const L_HAND_TWIST := 0.5
|
||||
|
||||
func _apply_rifle_hold(skel: Skeleton3D) -> void:
|
||||
var breathe := sin(_time * 2.2) * 0.015 + fwd * 0.03
|
||||
var kick := recoil * 0.2
|
||||
# Aim pitch tilts the whole hold on ADS; recoil kicks the muzzle up.
|
||||
var pitch := (-aim_pitch * ads) - kick - breathe
|
||||
var q_pitch := Quaternion(Vector3(1, 0, 0), pitch)
|
||||
var ua_r: int = _idx.get("DEF-upper_arm.R", -1)
|
||||
var ua_l: int = _idx.get("DEF-upper_arm.L", -1)
|
||||
if ua_r < 0:
|
||||
return
|
||||
var breathe := sin(_time * 2.2) * 0.012 + fwd * 0.02
|
||||
# ~7 degrees of muzzle rise per shot, stacking a little on full auto.
|
||||
var kick := recoil * 0.12
|
||||
|
||||
var ua_r: Vector3 = q_pitch * R_UA_HIP.lerp(R_UA_ADS, ads).normalized()
|
||||
var fa_r: Vector3 = q_pitch * R_FA_HIP.lerp(R_FA_ADS, ads).normalized()
|
||||
var ua_l: Vector3 = q_pitch * L_UA_HIP.lerp(L_UA_ADS, ads).normalized()
|
||||
var fa_l: Vector3 = q_pitch * L_FA_HIP.lerp(L_FA_ADS, ads).normalized()
|
||||
# 1. The gun's line: pitched down at low-ready, on the camera line at
|
||||
# ADS, kicked up by recoil.
|
||||
var gun_pitch := lerpf(GUN_PITCH_HIP, -aim_pitch, ads) - kick + breathe
|
||||
var aim_dir: Vector3 = (Quaternion(Vector3(1, 0, 0), gun_pitch) \
|
||||
* Vector3(0, 0, 1)).normalized()
|
||||
# Gun's "up" — perpendicular to the barrel in the vertical plane, so the
|
||||
# magazine always hangs DOWN.
|
||||
var side := aim_dir.cross(Vector3.UP)
|
||||
if side.length_squared() < 0.0001:
|
||||
side = Vector3(-1, 0, 0)
|
||||
side = side.normalized()
|
||||
var gun_up := side.cross(aim_dir).normalized()
|
||||
|
||||
var g_fa_r := _aim_chain(skel, "DEF-upper_arm.R", "DEF-forearm.R",
|
||||
ua_r, fa_r, R_UA_TWIST, _hold_r)
|
||||
var g_fa_l := _aim_chain(skel, "DEF-upper_arm.L", "DEF-forearm.L",
|
||||
ua_l, fa_l, L_UA_TWIST, _hold_l)
|
||||
# 2. Anchor the stock at the shoulder, then walk out along the barrel.
|
||||
var shoulder := skel.get_bone_global_pose(ua_r).origin
|
||||
var pocket: Vector3 = POCKET_HIP.lerp(POCKET_ADS, ads)
|
||||
var stock_pos := shoulder + pocket
|
||||
var grip_pos := stock_pos + aim_dir * gun_stock
|
||||
# The support hand rides as far out the handguard as it can actually
|
||||
# REACH. Without this a long rifle puts the foregrip past the left
|
||||
# arm's limit and the IK yanks the whole arm out straight.
|
||||
var fore_dist := gun_fore
|
||||
if ua_l >= 0:
|
||||
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:
|
||||
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
|
||||
var fore_pos := grip_pos + aim_dir * fore_dist
|
||||
|
||||
# Gun hand: orient the wrist so the MUZZLE points exactly along the aim
|
||||
# line (forward-down at low-ready, camera pitch on ADS), gun kept
|
||||
# upright. This is what makes the weapon read "aimed" instead of
|
||||
# dangling at whatever angle the wrist twist happens to produce.
|
||||
# 3. Support hand goes to the mag well during a reload (under the
|
||||
# receiver — the correct side), otherwise to the handguard.
|
||||
var l_target := fore_pos
|
||||
if reload_phase > 0.001:
|
||||
var mag_well := grip_pos + aim_dir * (gun_fore * 0.35) - gun_up * 0.10
|
||||
var drop := mag_well - gun_up * 0.22 - aim_dir * 0.05
|
||||
var p := reload_phase
|
||||
if p < 0.30: # rip the mag straight down out of the well
|
||||
l_target = mag_well.lerp(drop, p / 0.30)
|
||||
elif p < 0.55: # reach down for a fresh one
|
||||
l_target = drop
|
||||
elif p < 0.80: # bring it back up and seat it
|
||||
l_target = drop.lerp(mag_well, (p - 0.55) / 0.25)
|
||||
else: # hand returns to the handguard
|
||||
l_target = mag_well.lerp(fore_pos, (p - 0.80) / 0.20)
|
||||
|
||||
# 4. Solve both arms onto those points.
|
||||
var pole_r: Vector3 = POLE_R_HIP.lerp(POLE_R_ADS, ads).normalized()
|
||||
var pole_l: Vector3 = POLE_L_HIP.lerp(POLE_L_ADS, ads).normalized()
|
||||
var g_fa_r := _ik_arm(skel, "DEF-upper_arm.R", "DEF-forearm.R",
|
||||
"DEF-hand.R", grip_pos, pole_r, _hold_r)
|
||||
var g_fa_l := _ik_arm(skel, "DEF-upper_arm.L", "DEF-forearm.L",
|
||||
"DEF-hand.L", l_target, pole_l, _hold_l)
|
||||
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],
|
||||
" 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 "-",
|
||||
" reachL=%.3f" % _arm_reach(skel, "DEF-upper_arm.L",
|
||||
"DEF-forearm.L", "DEF-hand.L"))
|
||||
|
||||
# 5. Roll the gun hand so the BARREL lies on the aim line and the
|
||||
# magazine points down — the gun's orientation comes entirely from
|
||||
# this wrist, so it can never end up inverted.
|
||||
if _hold_r > 0.001 and g_fa_r != Quaternion.IDENTITY \
|
||||
and gun_fwd_hand.length_squared() > 0.5:
|
||||
var gun_pitch := lerpf(GUN_PITCH_HIP, -aim_pitch, ads) - kick * 2.0
|
||||
var d := Quaternion(Vector3(1, 0, 0), gun_pitch) * Vector3(0, 0, 1)
|
||||
var hand: int = _idx.get("DEF-hand.R", -1)
|
||||
if hand >= 0:
|
||||
var arc := Quaternion(gun_fwd_hand.normalized(), d)
|
||||
# Kill the roll: rotate about the aim line so the gun's up
|
||||
# vector lands in the vertical plane of the aim direction.
|
||||
var arc := Quaternion(gun_fwd_hand.normalized(), aim_dir)
|
||||
var up_now := arc * gun_up_hand.normalized()
|
||||
var side := d.cross(Vector3.UP)
|
||||
if side.length_squared() > 0.001:
|
||||
var up_ideal := side.normalized().cross(d).normalized()
|
||||
var up_flat := (up_now - d * up_now.dot(d)).normalized()
|
||||
var roll := up_flat.signed_angle_to(up_ideal, d)
|
||||
arc = Quaternion(d, roll) * arc
|
||||
_blend_local(skel, hand, g_fa_r.inverse() * arc, _hold_r)
|
||||
if OS.has_environment("GUN_POSE_DEBUG"):
|
||||
var fa_actual := skel.get_bone_global_pose(
|
||||
_idx.get("DEF-forearm.R", -1)).basis.get_rotation_quaternion()
|
||||
var hand_actual := skel.get_bone_global_pose(hand).basis.get_rotation_quaternion()
|
||||
print("MOD DEBUG d=", d,
|
||||
" fa_target=", g_fa_r, " fa_actual=", fa_actual,
|
||||
" hand_global*v=", hand_actual * gun_fwd_hand)
|
||||
var up_flat := (up_now - aim_dir * up_now.dot(aim_dir))
|
||||
if up_flat.length_squared() > 0.0001:
|
||||
var roll := up_flat.normalized().signed_angle_to(gun_up, aim_dir)
|
||||
arc = Quaternion(aim_dir, roll + R_HAND_TWIST) * arc
|
||||
_set_global_rot(skel, hand, g_fa_r, arc, _hold_r)
|
||||
|
||||
# Support hand: follow the forearm line with a fixed palm twist.
|
||||
if _hold_l > 0.001 and g_fa_l != Quaternion.IDENTITY:
|
||||
# 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:
|
||||
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 hand_rest_q := skel.get_bone_global_rest(hand_l).basis.get_rotation_quaternion()
|
||||
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 g_hand := Quaternion(fa_l, L_HAND_TWIST) \
|
||||
* Quaternion(fa_rest_dir, fa_l) * hand_rest_q
|
||||
_blend_local(skel, hand_l, g_fa_l.inverse() * g_hand, _hold_l)
|
||||
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)
|
||||
|
||||
## 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:
|
||||
var ua: int = _idx.get(ua_name, -1)
|
||||
var fa: int = _idx.get(fa_name, -1)
|
||||
var hand: int = _idx.get(hand_name, -1)
|
||||
if ua < 0 or fa < 0 or hand < 0:
|
||||
return 0.5
|
||||
var a := skel.get_bone_global_rest(ua).origin
|
||||
var b := skel.get_bone_global_rest(fa).origin
|
||||
var c := skel.get_bone_global_rest(hand).origin
|
||||
return a.distance_to(b) + b.distance_to(c)
|
||||
|
||||
## Two-bone IK: rotate the upper arm + forearm so the HAND JOINT lands on
|
||||
## `target` (skeleton space). `pole` biases which way the elbow breaks.
|
||||
## Returns the forearm's achieved global rotation (IDENTITY when skipped).
|
||||
func _ik_arm(skel: Skeleton3D, ua_name: String, fa_name: String,
|
||||
hand_name: String, target: Vector3, pole: Vector3,
|
||||
w: float) -> Quaternion:
|
||||
if w <= 0.001:
|
||||
return Quaternion.IDENTITY
|
||||
var ua: int = _idx.get(ua_name, -1)
|
||||
var fa: int = _idx.get(fa_name, -1)
|
||||
var hand: int = _idx.get(hand_name, -1)
|
||||
if ua < 0 or fa < 0 or hand < 0:
|
||||
return Quaternion.IDENTITY
|
||||
|
||||
# Segment lengths come from the REST pose so they never drift.
|
||||
var ua_rest := skel.get_bone_global_rest(ua).origin
|
||||
var fa_rest := skel.get_bone_global_rest(fa).origin
|
||||
var hand_rest := skel.get_bone_global_rest(hand).origin
|
||||
var l1 := ua_rest.distance_to(fa_rest)
|
||||
var l2 := fa_rest.distance_to(hand_rest)
|
||||
if l1 < 0.0001 or l2 < 0.0001:
|
||||
return Quaternion.IDENTITY
|
||||
|
||||
# The shoulder's CURRENT position (spine lean/aim pitch already moved it).
|
||||
var root := skel.get_bone_global_pose(ua).origin
|
||||
var to_target := target - root
|
||||
var d := to_target.length()
|
||||
if d < 0.0001:
|
||||
return Quaternion.IDENTITY
|
||||
var reach := to_target / d
|
||||
d = clampf(d, absf(l1 - l2) + 0.002, l1 + l2 - 0.002)
|
||||
|
||||
# Law of cosines for the shoulder angle, then break the elbow toward
|
||||
# the pole to pick one of the infinitely many solutions.
|
||||
var cos_a := clampf((l1 * l1 + d * d - l2 * l2) / (2.0 * l1 * d), -1.0, 1.0)
|
||||
var axis := reach.cross(pole)
|
||||
if axis.length_squared() < 0.000001:
|
||||
axis = reach.cross(Vector3.UP)
|
||||
if axis.length_squared() < 0.000001:
|
||||
axis = reach.cross(Vector3(1, 0, 0))
|
||||
axis = axis.normalized()
|
||||
var ua_dir := (Quaternion(axis, acos(cos_a)) * reach).normalized()
|
||||
var elbow := root + ua_dir * l1
|
||||
var fa_dir := target - elbow
|
||||
fa_dir = fa_dir.normalized() if fa_dir.length_squared() > 0.00000001 else reach
|
||||
|
||||
var g_fa := _aim_chain(skel, ua_name, fa_name, ua_dir, fa_dir, 0.0, w)
|
||||
if OS.has_environment("IK_DEBUG") and ua_name.ends_with(".R"):
|
||||
var got_ua := skel.get_bone_global_pose(ua)
|
||||
var got_fa := skel.get_bone_global_pose(fa)
|
||||
var got_hand := skel.get_bone_global_pose(hand)
|
||||
print("IK l1=%.3f l2=%.3f d=%.3f" % [l1, l2, d],
|
||||
"\n root_want=", root, " ua_origin_got=", got_ua.origin,
|
||||
"\n elbow_want=", elbow, " fa_origin_got=", got_fa.origin,
|
||||
"\n hand_want=", target, " hand_got=", got_hand.origin,
|
||||
"\n ua_dir=", ua_dir, " ua_dir_got=",
|
||||
(got_fa.origin - got_ua.origin).normalized())
|
||||
return g_fa
|
||||
|
||||
# Aim an upper-arm/forearm chain along the given directions with exact FK:
|
||||
# desired global orientation = (shortest arc from the bone's rest line to
|
||||
@@ -921,13 +1111,21 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
||||
var g_ua := Quaternion(ua_dir, ua_twist) * Quaternion(ua_rest_dir, ua_dir) * ua_rest_q
|
||||
var g_fa := Quaternion(fa_rest_dir, fa_dir) * fa_rest_q
|
||||
|
||||
# Local poses against the actual (clip-posed) parent for the shoulder
|
||||
# link, then against our own target down the chain.
|
||||
# Convert both to LOCAL pose rotations. Godot composes a bone as
|
||||
# global = parent_global * rest_local * pose_local
|
||||
# so pose_local = rest_local⁻¹ * parent_global⁻¹ * global_target.
|
||||
# (Dropping the rest_local⁻¹ term silently biases every bone by its
|
||||
# rest orientation — which is why hand-tuned angles used to be needed.)
|
||||
var parent := skel.get_bone_parent(ua)
|
||||
var g_parent := skel.get_bone_global_pose(parent).basis.get_rotation_quaternion() \
|
||||
if parent >= 0 else Quaternion.IDENTITY
|
||||
_blend_local(skel, ua, g_parent.inverse() * g_ua, w)
|
||||
_blend_local(skel, fa, g_ua.inverse() * g_fa, w)
|
||||
_set_global_rot(skel, ua, g_parent, g_ua, w)
|
||||
# The forearm hangs off the upper arm we just aimed, so its parent's
|
||||
# global IS g_ua (using the cached pose here would lag a frame).
|
||||
var fa_parent := skel.get_bone_parent(fa)
|
||||
var g_fa_parent := g_ua if fa_parent == ua \
|
||||
else skel.get_bone_global_pose(fa_parent).basis.get_rotation_quaternion()
|
||||
_set_global_rot(skel, fa, g_fa_parent, g_fa, w)
|
||||
if OS.has_environment("GUN_POSE_DEBUG") and ua_name.ends_with(".R"):
|
||||
var ua_actual := skel.get_bone_global_pose(ua).basis.get_rotation_quaternion()
|
||||
var fa_actual := skel.get_bone_global_pose(fa).basis.get_rotation_quaternion()
|
||||
@@ -941,6 +1139,13 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
||||
skel.set_bone_pose_rotation(idx,
|
||||
skel.get_bone_pose_rotation(idx).slerp(target.normalized(), w))
|
||||
|
||||
## Give a bone the requested GLOBAL rotation, given its parent's global
|
||||
## rotation. In Godot 4 a bone's POSE *is* its local transform (rest is
|
||||
## merely the default), so this is a plain parent-relative conversion.
|
||||
func _set_global_rot(skel: Skeleton3D, idx: int, g_parent: Quaternion,
|
||||
g_target: Quaternion, w: float) -> void:
|
||||
_blend_local(skel, idx, g_parent.inverse() * g_target, w)
|
||||
|
||||
# Compose a skeleton-space rotation onto a bone's animated local pose.
|
||||
func _add_space(skel: Skeleton3D, idx: int, q_space: Quaternion) -> void:
|
||||
if idx < 0:
|
||||
|
||||
Reference in New Issue
Block a user