feat: real two-hand rifle hold — armed animations finally read as a shooter
The clips were playing all along, but every locomotion state used unarmed jog arms while the gun floated at the hip, so nothing looked animated. - ShooterPoseModifier now REPLACES the arm chain with a direction-based FK rifle hold (low-ready at hip, shouldered on ADS) instead of nudging the unarmed clip additively; the wrist is solved so the muzzle points exactly along the aim line, gun kept upright (no-roll constraint). - Per-arm release logic: reload/throw/hit one-shots, dance, death, the slide ground-brace, wall-run reach and grapple free the authored clips. - Third-person weapon: fixed re-hide on weapon swap while in third person, scaled to character proportions, gripped along the hand. - Armed idle uses Idle (arms owned by the hold) instead of arms-crossed PistolIdle base. - FP viewmodel arms: character-styled sleeve + teal cuff + skin hand instead of the blue slabs. - debug/anim_capture.gd: front+side screenshots of every movement state, one-shot action and ADS for visual regression of the model pipeline. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
39af109893
commit
6c227b5538
@@ -93,6 +93,7 @@ var _cur_fwd: float = 0.0
|
||||
var _cur_ads: float = 0.0
|
||||
var _cur_slide: float = 0.0
|
||||
var _cur_wall: float = 0.0
|
||||
var _owner_visible: bool = false
|
||||
const POSE_SMOOTH := 10.0
|
||||
|
||||
|
||||
@@ -197,6 +198,7 @@ func _find_clip(available: PackedStringArray, wanted: String) -> String:
|
||||
## toggle calls this with on=true to reveal the full animated model. Either way
|
||||
## the model keeps casting shadows and stays visible to other players.
|
||||
func set_owner_visible(on: bool) -> void:
|
||||
_owner_visible = on
|
||||
var mode := GeometryInstance3D.SHADOW_CASTING_SETTING_ON if on \
|
||||
else GeometryInstance3D.SHADOW_CASTING_SETTING_SHADOWS_ONLY
|
||||
_set_shadow_mode_recursive(self, mode)
|
||||
@@ -283,9 +285,8 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
|
||||
clip = "Run"
|
||||
elif speed > 0.5:
|
||||
clip = "Walk"
|
||||
elif is_holding_weapon and _resolved_clips.has("PistolIdle"):
|
||||
# Armed idle: weapon actually held ready instead of arms-down
|
||||
clip = "PistolIdle"
|
||||
# Armed idle uses the plain Idle clip — the rifle-hold pose layer
|
||||
# owns the arms, so the odd arms-crossed PistolIdle base reads worse.
|
||||
"air":
|
||||
# Rising = jump, falling = the fall loop.
|
||||
clip = "Jump" if _vertical_speed() > 0.5 else "Fall"
|
||||
@@ -351,6 +352,30 @@ func _process(delta: float) -> void:
|
||||
_pose_mod.slide = _cur_slide
|
||||
_pose_mod.wall = _cur_wall
|
||||
|
||||
# Two-hand rifle hold: owns the arms whenever a weapon is held, EXCEPT when
|
||||
# a one-shot clip (reload/throw/hit), an emote, or a state whose arms matter
|
||||
# (death, dance) needs the authored animation to read through.
|
||||
var st: String = _pose_mod.state
|
||||
var clip_owns_arms := _oneshot_lock > 0.0 or _dancing or st == "death"
|
||||
var hold_r := 0.0
|
||||
var hold_l := 0.0
|
||||
if is_holding_weapon and not clip_owns_arms:
|
||||
hold_r = 1.0
|
||||
hold_l = 1.0
|
||||
match st:
|
||||
"slide":
|
||||
hold_l = 0.0 # trailing arm braces the ground
|
||||
"wall_run":
|
||||
# The wall-side arm reaches for the wall.
|
||||
if _cur_wall > 0.05:
|
||||
hold_r = 0.0
|
||||
elif _cur_wall < -0.05:
|
||||
hold_l = 0.0
|
||||
"grapple":
|
||||
hold_l = 0.0 # left hand rides the grapple line
|
||||
_pose_mod.hold_r_target = hold_r
|
||||
_pose_mod.hold_l_target = hold_l
|
||||
|
||||
|
||||
func _play_clip(canonical: String, restart: bool = false) -> void:
|
||||
if not animation_player or not _resolved_clips.has(canonical):
|
||||
@@ -395,13 +420,21 @@ func set_weapon(script_path: String) -> void:
|
||||
w.set_process_input(false)
|
||||
# Owner's first-person view must not see their own held weapon (it
|
||||
# sits right in front of the lens as a huge blob) — shadows only,
|
||||
# same as the body. set_owner_visible reveals it in third person.
|
||||
if shadows_only or first_person_mode:
|
||||
# same as the body. Skip when the owner is already in third person
|
||||
# (weapon swap while toggled), else the new weapon comes up invisible.
|
||||
if (shadows_only or first_person_mode) and not _owner_visible:
|
||||
_set_shadows_recursive(w)
|
||||
# Undo the first-person viewmodel placement from the weapon's _ready.
|
||||
w.position = Vector3(0.0, 0.08, 0.03)
|
||||
w.rotation_degrees = Vector3(0, 90, 0)
|
||||
w.scale = Vector3(0.8, 0.8, 0.8)
|
||||
# Undo the first-person viewmodel placement from the weapon's _ready:
|
||||
# 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)
|
||||
# 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)
|
||||
)
|
||||
|
||||
var hand_idx := _find_bone(["RightHand", "Hand_R", "hand.R"])
|
||||
@@ -468,6 +501,15 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
||||
var recoil: float = 0.0 # decaying shot kick
|
||||
var state: String = "idle"
|
||||
var weapon_held: bool = false
|
||||
# Per-arm rifle-hold weights (0 = clip owns the arm, 1 = hold pose owns it).
|
||||
var hold_r_target: float = 0.0
|
||||
var hold_l_target: float = 0.0
|
||||
var _hold_r: float = 0.0
|
||||
var _hold_l: float = 0.0
|
||||
var _time: float = 0.0
|
||||
# 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
|
||||
|
||||
# Tuning (radians). Positive pitch leans forward; positive roll leans right.
|
||||
const LEAN_ROLL := 0.30
|
||||
@@ -478,9 +520,7 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
||||
const SLIDE_KNEE := 0.55 # shins straighten against the crouch clip's bend
|
||||
const WALL_PITCH := 0.2 # forward drive lean during a wall run
|
||||
const WALL_ARM_OUT := 0.9 # inner arm reaches out to touch the wall
|
||||
const ADS_LIFT := 1.05 # upper-arm raise toward aim at full ADS
|
||||
const ADS_SWING := 0.6 # swing arms in toward centre-front on ADS
|
||||
const ADS_FOREARM := 0.55 # forearm bend to bring the weapon up on ADS
|
||||
const HOLD_SMOOTH := 8.0 # how fast the hold takes/releases the arms
|
||||
|
||||
const SPINE := ["DEF-hips", "DEF-spine.001", "DEF-spine.002", "DEF-spine.003"]
|
||||
|
||||
@@ -503,6 +543,12 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
||||
return
|
||||
if not _resolved:
|
||||
_resolve()
|
||||
var delta := get_physics_process_delta_time() if Engine.is_in_physics_frame() \
|
||||
else get_process_delta_time()
|
||||
_time += delta
|
||||
var t := 1.0 - exp(-HOLD_SMOOTH * delta)
|
||||
_hold_r = lerpf(_hold_r, hold_r_target, t)
|
||||
_hold_l = lerpf(_hold_l, hold_l_target, t)
|
||||
|
||||
_apply_lean(skel)
|
||||
if absf(aim_pitch) > 0.01:
|
||||
@@ -511,8 +557,8 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
||||
_apply_slide(skel)
|
||||
if absf(wall) > 0.01:
|
||||
_apply_wall_lean(skel)
|
||||
if weapon_held:
|
||||
_apply_weapon(skel)
|
||||
if _hold_r > 0.01 or _hold_l > 0.01:
|
||||
_apply_rifle_hold(skel)
|
||||
if recoil > 0.01:
|
||||
_apply_recoil(skel)
|
||||
recoil = lerpf(recoil, 0.0, 0.25)
|
||||
@@ -592,24 +638,140 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
||||
_add_space(skel, _idx.get("DEF-upper_arm.L", -1), brace)
|
||||
_add_space(skel, _idx.get("DEF-forearm.L", -1), Quaternion(Vector3(1, 0, 0), 0.35 * slide))
|
||||
|
||||
# Weapon hold. At the hip the base clip already keeps the arms down with the
|
||||
# weapon (attached to the hand) at the side, so we leave it alone. On ADS we
|
||||
# additively lift both arms forward-up toward an aiming pose. Rotations are
|
||||
# about the skeleton's X axis (the shoulder line), so the down arms swing
|
||||
# forward to eye level.
|
||||
func _apply_weapon(skel: Skeleton3D) -> void:
|
||||
if ads < 0.01:
|
||||
return
|
||||
# Raise about X (down arm -> forward) and swing about Y so each arm comes
|
||||
# IN toward centre-front instead of splaying out to the side. Right arm
|
||||
# is on -X so it swings +Y; the left mirrors it.
|
||||
var lift := Quaternion(Vector3(1, 0, 0), -ADS_LIFT * ads)
|
||||
var swing := ADS_SWING * ads
|
||||
_add_space(skel, _idx.get("DEF-upper_arm.R", -1), Quaternion(Vector3(0, 1, 0), swing) * lift)
|
||||
_add_space(skel, _idx.get("DEF-upper_arm.L", -1), Quaternion(Vector3(0, 1, 0), -swing) * lift)
|
||||
var bend := Quaternion(Vector3(1, 0, 0), -ADS_FOREARM * ads)
|
||||
_add_space(skel, _idx.get("DEF-forearm.R", -1), bend)
|
||||
_add_space(skel, _idx.get("DEF-forearm.L", -1), bend)
|
||||
# ── 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.
|
||||
|
||||
# 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
|
||||
|
||||
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: 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()
|
||||
|
||||
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)
|
||||
|
||||
# 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.
|
||||
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 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)
|
||||
|
||||
# Support hand: follow the forearm line with a fixed palm twist.
|
||||
if _hold_l > 0.001 and g_fa_l != Quaternion.IDENTITY:
|
||||
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)
|
||||
|
||||
# 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
|
||||
# the target dir, plus a twist about that line) ⊕ rest, each local pose
|
||||
# derived against the parent's posed global so there is no drift.
|
||||
# Returns the forearm's target global rotation (IDENTITY when skipped).
|
||||
func _aim_chain(skel: Skeleton3D, ua_name: String, fa_name: String,
|
||||
ua_dir: Vector3, fa_dir: Vector3, ua_twist: float,
|
||||
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)
|
||||
if ua < 0 or fa < 0:
|
||||
return Quaternion.IDENTITY
|
||||
|
||||
# Bone lines at rest (upper arm -> forearm -> hand joint origins).
|
||||
var ua_o := skel.get_bone_global_rest(ua).origin
|
||||
var fa_o := skel.get_bone_global_rest(fa).origin
|
||||
var fa_children := skel.get_bone_children(fa)
|
||||
var fa_tip := skel.get_bone_global_rest(fa_children[0]).origin \
|
||||
if fa_children.size() > 0 else fa_o + (fa_o - ua_o)
|
||||
var ua_rest_dir := (fa_o - ua_o).normalized()
|
||||
var fa_rest_dir := (fa_tip - fa_o).normalized()
|
||||
|
||||
var ua_rest_q := skel.get_bone_global_rest(ua).basis.get_rotation_quaternion()
|
||||
var fa_rest_q := skel.get_bone_global_rest(fa).basis.get_rotation_quaternion()
|
||||
|
||||
# Desired global rotations.
|
||||
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.
|
||||
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)
|
||||
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()
|
||||
print("CHAIN DEBUG w=", w,
|
||||
" ua_t=", g_ua, " ua_a=", ua_actual,
|
||||
" | fa_t=", g_fa, " fa_a=", fa_actual,
|
||||
" | ua_dir_t=", ua_dir, " ua_dir_a=", ua_actual * (ua_rest_q.inverse() * ua_rest_dir))
|
||||
return g_fa
|
||||
|
||||
func _blend_local(skel: Skeleton3D, idx: int, target: Quaternion, w: float) -> void:
|
||||
skel.set_bone_pose_rotation(idx,
|
||||
skel.get_bone_pose_rotation(idx).slerp(target.normalized(), w))
|
||||
|
||||
# Compose a skeleton-space rotation onto a bone's animated local pose.
|
||||
func _add_space(skel: Skeleton3D, idx: int, q_space: Quaternion) -> void:
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
extends SceneTree
|
||||
|
||||
## Dev tool: screenshot every animation state on the active GLB skin.
|
||||
## Run:
|
||||
## godot --path . --windowed --resolution 1280x720 -s res://debug/anim_capture.gd -- <out_dir> [skin_id]
|
||||
## Saves anim_<state>.png per state plus mid-action shots for reload/throw.
|
||||
|
||||
var _frames := 0
|
||||
var _out_dir := "."
|
||||
var _skin := "miku"
|
||||
var _player: Node = null
|
||||
var _model: Node = null
|
||||
var _cam: Camera3D = null
|
||||
|
||||
# [tag, state, speed, crouch, hold_frames]
|
||||
var _states := [
|
||||
["idle", "idle", 0.0, false, 50],
|
||||
["walk", "ground", 2.2, false, 50],
|
||||
["run", "ground", 6.5, false, 50],
|
||||
["sprint", "ground", 12.0, false, 50],
|
||||
["crouch", "ground", 0.0, true, 50],
|
||||
["crouchwalk", "ground", 2.0, true, 50],
|
||||
["fall", "air", 4.0, false, 50],
|
||||
["slide", "slide", 10.0, true, 50],
|
||||
["dash", "dash", 14.0, false, 30],
|
||||
["wallrun", "wall_run", 9.0, false, 50],
|
||||
["grapple", "grapple", 10.0, false, 50],
|
||||
["dance", "idle", 0.0, false, 60],
|
||||
]
|
||||
var _phase := 0 # index into _states, then actions after
|
||||
var _phase_frame := 0
|
||||
var _actions := [["reload", 30], ["throw", 18]]
|
||||
var _action_i := 0
|
||||
var _mode := "states" # states -> actions -> ads -> done
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var args := OS.get_cmdline_user_args()
|
||||
if args.size() > 0:
|
||||
_out_dir = args[0]
|
||||
if args.size() > 1:
|
||||
_skin = args[1]
|
||||
change_scene_to_file("res://ui/main_menu/main_menu.tscn")
|
||||
|
||||
|
||||
func _process(_delta: float) -> bool:
|
||||
_frames += 1
|
||||
if _frames == 40:
|
||||
var sm = root.get_node_or_null("SkinManager")
|
||||
if sm:
|
||||
sm.set_active_skin(_skin)
|
||||
var nm = root.get_node_or_null("NetworkManager")
|
||||
if nm and nm.has_method("start_singleplayer_match"):
|
||||
nm.start_singleplayer_match("Deathmatch")
|
||||
change_scene_to_file("res://scenes/maps/test_level/test_level.tscn")
|
||||
return false
|
||||
if _frames < 160:
|
||||
return false
|
||||
if _frames == 160:
|
||||
for p in root.find_children("*", "CharacterBody3D", true, false):
|
||||
if p.has_method("get_visual_model") and p.is_multiplayer_authority():
|
||||
_player = p
|
||||
break
|
||||
if not _player:
|
||||
printerr("anim_capture: no player found")
|
||||
return true
|
||||
_model = _player.get_node_or_null("SkinnedModel")
|
||||
if not _model:
|
||||
printerr("anim_capture: no SkinnedModel (skin '%s' active?)" % _skin)
|
||||
return true
|
||||
# Freeze gameplay driving so we control the animation state directly.
|
||||
_player.set_physics_process(false)
|
||||
_player.set_process(false)
|
||||
_model.set_owner_visible(true)
|
||||
# Give the model a weapon so armed poses read.
|
||||
if _model.has_method("set_weapon"):
|
||||
_model.set_weapon("res://weapons/ak47.gd")
|
||||
# Camera: repositioned per shot (front + side). Model faces -Z.
|
||||
_cam = Camera3D.new()
|
||||
_cam.cull_mask &= ~(1 << 19) # don't render the FP viewmodel layer
|
||||
current_scene.add_child(_cam)
|
||||
_cam_to(Vector3(0.4, 0.9, -2.4))
|
||||
_cam.current = true
|
||||
return false
|
||||
|
||||
# Drive the forced state every frame so blends settle. Front shot two
|
||||
# frames before the hold ends, side shot on the last frame (the camera
|
||||
# hop needs a real rendered frame in between).
|
||||
if _mode == "states":
|
||||
var s: Array = _states[_phase]
|
||||
if s[0] == "dance" and _model.has_method("set_dancing"):
|
||||
_model.set_dancing(true)
|
||||
_model.update_state(s[1], s[2], s[3])
|
||||
_phase_frame += 1
|
||||
var hold: int = int(s[4])
|
||||
if _phase_frame == hold - 2:
|
||||
_snap("anim_" + String(s[0]) + "_f")
|
||||
_cam_to(Vector3(2.4, 0.9, -0.4))
|
||||
elif _phase_frame >= hold:
|
||||
_snap("anim_" + String(s[0]) + "_s")
|
||||
_cam_to(Vector3(0.4, 0.9, -2.4))
|
||||
if s[0] == "dance" and _model.has_method("set_dancing"):
|
||||
_model.set_dancing(false)
|
||||
if s[0] == "idle":
|
||||
_debug_gun()
|
||||
_phase_frame = 0
|
||||
_phase += 1
|
||||
if _phase >= _states.size():
|
||||
_mode = "actions"
|
||||
return false
|
||||
if _mode == "actions":
|
||||
var a: Array = _actions[_action_i]
|
||||
if _phase_frame == 0:
|
||||
_model.play_action(a[0])
|
||||
_model.update_state("idle", 0.0, false)
|
||||
_phase_frame += 1
|
||||
var hold_a: int = int(a[1])
|
||||
if _phase_frame == hold_a - 2:
|
||||
_snap("anim_" + String(a[0]) + "_f")
|
||||
_cam_to(Vector3(2.4, 0.9, -0.4))
|
||||
elif _phase_frame >= hold_a:
|
||||
_snap("anim_" + String(a[0]) + "_s")
|
||||
_cam_to(Vector3(0.4, 0.9, -2.4))
|
||||
_phase_frame = 0
|
||||
_action_i += 1
|
||||
if _action_i >= _actions.size():
|
||||
_mode = "ads"
|
||||
return false
|
||||
if _mode == "ads":
|
||||
_model.update_state("idle", 0.0, false)
|
||||
if _model.has_method("set_locomotion"):
|
||||
_model.set_locomotion(0.0, 0.0, 1.0)
|
||||
_phase_frame += 1
|
||||
if _phase_frame == 48:
|
||||
_snap("anim_ads_f")
|
||||
_cam_to(Vector3(2.4, 0.9, -0.4))
|
||||
elif _phase_frame >= 50:
|
||||
_snap("anim_ads_s")
|
||||
_debug_gun()
|
||||
return true
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _debug_gun() -> void:
|
||||
var pm = _model.get("_pose_mod")
|
||||
if not pm:
|
||||
return
|
||||
var skel: Skeleton3D = _model.skeleton
|
||||
var hand := skel.find_bone("DEF-hand.R")
|
||||
var achieved := Vector3.ZERO
|
||||
if hand >= 0 and pm.gun_fwd_hand.length_squared() > 0.01:
|
||||
achieved = skel.get_bone_global_pose(hand).basis * pm.gun_fwd_hand
|
||||
print("GUN DEBUG: fwd_hand=", pm.gun_fwd_hand, " ads=", pm.ads,
|
||||
" hold_r=", pm._hold_r, " achieved_world_fwd=", achieved)
|
||||
|
||||
|
||||
func _cam_to(offset: Vector3) -> void:
|
||||
var base: Vector3 = _player.global_position
|
||||
_cam.global_position = base + offset
|
||||
_cam.look_at(base + Vector3(0, 0.25, 0), Vector3.UP)
|
||||
|
||||
|
||||
func _snap(tag: String) -> void:
|
||||
var img := root.get_viewport().get_texture().get_image()
|
||||
var path := _out_dir + "/" + tag + ".png"
|
||||
img.save_png(path)
|
||||
print("anim_capture: saved ", path)
|
||||
+45
-38
@@ -273,47 +273,54 @@ func _set_layer_recursive(node: Node, layer_mask: int) -> void:
|
||||
|
||||
func _add_procedural_arms(weapon: Node3D) -> void:
|
||||
# Attach to weapon instead of model_root to avoid arms spinning on reload
|
||||
|
||||
var mat = StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(0.2, 0.4, 0.8) # Player color
|
||||
mat.roughness = 0.8
|
||||
|
||||
# Right Arm (Main Grip)
|
||||
var r_arm = MeshInstance3D.new()
|
||||
var r_mesh = BoxMesh.new()
|
||||
r_mesh.size = Vector3(0.08, 0.7, 0.08)
|
||||
r_mesh.material = mat
|
||||
r_arm.mesh = r_mesh
|
||||
|
||||
var r_pivot = Node3D.new()
|
||||
var r_shoulder = Vector3(0.25, -0.3, 0.5)
|
||||
var r_hand = Vector3(0.04, -0.05, 0.05)
|
||||
r_pivot.position = r_shoulder
|
||||
r_pivot.look_at_from_position(r_shoulder, r_hand, Vector3.UP)
|
||||
|
||||
r_arm.rotation_degrees.x = 90 # Mesh points UP by default, we need it to point forward along -Z
|
||||
r_arm.position.z = -0.35
|
||||
r_pivot.add_child(r_arm)
|
||||
weapon.add_child(r_pivot)
|
||||
|
||||
# Left Arm (Foregrip)
|
||||
_build_arm(weapon, Vector3(0.25, -0.3, 0.5), Vector3(0.04, -0.05, 0.05))
|
||||
if "weapon_name" in weapon and weapon.weapon_name != "Knife":
|
||||
var l_arm = MeshInstance3D.new()
|
||||
var l_mesh = BoxMesh.new()
|
||||
l_mesh.size = Vector3(0.08, 0.7, 0.08)
|
||||
l_mesh.material = mat
|
||||
l_arm.mesh = l_mesh
|
||||
_build_arm(weapon, Vector3(-0.25, -0.3, 0.4), Vector3(-0.02, -0.02, -0.3))
|
||||
|
||||
var l_pivot = Node3D.new()
|
||||
var l_shoulder = Vector3(-0.25, -0.3, 0.4)
|
||||
var l_hand = Vector3(-0.02, -0.02, -0.3)
|
||||
l_pivot.position = l_shoulder
|
||||
l_pivot.look_at_from_position(l_shoulder, l_hand, Vector3.UP)
|
||||
|
||||
l_arm.rotation_degrees.x = 90
|
||||
l_arm.position.z = -0.35
|
||||
l_pivot.add_child(l_arm)
|
||||
weapon.add_child(l_pivot)
|
||||
## A first-person arm styled after the character skin: dark detached sleeve,
|
||||
## glowing cuff, bare hand — instead of the old featureless blue slab.
|
||||
func _build_arm(weapon: Node3D, shoulder: Vector3, hand: Vector3) -> void:
|
||||
var sleeve_mat = StandardMaterial3D.new()
|
||||
sleeve_mat.albedo_color = Color(0.10, 0.11, 0.14) # near-black sleeve
|
||||
sleeve_mat.roughness = 0.8
|
||||
var cuff_mat = StandardMaterial3D.new()
|
||||
cuff_mat.albedo_color = Color(0.25, 0.95, 0.90) # signature teal cuff
|
||||
cuff_mat.emission_enabled = true
|
||||
cuff_mat.emission = Color(0.10, 0.55, 0.52)
|
||||
var skin_mat = StandardMaterial3D.new()
|
||||
skin_mat.albedo_color = Color(0.98, 0.88, 0.82) # skin
|
||||
skin_mat.roughness = 0.9
|
||||
|
||||
var pivot = Node3D.new()
|
||||
pivot.position = shoulder
|
||||
pivot.look_at_from_position(shoulder, hand, Vector3.UP)
|
||||
|
||||
var sleeve = MeshInstance3D.new()
|
||||
var sleeve_mesh = BoxMesh.new()
|
||||
sleeve_mesh.size = Vector3(0.075, 0.075, 0.46)
|
||||
sleeve_mesh.material = sleeve_mat
|
||||
sleeve.mesh = sleeve_mesh
|
||||
sleeve.position.z = -0.27
|
||||
pivot.add_child(sleeve)
|
||||
|
||||
var cuff = MeshInstance3D.new()
|
||||
var cuff_mesh = BoxMesh.new()
|
||||
cuff_mesh.size = Vector3(0.085, 0.085, 0.05)
|
||||
cuff_mesh.material = cuff_mat
|
||||
cuff.mesh = cuff_mesh
|
||||
cuff.position.z = -0.52
|
||||
pivot.add_child(cuff)
|
||||
|
||||
var hand_box = MeshInstance3D.new()
|
||||
var hand_mesh = BoxMesh.new()
|
||||
hand_mesh.size = Vector3(0.06, 0.055, 0.12)
|
||||
hand_mesh.material = skin_mat
|
||||
hand_box.mesh = hand_mesh
|
||||
hand_box.position.z = -0.60
|
||||
pivot.add_child(hand_box)
|
||||
|
||||
weapon.add_child(pivot)
|
||||
|
||||
func _equip_slot(slot: int) -> void:
|
||||
if weapons.has(active_slot):
|
||||
|
||||
Reference in New Issue
Block a user