|
|
|
@@ -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:
|
|
|
|
|