From 650e7c9eb458e32f0720f1f6cad9fc04ca53b123 Mon Sep 17 00:00:00 2001 From: robtfm <50659922+robtfm@users.noreply.github.com> Date: Thu, 15 Aug 2024 16:10:23 +0100 Subject: [PATCH] apply finished animations (#14743) # Objective fix #14742 ## Solution the issue arises because "finished" animations (where current time >= last keyframe time) are not applied at all. when transitioning from a finished animation to another later-indexed anim, the transition kind-of works because the finished anim is skipped, then the new anim is applied with a lower weight (weight / total_weight) when transitioning from a finished animation to another earlier-indexed anim, the transition is instant as the new anim is applied with 1.0 (as weight == total_weight for the first applied), then the finished animation is skipped. to fix this we can always apply every animation based on the nearest 2 keyframes, and clamp the interpolation between them to [0,1]. pros: - finished animations can be transitioned out of correctly - blended animations where some curves have a last-keyframe before the end of the animation will blend properly - animations will actually finish on their last keyframe, rather than a fraction of a render-frame before the end cons: - we have to re-apply finished animations every frame whether it's necessary or not. i can't see a way to avoid this. --- crates/bevy_animation/src/lib.rs | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 42ff5afe90..7ab4ab7a92 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -155,6 +155,29 @@ impl VariableCurve { Some(step_start) } + + /// Find the index of the keyframe at or before the current time. + /// + /// Returns the first keyframe if the `seek_time` is before the first keyframe, and + /// the second-to-last keyframe if the `seek_time` is after the last keyframe. + /// Panics if there are less than 2 keyframes. + pub fn find_interpolation_start_keyframe(&self, seek_time: f32) -> usize { + // An Ok(keyframe_index) result means an exact result was found by binary search + // An Err result means the keyframe was not found, and the index is the keyframe + // PERF: finding the current keyframe can be optimised + let search_result = self + .keyframe_timestamps + .binary_search_by(|probe| probe.partial_cmp(&seek_time).unwrap()); + + // We want to find the index of the keyframe before the current time + // If the keyframe is past the second-to-last keyframe, the animation cannot be interpolated. + match search_result { + // An exact match was found + Ok(i) => i.clamp(0, self.keyframe_timestamps.len() - 2), + // No exact match was found, so return the previous keyframe to interpolate from. + Err(i) => (i.saturating_sub(1)).clamp(0, self.keyframe_timestamps.len() - 2), + } + } } /// Interpolation method to use between keyframes. @@ -877,15 +900,13 @@ impl AnimationTargetContext<'_> { continue; } - // Find the current keyframe - let Some(step_start) = curve.find_current_keyframe(seek_time) else { - continue; - }; + // Find the best keyframe to interpolate from + let step_start = curve.find_interpolation_start_keyframe(seek_time); let timestamp_start = curve.keyframe_timestamps[step_start]; let timestamp_end = curve.keyframe_timestamps[step_start + 1]; // Compute how far we are through the keyframe, normalized to [0, 1] - let lerp = f32::inverse_lerp(timestamp_start, timestamp_end, seek_time); + let lerp = f32::inverse_lerp(timestamp_start, timestamp_end, seek_time).clamp(0.0, 1.0); self.apply_tweened_keyframe( curve,