ani fixed

This commit is contained in:
Nicholas Butzke
2026-08-10 12:17:59 -04:00
parent 9358746582
commit 833e936bcd
10 changed files with 554 additions and 23 deletions
+118 -9
View File
@@ -100,10 +100,16 @@ const BLEND_TIME := 0.36
## visible.
const BLEND_TIMES := {
"Dash": 0.20, "Jump": 0.24, "Hit": 0.14, "Land": 0.28,
"Slide": 0.30, "Death": 0.34, "Throw": 0.20, "PistolReload": 0.30,
# SlideStart has a distinct authored crouch pose; blending into it from
# airborne/standing can send the lower body through a ground-plane flip.
"SlideStart": 0.0, "Slide": 0.30, "Death": 0.34,
"Throw": 0.20, "PistolReload": 0.30,
"Idle": 0.42, "PistolIdle": 0.42, "Walk": 0.40, "Run": 0.40, "Sprint": 0.40,
"CrouchIdle": 0.40, "CrouchWalk": 0.40, "Fall": 0.32,
"WallRunLeft": 0.26, "WallRunRight": 0.26,
# The two authored wall performances start in distinct side poses. A full
# body crossfade from Fall/ground interpolates the hips and thighs through
# an upside-down midpoint, so hand off directly to the authored first pose.
"WallRunLeft": 0.0, "WallRunRight": 0.0,
"WallCling": 0.32, "Grapple": 0.32,
}
const GROUND_STATE_BLEND := 0.30
@@ -125,7 +131,10 @@ var loaded: bool = false
## manufactures poses.
var _motion_root: Node3D
var _wall_glide_velocity_world := Vector3.ZERO
var _wall_surface_normal_world := Vector3.ZERO
var _wall_surface_point_world := Vector3.ZERO
var _motion_yaw := 0.0
var _motion_was_wall_run := false
var _resolved_clips: Dictionary = {} # canonical name -> actual clip name
var _current_clip: String = ""
@@ -221,7 +230,10 @@ func load_model(path: String) -> void:
animation_player = null
_motion_root = null
_wall_glide_velocity_world = Vector3.ZERO
_wall_surface_normal_world = Vector3.ZERO
_wall_surface_point_world = Vector3.ZERO
_motion_yaw = 0.0
_motion_was_wall_run = false
_loco_blend_target = Vector2.ZERO
_loco_blend_visual = Vector2.ZERO
_loco_scale_target = 1.0
@@ -233,6 +245,7 @@ func load_model(path: String) -> void:
_current_clip = ""
_has_titanfall_motion_reference = false
_current_state_node = ""
_slide_start_remaining = 0.0
_weapon_attachment = null
_grapple_attachment = null
_grapple_shoulder_attachment = null
@@ -564,8 +577,12 @@ func _ensure_meshes_bound(scene: Node) -> void:
## loop modes (glTF has no loop flag, so we set it here).
func _index_animations() -> void:
var available := animation_player.get_animation_list()
# ReviewWallHangFront is unique to the ignored local Titanfall motion lab.
# A canonical WallRunLeft/Right pair also exists in shipping animation packs
# and must not opt those rigs into the Titanfall-specific side convention.
_has_titanfall_motion_reference = \
_find_clip(available, "ReviewWallHangFront") != ""
_normalize_rotation_track_signs()
for canonical in CLIP_FALLBACKS:
for candidate in CLIP_FALLBACKS[canonical]:
var match_name := _find_clip(available, candidate)
@@ -579,6 +596,34 @@ func _index_animations() -> void:
anim.loop_mode = Animation.LOOP_LINEAR
## Quaternions q and -q encode the same pose, but interpolation does not know
## that unless adjacent keys use a consistent sign. A few imported traversal
## tracks contain those sign changes, so Godot interpolates the long arc and
## briefly turns the waist/legs upside down on wall-run entry. Normalize every
## rotation track once after GLB import so the authored pose is preserved while
## interpolation always takes the shortest arc between keys.
func _normalize_rotation_track_signs() -> void:
for animation_name in animation_player.get_animation_list():
var animation := animation_player.get_animation(animation_name)
if animation == null:
continue
for track_index in animation.get_track_count():
if animation.track_get_type(track_index) != Animation.TYPE_ROTATION_3D:
continue
var previous := Quaternion.IDENTITY
var has_previous := false
for key_index in animation.track_get_key_count(track_index):
var value = animation.track_get_key_value(track_index, key_index)
if not value is Quaternion:
continue
var current: Quaternion = value.normalized()
if has_previous and previous.dot(current) < 0.0:
current = -current
animation.track_set_key_value(track_index, key_index, current)
previous = current
has_previous = true
## Runtime blend tree:
##
## authored cycles -> direction/speed BlendSpaces \
@@ -733,6 +778,7 @@ func _set_shadow_mode_recursive(node: Node, mode: int) -> void:
var _prev_state: String = ""
var _oneshot_lock: float = 0.0 # seconds left where a one-shot owns playback
var _slide_start_remaining: float = 0.0
var _dancing: bool = false
## Which authored entry in DanceRoutines.ROUTINES is playing.
var _dance_index: int = 0
@@ -801,6 +847,8 @@ func add_gun_recoil(strength: float = 1.0) -> void:
func update_state(state: String, speed: float, is_crouching: bool = false) -> void:
if not loaded or not animation_player:
return
var previous_state := _prev_state
var exiting_wall_run := previous_state == "wall_run" and state != "wall_run"
# One-shots (Land, Hit) own playback briefly.
if _oneshot_lock > 0.0:
@@ -816,6 +864,8 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
_prev_state = state
return
_prev_state = state
if state != "slide":
_slide_start_remaining = 0.0
var clip := "Idle"
match state:
@@ -829,12 +879,22 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
# Rising = jump, falling = the fall loop.
clip = "Jump" if _vertical_speed() > 0.5 else "Fall"
"slide":
clip = "Slide"
if previous_state != "slide":
_slide_start_remaining = _animation_length_if_distinct(
"SlideStart", "Slide") / _slide_playback_scale(speed)
if _slide_start_remaining > 0.0:
clip = "SlideStart"
else:
clip = "Slide"
"wall_run":
# The jets still own propulsion, but the authored pilot pack supplies a
# distinct compact wall-performance for each side. This is body motion,
# not gameplay root motion or procedural feet planted against the wall.
if _has_titanfall_motion_reference:
# The imported pair is mirrored relative to gameplay's wall-side
# convention: the clip named Right is the left-wall performance and
# vice versa. Keep that conversion here rather than swapping physics
# wall_side values, which would break camera tilt and networking.
clip = "WallRunRight" if _target_wall < 0.0 else "WallRunLeft"
else:
clip = "Grapple"
@@ -850,7 +910,10 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
clip = "Death"
if clip != "":
_play_clip(clip)
# A wall jump hands the full body from a side performance to Jump/Fall.
# Interpolating those two poses can put the thighs through the horizontal
# midpoint, which reads as a delayed leg flip on the jump frame.
_play_clip(clip, false, 0.0 if exiting_wall_run else -1.0)
if _pose_mod:
_pose_mod.state = state
@@ -862,8 +925,8 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
# gameplay speed; never synthesize a different pose.
if _anim_tree and clip != "":
var scale := 1.0
if clip == "Slide":
scale = clampf(speed / run_anim_reference_speed, 0.75, 1.8)
if clip in ["SlideStart", "Slide"]:
scale = _slide_playback_scale(speed)
elif clip == "Grapple" or clip in ["WallRunLeft", "WallRunRight"]:
# Grapple is held, and the wall clips are compact propulsion loops.
# Neither should become a frantic treadmill at high traversal speed.
@@ -888,6 +951,16 @@ func set_wall_side(side: float) -> void:
_target_wall = clampf(side, -1.0, 1.0)
## Retain the live wall plane as traversal telemetry. The authored animation
## remains untouched; this is available to presentation/debug consumers only.
## `normal_world` points from the wall toward the player.
func set_wall_surface(normal_world: Vector3,
contact_point_world: Vector3 = Vector3.ZERO) -> void:
_wall_surface_normal_world = normal_world.normalized() \
if normal_world.length_squared() > 0.0001 else Vector3.ZERO
_wall_surface_point_world = contact_point_world
## Gameplay velocity aligns the held glide silhouette with the live wall
## tangent. The mechanic still reports `wall_run` internally for save/network
## compatibility, but the character presentation is entirely propulsion-based.
@@ -998,6 +1071,8 @@ var _lod_timer: float = 0.0
func _process(delta: float) -> void:
_update_cloth_lod(delta)
_update_motion_orientation(delta)
if _slide_start_remaining > 0.0:
_slide_start_remaining = maxf(_slide_start_remaining - delta, 0.0)
_update_mecha_nozzles()
_update_animation_smoothing(delta)
if not _pose_mod:
@@ -1068,6 +1143,17 @@ func _process(delta: float) -> void:
func _update_motion_orientation(delta: float) -> void:
if not is_instance_valid(_motion_root):
return
var wall_run_active := _pose_mod != null and _pose_mod.state == "wall_run"
if not wall_run_active:
# This root is only a presentation frame for lateral wall-run clips.
# Blending it back to zero makes the whole character visibly spin on the
# floor when a wall run ends into a slide/ground state.
if _motion_was_wall_run:
_motion_yaw = 0.0
_motion_root.rotation.y = 0.0
_motion_was_wall_run = false
return
_motion_was_wall_run = true
var wanted_yaw := 0.0
if _pose_mod and _pose_mod.state == "wall_run":
var local_velocity := global_transform.basis.inverse() \
@@ -1083,6 +1169,24 @@ func _update_motion_orientation(delta: float) -> void:
_motion_root.rotation.y = _motion_yaw
func _animation_length_if_distinct(start: String, loop: String) -> float:
if not _resolved_clips.has(start) or not _resolved_clips.has(loop):
return 0.0
var start_name: String = _resolved_clips[start]
var loop_name: String = _resolved_clips[loop]
if start_name == loop_name:
return 0.0
var clip := animation_player.get_animation(start_name)
return clip.length if clip else 0.0
func _slide_playback_scale(speed: float) -> float:
# SlideStart is part of the same momentum gesture as Slide. It must advance
# with the entry velocity too, otherwise a fast slide spends a full authored
# second in a slow crouch pose before the loop catches up.
return clampf(speed / maxf(run_anim_reference_speed, 0.01), 0.75, 1.8)
## Pick the cloth solver's detail level from how far the camera is.
##
## Re-checked a few times a second rather than every frame: the answer changes
@@ -1142,7 +1246,10 @@ func _play_ground_locomotion(speed: float, crouched: bool) -> void:
return
var state_node := "CrouchLocomotion" if crouched else "Locomotion"
if _current_state_node != state_node:
_state_trans.xfade_time = GROUND_STATE_BLEND
var exiting_wall_run := _current_state_node in [
"WallRunLeft", "WallRunRight"]
_state_trans.xfade_time = 0.0 if exiting_wall_run \
else GROUND_STATE_BLEND
_anim_tree.set("parameters/state/transition_request", state_node)
_current_state_node = state_node
@@ -1193,13 +1300,15 @@ func _play_ground_locomotion(speed: float, crouched: bool) -> void:
_current_clip = _resolved_clips.get(canonical, "")
func _play_clip(canonical: String, restart: bool = false) -> void:
func _play_clip(canonical: String, restart: bool = false,
blend_override: float = -1.0) -> void:
if not _anim_tree or not _resolved_clips.has(canonical):
return
var clip_name: String = _resolved_clips[canonical]
if not restart and _current_state_node == clip_name:
return
_state_trans.xfade_time = BLEND_TIMES.get(canonical, BLEND_TIME)
_state_trans.xfade_time = blend_override \
if blend_override >= 0.0 else BLEND_TIMES.get(canonical, BLEND_TIME)
_anim_tree.set("parameters/state/transition_request", clip_name)
_current_state_node = clip_name
_current_clip = clip_name