ani fixed
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user