diff --git a/Cargo.toml b/Cargo.toml index 9e8551ffc3daf..5c632733ec115 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -481,6 +481,11 @@ crate-type = ["cdylib"] name = "android" path = "examples/android/android.rs" +# Animation +[[example]] +name = "smooth_curves" +path = "examples/animations/smooth_curves.rs" + [package.metadata.android] apk_label = "Bevy Example" assets = "assets" diff --git a/benches/Cargo.toml b/benches/Cargo.toml index 3d13d202eff82..5f0c9a273ead4 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -9,6 +9,7 @@ edition = "2018" [dev-dependencies] criterion = "0.3" +rand = "0.8.3" bevy = { path = "../" } [[bench]] @@ -25,3 +26,8 @@ harness = false name = "iter" path = "benches/bevy_tasks/iter.rs" harness = false + +[[bench]] +name = "curves" +path = "benches/bevy_math/curves.rs" +harness = false diff --git a/benches/benches/bevy_math/curves.rs b/benches/benches/bevy_math/curves.rs new file mode 100644 index 0000000000000..56985c3c1e18a --- /dev/null +++ b/benches/benches/bevy_math/curves.rs @@ -0,0 +1,58 @@ +use bevy::math::{ + curves::{Curve, KeyframeIndex, CurveVariable}, + Vec4, +}; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use rand::prelude::*; + +criterion_group!(benches, curve_variable); +criterion_main!(benches); + +const SAMPLES_COUNT: usize = 100; + +fn curve_sampling(samples: &[f32], curve: &impl Curve) { + let mut c: KeyframeIndex = 0; + for t in samples { + let (nc, v) = curve.sample_with_cursor(c, *t); + black_box(v); + c = nc; + } +} + +fn curve_variable(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group("curve_variable"); + group.warm_up_time(std::time::Duration::from_millis(500)); + group.measurement_time(std::time::Duration::from_secs(3)); + + let curve = CurveVariable::with_auto_tangents( + vec![0.0, 1.0, 1.3, 1.6, 1.7, 1.8, 1.9, 2.0], + vec![3.0, 0.0, 1.0, 0.0, 0.5, 0.0, 0.25, 0.0] + .iter() + .map(|x| Vec4::splat(*x)) + .collect::>(), + ) + .unwrap(); + + let duration = curve.duration(); + let mut rand = rand::thread_rng(); + + let rand_samples = (0..SAMPLES_COUNT) + .into_iter() + .map(|_| duration * rand.gen::()) + .collect::>(); + + let samples = (0..SAMPLES_COUNT) + .into_iter() + .map(|i| duration * (i as f32) / (SAMPLES_COUNT - 1) as f32) + .collect::>(); + + group.bench_function("random_sampling", |bencher| { + bencher.iter(|| black_box(curve_sampling(&rand_samples[..], &curve))); + }); + + group.bench_function("sampling", |bencher| { + bencher.iter(|| black_box(curve_sampling(&samples[..], &curve))); + }); + + group.finish() +} diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index 52a0510805714..001e5d0999c7c 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -25,6 +25,7 @@ bevy_log = { path = "../bevy_log", version = "0.5.0" } bevy_reflect = { path = "../bevy_reflect", version = "0.5.0", features = ["bevy"] } bevy_tasks = { path = "../bevy_tasks", version = "0.5.0" } bevy_utils = { path = "../bevy_utils", version = "0.5.0" } +bevy_math = { path = "../bevy_math", version = "0.5.0" } # other serde = { version = "1", features = ["derive"] } diff --git a/crates/bevy_asset/src/handle.rs b/crates/bevy_asset/src/handle.rs index ae2bb45c0837c..8acbce82e18da 100644 --- a/crates/bevy_asset/src/handle.rs +++ b/crates/bevy_asset/src/handle.rs @@ -10,6 +10,7 @@ use crate::{ Asset, Assets, }; use bevy_ecs::reflect::ReflectComponent; +use bevy_math::interpolation::{utils::step_unclamped, Interpolate, Lerp, TangentIgnore}; use bevy_reflect::{Reflect, ReflectDeserialize}; use bevy_utils::Uuid; use crossbeam_channel::{Receiver, Sender}; @@ -230,6 +231,34 @@ impl Clone for Handle { } } +impl Lerp for Handle { + #[inline(always)] + fn lerp_unclamped(a: &Self, b: &Self, t: f32) -> Self { + step_unclamped(a, b, t) + } +} + +impl Interpolate for Handle { + type Tangent = TangentIgnore; + const FLAT_TANGENT: Self::Tangent = TangentIgnore; + + fn interpolate_unclamped( + k0: &Self, + _: &Self::Tangent, + k1: &Self, + _: &Self::Tangent, + _: bevy_math::interpolation::Interpolation, + t: f32, + _: f32, + ) -> Self { + step_unclamped(k0, k1, t) + } + + fn auto_tangent(_: f32, _: f32, _: f32, _: &Self, _: &Self, _: &Self) -> Self::Tangent { + TangentIgnore + } +} + /// A non-generic version of [Handle] /// /// This allows handles to be mingled in a cross asset context. For example, storing `Handle` and @@ -337,6 +366,34 @@ impl Clone for HandleUntyped { } } +impl Lerp for HandleUntyped { + #[inline(always)] + fn lerp_unclamped(a: &Self, b: &Self, t: f32) -> Self { + step_unclamped(a, b, t) + } +} + +impl Interpolate for HandleUntyped { + type Tangent = TangentIgnore; + const FLAT_TANGENT: Self::Tangent = TangentIgnore; + + fn interpolate_unclamped( + k0: &Self, + _: &Self::Tangent, + k1: &Self, + _: &Self::Tangent, + _: bevy_math::interpolation::Interpolation, + t: f32, + _: f32, + ) -> Self { + step_unclamped(k0, k1, t) + } + + fn auto_tangent(_: f32, _: f32, _: f32, _: &Self, _: &Self, _: &Self) -> Self::Tangent { + TangentIgnore + } +} + pub(crate) enum RefChange { Increment(HandleId), Decrement(HandleId), diff --git a/crates/bevy_math/Cargo.toml b/crates/bevy_math/Cargo.toml index 81823421b4c5f..d253793ba2919 100644 --- a/crates/bevy_math/Cargo.toml +++ b/crates/bevy_math/Cargo.toml @@ -1,17 +1,18 @@ -[package] -name = "bevy_math" -version = "0.5.0" -edition = "2018" -authors = [ - "Bevy Contributors ", - "Carter Anderson ", -] -description = "Provides math functionality for Bevy Engine" -homepage = "https://bevyengine.org" -repository = "https://github.com/bevyengine/bevy" -license = "MIT" -keywords = ["bevy"] - -[dependencies] -glam = { version = "0.15.1", features = ["serde", "bytemuck"] } -bevy_reflect = { path = "../bevy_reflect", version = "0.5.0", features = ["bevy"] } +[package] +name = "bevy_math" +version = "0.5.0" +edition = "2018" +authors = [ + "Bevy Contributors ", + "Carter Anderson ", +] +description = "Provides math functionality for Bevy Engine" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT" +keywords = ["bevy"] + +[dependencies] +glam = { version = "0.15.1", features = ["serde", "bytemuck"] } +bevy_reflect = { path = "../bevy_reflect", version = "0.5.0", features = ["bevy"] } +thiserror = "1.0" diff --git a/crates/bevy_math/src/curves/fixed.rs b/crates/bevy_math/src/curves/fixed.rs new file mode 100644 index 0000000000000..04e4b70254c3a --- /dev/null +++ b/crates/bevy_math/src/curves/fixed.rs @@ -0,0 +1,126 @@ +use crate::{ + curves::{Curve, KeyframeIndex}, + interpolation::Lerp, +}; + +// TODO: impl Serialize, Deserialize +/// Curve with evenly spaced keyframes, in another words a curve with a fixed frame rate. +/// +/// This curve maintains the faster sampling rate over a wide range of frame rates, because +/// it doesn't rely on keyframe cursor. As a downside, it will have a bigger memory foot print. +#[derive(Default, Debug, Clone)] +pub struct CurveFixed { + /// Frames per second + frame_rate: f32, + /// Negative number of frames before the curve starts, it's stored + /// in a `f32` to avoid castings in the when sampling the curve and also + /// negated to use [`std::f32::mul_add`] + negative_frame_offset: f32, + pub keyframes: Vec, +} + +impl CurveFixed { + pub fn from_keyframes(frame_rate: f32, frame_offset: i32, keyframes: Vec) -> Self { + Self { + frame_rate, + negative_frame_offset: -(frame_offset as f32), + keyframes, + } + } + + pub fn from_constant(v: T) -> Self { + Self { + frame_rate: 30.0, + negative_frame_offset: 0.0, + keyframes: vec![v], + } + } + + #[inline] + pub fn frame_rate(&self) -> f32 { + self.frame_rate + } + + #[inline] + pub fn set_frame_rate(&mut self, frame_rate: f32) { + self.frame_rate = frame_rate; + } + + /// Sets the start keyframe index. + /// + /// Adds a starting delay in multiples of the frame duration `(1 / frame_rate)` + #[inline] + pub fn set_frame_offset(&mut self, offset: i32) { + self.negative_frame_offset = -offset as f32; + } + + /// Number of the start keyframe + #[inline] + pub fn frame_offset(&self) -> i32 { + -self.negative_frame_offset as i32 + } + + /// `true` when this `CurveFixed` doesn't have any keyframe + #[inline] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + #[inline] + pub fn iter(&self) -> impl Iterator { + self.keyframes.iter() + } + + #[inline] + pub fn iter_mut(&mut self) -> impl Iterator { + self.keyframes.iter_mut() + } +} + +impl Curve for CurveFixed { + type Output = T; + + fn duration(&self) -> f32 { + ((self.len() as f32 - 1.0 - self.negative_frame_offset) / self.frame_rate).max(0.0) + } + + #[inline] + fn time_offset(&self) -> f32 { + -self.negative_frame_offset / self.frame_rate + } + + #[inline] + fn len(&self) -> usize { + self.keyframes.len() + } + + fn sample(&self, time: f32) -> Self::Output { + // Make sure to have at least one sample + assert!(!self.keyframes.is_empty(), "track is empty"); + + let t = time.mul_add(self.frame_rate, self.negative_frame_offset); + if t.is_sign_negative() { + // Underflow clamp + return self.keyframes[0].clone(); + } + + let f = t.trunc(); + let t = t - f; + + let f = f as usize; + let f_n = self.len() - 1; + if f >= f_n { + // Overflow clamp + return self.keyframes[f_n].clone(); + } + + // Lerp the value + T::lerp_unclamped(&self.keyframes[f], &self.keyframes[f + 1], t) + } + + /// Same as the [`sample`] function + #[inline] + fn sample_with_cursor(&self, _: KeyframeIndex, time: f32) -> (KeyframeIndex, Self::Output) { + (0, self.sample(time)) + } +} diff --git a/crates/bevy_math/src/curves/mod.rs b/crates/bevy_math/src/curves/mod.rs new file mode 100644 index 0000000000000..3d1a3f9caab2a --- /dev/null +++ b/crates/bevy_math/src/curves/mod.rs @@ -0,0 +1,123 @@ +use thiserror::Error; + +mod fixed; +mod variable; +mod variable_linear; + +pub use fixed::*; +pub use variable::*; +pub use variable_linear::*; + +use crate::interpolation::Lerp; + +/// Points to a keyframe inside a given curve. +/// +/// When sampling curves with variable framerate like [`CurveVariable`] and [`CurveVariableLinear`] +/// is useful to keep track of a particular keyframe near the last sampling time, this keyframe index +/// is referred as cursor and speeds up sampling when the next time is close to the previous on, that +/// happens very often when playing a animation for instance. +/// +/// **NOTE** By default each keyframe is indexed using a `u16` to reduce memory usage for the curve cursor cache when implemented +pub type KeyframeIndex = u16; + +/// Defines a curve function usually made of keyframes +pub trait Curve { + type Output; + + /// Curve duration in seconds + fn duration(&self) -> f32; + + /// Time offset before the first keyframe + fn time_offset(&self) -> f32; + + /// Number of keyframes + fn len(&self) -> usize; + + /// Easier to use sampling method that doesn't needs the keyframe cursor, + /// but is more expensive in some types of curve, been always `O(n)`. + /// + /// This means sampling is more expensive to evaluate as the `time` gets bigger; + /// + /// # Panics + /// + /// Panics when the curve is empty, e.i. has no keyframes + fn sample(&self, time: f32) -> Self::Output; + + /// Samples the curve starting from some keyframe cursor, this make the common case `O(1)` + /// + /// # Example + /// + /// ```rust,ignore + /// let mut time = 0.0; + /// let mut current_cursor = 0; + /// loop { + /// let (next_cursor, value) = curve.sample_with_cursor(current_cursor, time); + /// current_cursor = next_cursor; + /// time += 0.01333f; + /// /// ... + /// } + /// ``` + /// + /// # Panics + /// + /// Panics when the curve is empty, e.i. has no keyframes + fn sample_with_cursor(&self, cursor: KeyframeIndex, time: f32) + -> (KeyframeIndex, Self::Output); +} + +#[derive(Error, Debug)] +pub enum CurveError { + #[error("number of keyframes time stamps and values doesn't match")] + MismatchedLength, + #[error("limit of {0} keyframes exceeded")] + KeyframeLimitReached(usize), + #[error("keyframes aren't sorted by time")] + NotSorted, +} + +pub trait CurveUtils { + type Output: Lerp + Clone; + + /// Resamples the curve preserving the loop cycle. + /// + /// [`CurveFixed`] only supports evenly spaced keyframes, because of that the curve duration + /// is always a multiple of the frame rate. So resampling a curve will always round up their duration + /// but it's still possible to preserve the loop cycle, i.e. both start and end keyframes will be remain the same, + /// which is a very desired property. + fn resample_preserving_loop(&self, frame_rate: f32) -> CurveFixed; +} + +impl CurveUtils for C +where + C: Curve, + ::Output: Lerp + Clone, +{ + type Output = ::Output; + + fn resample_preserving_loop(&self, frame_rate: f32) -> CurveFixed { + // get properties + let offset = self.time_offset(); + let duration = self.duration(); + + let frame_count = (duration * frame_rate).round() as usize; + let frame_offset = (offset * frame_rate).round() as i32; + + let normalize = 1.0 / (frame_count - 1) as f32; + let mut cursor0 = 0; + let keyframes = (0..frame_count) + .into_iter() + .map(|f| { + let time = duration * (f as f32 * normalize) + offset; + let (cursor1, value) = self.sample_with_cursor(cursor0, time); + cursor0 = cursor1; + value + }) + .collect::>(); + + // TODO: copy the start and end keyframes, because f32 precision might not be enough to preserve the loop + // keyframes[0] = self.value_at(0); + // keyframes[frame_count - 1] = self.value_at((self.len() - 1) as KeyframeIndex); + + CurveFixed::from_keyframes(frame_rate, frame_offset, keyframes) + } +} diff --git a/crates/bevy_math/src/curves/variable.rs b/crates/bevy_math/src/curves/variable.rs new file mode 100644 index 0000000000000..fddcb40d93588 --- /dev/null +++ b/crates/bevy_math/src/curves/variable.rs @@ -0,0 +1,769 @@ +use std::cmp::Ordering; + +use crate::{ + curves::{Curve, CurveError, KeyframeIndex}, + interpolation::{Interpolate, Interpolation}, +}; + +/// Controls how tangents of each keyframe of the [`CurveVariable`] will behave when editing or creating keyframes +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum TangentControl { + /// Tangents are automatically calculated, based on the catmull-rom algorithm + Auto, + /// In tangent will be the same as the out tangent + Free, + /// Tangents will set to be [`Interpolate::FLAT_TANGENT`] + Flat, + /// In and out tangents can be set to a different values + Broken, +} + +impl Default for TangentControl { + fn default() -> Self { + TangentControl::Auto + } +} + +// TODO: impl Serialize, Deserialize +// TODO: How better handling of SOA? the length for instance is repeated and extra checks are need on deserialization +// ? NOTE: Grouping fields (except `time_stamps`) in a array of struct (AOS) decreases performance on random sampling by ~15%, +// ? sequential sampling remains unchanged +/// Curve with sparse keyframes frames, in another words a curve with variable frame rate; +/// +/// Similar in design to the [`CurveVariableLinear`](super::CurveVariableLinear) but allows +/// for smoother catmull-rom interpolations using tangents, which can further reduce the number of keyframes at +/// the cost of performance; +/// +/// It can't handle discontinuities, as in two keyframes with the same timestamp. +/// +/// Interpolation is based on this [article](http://archive.gamedev.net/archive/reference/articles/article1497.html), +/// it's very similar to the implementation used by Unity, except that tangents doesn't have weighted mode; +/// +/// **NOTE**: The maximum number of keyframes is limited by the capacity of [`KeyframeIndex`] (a `u16`) +#[derive(Default, Debug, Clone)] +pub struct CurveVariable { + time_stamps: Vec, + keyframes: Vec, + modes: Vec, + tangents_control: Vec, + tangents_in: Vec, + tangents_out: Vec, +} + +impl CurveVariable +where + T: Interpolate + Clone, +{ + #[inline] + pub fn with_flat_tangents(samples: Vec, values: Vec) -> Result { + Self::with_tangents_and_mode( + samples, + values, + TangentControl::Flat, + Interpolation::Hermite, + ) + } + + #[inline] + pub fn with_auto_tangents(samples: Vec, values: Vec) -> Result { + Self::with_tangents_and_mode( + samples, + values, + TangentControl::Auto, + Interpolation::Hermite, + ) + } + + pub fn with_tangents_and_mode( + samples: Vec, + values: Vec, + tangent_control: TangentControl, + mode: Interpolation, + ) -> Result { + let length = samples.len(); + + // Make sure both have the same length + if length != values.len() { + return Err(CurveError::MismatchedLength); + } + + if values.len() > KeyframeIndex::MAX as usize { + return Err(CurveError::KeyframeLimitReached( + KeyframeIndex::MAX as usize, + )); + } + + // Make sure time stamps are ordered + if !samples + .iter() + .zip(samples.iter().skip(1)) + .all(|(a, b)| a < b) + { + return Err(CurveError::NotSorted); + } + + let mut tangents = Vec::with_capacity(length); + if tangent_control == TangentControl::Auto + || tangent_control == TangentControl::Free + || tangent_control == TangentControl::Broken + { + if length == 1 { + tangents.push(T::FLAT_TANGENT); + } else { + for i in 0..length { + let p = if i > 0 { i - 1 } else { 0 }; + let n = if (i + 1) < length { i + 1 } else { length - 1 }; + tangents.push(T::auto_tangent( + samples[p], samples[i], samples[n], &values[p], &values[i], &values[n], + )); + } + } + } else { + tangents.resize(length, T::FLAT_TANGENT); + } + + let mut tangents_control = Vec::with_capacity(length); + tangents_control.resize(length, tangent_control); + + let mut modes = Vec::with_capacity(length); + modes.resize(length, mode); + + Ok(Self { + time_stamps: samples, + keyframes: values, + modes, + tangents_control, + tangents_in: tangents.clone(), + tangents_out: tangents, + }) + } + + pub fn from_line(time0: f32, time1: f32, value0: T, value1: T) -> Self { + let mut modes = Vec::with_capacity(2); + modes.resize(2, Interpolation::Linear); + + let mut tangents_control = Vec::with_capacity(2); + tangents_control.resize(2, TangentControl::Auto); + + let mut tangents = Vec::with_capacity(2); + tangents.resize(2, T::FLAT_TANGENT); + + if time0 < time1 { + Self { + time_stamps: vec![time0, time1], + keyframes: vec![value0, value1], + modes, + tangents_control, + tangents_in: tangents.clone(), + tangents_out: tangents, + } + } else { + Self { + time_stamps: vec![time1, time0], + keyframes: vec![value1, value0], + modes, + tangents_control, + tangents_in: tangents.clone(), + tangents_out: tangents, + } + } + } + + pub fn from_constant(value: T) -> Self { + Self { + time_stamps: vec![0.0], + keyframes: vec![value], + modes: vec![Interpolation::Hermite], + tangents_control: vec![TangentControl::Auto], + tangents_in: vec![T::FLAT_TANGENT], + tangents_out: vec![T::FLAT_TANGENT], + } + } + + /// Insert a new keyframe + /// + /// ```rust + /// use bevy_math::curves::{CurveVariable, TangentControl}; + /// + /// # fn main() { + /// let mut curve = CurveVariable::from_constant(0.0f32); + /// curve.insert() + /// .set_time(1.0) + /// .set_value(2.0) + /// .set_tangent_control(TangentControl::Flat) + /// .done(); + /// + /// assert_eq!(curve.len(), 2); + /// # } + /// ``` + pub fn insert(&mut self) -> CurveVariableKeyframeBuilder { + CurveVariableKeyframeBuilder { + time: self + .time_stamps + .last() + .copied() + .map_or(0.0, |t| t + 0.03333), + value: self.keyframes.last().unwrap().clone(), + mode: *self.modes.last().unwrap(), + tangent_control: TangentControl::Auto, + tangent_in: T::FLAT_TANGENT, + tangent_out: T::FLAT_TANGENT, + curve: self, + } + } + + /// Removes the keyframe at the index specified. + /// + /// # Panics + /// + /// Panics if `at` is out of bounds. + pub fn remove(&mut self, at: KeyframeIndex) { + let i = at as usize; + + self.time_stamps.remove(i); + self.keyframes.remove(i); + self.modes.remove(i); + self.tangents_control.remove(i); + self.tangents_in.remove(i); + self.tangents_out.remove(i); + + // Adjusts the tangents of the neighbors keyframes left + // Checks if next keyframe exists, now with the same index of `i` since the keyframe was removed + if i < self.keyframes.len() { + self.adjust_tangents(i); + } + // Checks if previous keyframe exists + if i > 0 { + self.adjust_tangents(i - 1); + } + } + + /// Sets the given keyframe value then update tangents for self and neighboring keyframes + /// that are set with [`TangentControl::Auto`] mode. + /// + /// # Panics + /// + /// Panics if `at` is out of bounds. + pub fn set_value(&mut self, at: KeyframeIndex, value: T) { + let i = at as usize; + self.keyframes[i] = value; + self.adjust_tangents_with_neighbors(i); + } + + /// Moves the given keyframe to a different point in time then update + /// tangents for self and neighboring keyframes that are set with [`TangentControl::Auto`] mode + /// + /// # Panics + /// + /// Panics if `at` is out of bounds. + pub fn set_time(&mut self, at: KeyframeIndex, time: f32) -> Option { + let i = at as usize; + + let mut j = i; + let last = self.time_stamps.len() - 1; + if self.time_stamps[j] < time { + // Forward search + loop { + if j == last { + break; + } + + let temp = j + 1; + if self.time_stamps[temp] > time { + break; + } + + j = temp; + } + } else { + // Backward search + loop { + if j == 0 { + break; + } + + let temp = j - 1; + if self.time_stamps[temp] < time { + break; + } + + j = temp; + } + } + + match i.cmp(&j) { + Ordering::Greater => { + // Move backward + let k = i + 1; + self.time_stamps[j..k].rotate_right(1); + self.keyframes[j..k].rotate_right(1); + self.modes[j..k].rotate_right(1); + self.tangents_control[j..k].rotate_right(1); + self.tangents_in[j..k].rotate_right(1); + self.tangents_out[j..k].rotate_right(1); + + self.adjust_tangents_with_neighbors(j); + self.adjust_tangents_with_neighbors(i); + } + Ordering::Less => { + // Move forward + let k = j + 1; + self.time_stamps[i..k].rotate_left(1); + self.keyframes[i..k].rotate_left(1); + self.modes[i..k].rotate_left(1); + self.tangents_control[i..k].rotate_left(1); + self.tangents_in[i..k].rotate_left(1); + self.tangents_out[i..k].rotate_left(1); + + self.adjust_tangents_with_neighbors(j); + self.adjust_tangents_with_neighbors(i); + } + Ordering::Equal => { + // Just update the keyframe time + self.time_stamps[i] = time; + self.adjust_tangents_with_neighbors(i); + return None; + } + } + + Some(j as KeyframeIndex) + } + + /// Sets the function used to interpolate from the given keyframe to the next one. + /// + /// # Panics + /// + /// Panics if `at` is out of bounds. + #[inline] + pub fn set_interpolation(&mut self, at: KeyframeIndex, interpolation: Interpolation) { + self.modes[at as usize] = interpolation; + } + + /// Sets the keyframe in tangent and sets the tangent control mode to [`TangentControl::Broken`]. + /// + /// # Panics + /// + /// Panics if `at` is out of bounds. + #[inline] + pub fn set_in_tangent(&mut self, at: KeyframeIndex, tangent: T::Tangent) { + let i = at as usize; + self.tangents_control[i] = TangentControl::Broken; + self.tangents_in[i] = tangent; + } + + /// Sets the keyframe out tangent and sets the tangent control mode to [`TangentControl::Broken`]. + /// + /// # Panics + /// + /// Panics if `at` is out of bounds. + #[inline] + pub fn set_out_tangent(&mut self, at: KeyframeIndex, tangent: T::Tangent) { + let i = at as usize; + self.tangents_control[i] = TangentControl::Broken; + self.tangents_out[i] = tangent; + } + + /// Sets both in and out tangents for the given keyframe and sets the tangent control mode to [`TangentControl::Free`]. + /// + /// # Panics + /// + /// Panics if `at` is out of bounds. + #[inline] + pub fn set_in_out_tangent(&mut self, at: KeyframeIndex, tangent: T::Tangent) { + let i = at as usize; + self.tangents_control[i] = TangentControl::Free; + self.tangents_in[i] = tangent; + self.tangents_out[i] = tangent; + } + + /// Sets how tangents behave when editing the given keyframe and also updates the tangents when necessary. + /// + /// # Panics + /// + /// Panics if `at` is out of bounds. + #[inline] + pub fn set_tangent_control(&mut self, at: KeyframeIndex, tangent_control: TangentControl) { + let i = at as usize; + self.tangents_control[i] = tangent_control; + self.adjust_tangents(i); + } + + /// Adjust tangents for self and neighbors keyframes. + fn adjust_tangents_with_neighbors(&mut self, at: usize) { + if at > 0 { + self.adjust_tangents(at - 1); + } + + self.adjust_tangents(at); + + if at < self.keyframes.len() - 1 { + self.adjust_tangents(at + 1); + } + } + + /// Adjust tangents for a single keyframe according with their [`TangentControl`]. + fn adjust_tangents(&mut self, at: usize) { + let length = self.keyframes.len(); + let mut tangent = T::FLAT_TANGENT; + + match self.tangents_control[at] { + TangentControl::Auto => { + if length > 2 { + let p = if at > 0 { at - 1 } else { 0 }; + let n = if (at + 1) < length { + at + 1 + } else { + length - 1 + }; + + tangent = T::auto_tangent( + self.time_stamps[p], + self.time_stamps[at], + self.time_stamps[n], + &self.keyframes[p], + &self.keyframes[at], + &self.keyframes[n], + ); + } + } + TangentControl::Free => { + // Copy left tangent into the right tangent + self.tangents_out[at] = self.tangents_in[at]; + return; + } + TangentControl::Flat => {} + _ => { + // Do nothing + return; + } + } + + self.tangents_in[at] = tangent; + self.tangents_out[at] = tangent; + } + + /// Rebuilds tangents for the entire curve based on each keyframe [`TangentControl`] mode. + pub fn rebuild_curve_tangents(&mut self) { + for i in 0..self.len() { + self.adjust_tangents(i); + } + } + + /// Gets keyframe value at the given index. + /// + /// # Panics + /// + /// Panics if `at` is out of bounds. + #[inline] + pub fn get_value(&self, at: KeyframeIndex) -> &T { + &self.keyframes[at as usize] + } + + /// Gets keyframe time at the given index. + /// + /// # Panics + /// + /// Panics if `at` is out of bounds. + #[inline] + pub fn get_time(&self, at: KeyframeIndex) -> f32 { + self.time_stamps[at as usize] + } + + /// Gets the function used to interpolate from the given keyframe to the next one. + /// + /// # Panics + /// + /// Panics if `at` is out of bounds. + #[inline] + pub fn get_interpolation(&self, at: KeyframeIndex) -> Interpolation { + self.modes[at as usize] + } + + /// Gets how tangents behave when editing the given keyframe. + /// + /// # Panics + /// + /// Panics if `at` is out of bounds. + #[inline] + pub fn get_tangent_control(&self, at: KeyframeIndex) -> TangentControl { + self.tangents_control[at as usize] + } + + /// Gets both in and out tangents for the given keyframe. + /// + /// # Panics + /// + /// Panics if `at` is out of bounds. + #[inline] + pub fn get_in_out_tangent(&self, at: KeyframeIndex) -> (T::Tangent, T::Tangent) { + let i = at as usize; + (self.tangents_in[i], self.tangents_out[i]) + } + + /// `true` when this `CurveFixed` doesn't have any keyframe. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn set_time_offset(&mut self, mut time_offset: f32) { + time_offset -= self.time_offset(); // Removes current offset + self.time_stamps.iter_mut().for_each(|t| *t += time_offset); + } + + pub fn iter(&self) -> impl Iterator { + self.time_stamps.iter().copied().zip(self.keyframes.iter()) + } +} + +impl Curve for CurveVariable +where + T: Interpolate + Clone, +{ + type Output = T; + + fn duration(&self) -> f32 { + self.time_stamps.last().copied().unwrap_or(0.0) + } + + fn time_offset(&self) -> f32 { + self.time_stamps.first().copied().unwrap_or(0.0) + } + + #[inline] + fn len(&self) -> usize { + self.keyframes.len() + } + + fn sample(&self, time: f32) -> Self::Output { + // Index guessing gives a small search optimization + let index = if time < self.duration() * 0.5 { + 0 + } else { + self.time_stamps.len() - 1 + }; + + self.sample_with_cursor(index as u16, time).1 + } + + fn sample_with_cursor( + &self, + mut cursor: KeyframeIndex, + time: f32, + ) -> (KeyframeIndex, Self::Output) { + // Adjust for the current keyframe cursor + let last_cursor = (self.time_stamps.len() - 1) as u16; + + cursor = cursor.max(0).min(last_cursor); + if self.time_stamps[cursor as usize] < time { + // Forward search + loop { + if cursor == last_cursor { + return (last_cursor, self.keyframes[last_cursor as usize].clone()); + } + cursor += 1; + + if self.time_stamps[cursor as usize] >= time { + break; + } + } + } else { + // Backward search + loop { + if cursor == 0 { + return (0, self.keyframes[0].clone()); + } + + let i = cursor - 1; + if self.time_stamps[i as usize] <= time { + break; + } + + cursor = i; + } + } + + // Lerp the value + let i = cursor - 1; + let previous_time = self.time_stamps[i as usize]; + let dt = self.time_stamps[cursor as usize] - previous_time; + let t = (time - previous_time) / dt; + debug_assert!( + (0.0..=1.0).contains(&t), + "t = {} but should be normalized", + t + ); // Checks if it's required to normalize t + + let a = i as usize; + let b = cursor as usize; + let value = T::interpolate_unclamped( + &self.keyframes[a], + &self.tangents_out[a], + &self.keyframes[b], + &self.tangents_in[b], + self.modes[a as usize], + t, + dt, + ); + + (cursor, value) + } +} + +#[must_use = "use the `done` function to insert the keyframe"] +pub struct CurveVariableKeyframeBuilder<'a, T: Interpolate> { + curve: &'a mut CurveVariable, + time: f32, + value: T, + mode: Interpolation, + tangent_control: TangentControl, + tangent_in: T::Tangent, + tangent_out: T::Tangent, +} + +impl<'a, T: Interpolate> CurveVariableKeyframeBuilder<'a, T> { + #[inline] + pub fn set_time(mut self, time: f32) -> Self { + self.time = time; + self + } + + #[inline] + pub fn set_value(mut self, value: T) -> Self { + self.value = value; + self + } + + #[inline] + pub fn set_mode(mut self, mode: Interpolation) -> Self { + self.mode = mode; + self + } + + #[inline] + pub fn set_tangent_control(mut self, tangent_control: TangentControl) -> Self { + if tangent_control == TangentControl::Flat { + self.tangent_in = T::FLAT_TANGENT; + self.tangent_out = T::FLAT_TANGENT; + } + + self.tangent_control = tangent_control; + self + } + + #[inline] + pub fn set_in_tangent(mut self, tangent: T::Tangent) -> Self { + self.tangent_control = TangentControl::Broken; + self.tangent_in = tangent; + self + } + + #[inline] + pub fn set_out_tangent(mut self, tangent: T::Tangent) -> Self { + self.tangent_control = TangentControl::Broken; + self.tangent_out = tangent; + self + } + + #[inline] + pub fn set_in_out_tangent(&mut self, tangent: T::Tangent) { + self.tangent_control = TangentControl::Free; + self.tangent_in = tangent; + self.tangent_out = tangent; + } + + pub fn done(self) -> Result { + let index; + + if self.curve.len() >= (KeyframeIndex::MAX - 1) as usize { + return Err(CurveError::KeyframeLimitReached( + KeyframeIndex::MAX as usize, + )); + } + + if let Some(i) = self.curve.time_stamps.iter().position(|t| *t > self.time) { + index = i; + self.curve.time_stamps.insert(index, self.time); + self.curve.keyframes.insert(index, self.value); + self.curve.modes.insert(index, self.mode); + self.curve + .tangents_control + .insert(index, self.tangent_control); + self.curve.tangents_in.insert(index, self.tangent_in); + self.curve.tangents_out.insert(index, self.tangent_out); + } else { + self.curve.time_stamps.push(self.time); + self.curve.keyframes.push(self.value); + self.curve.modes.push(self.mode); + self.curve.tangents_control.push(self.tangent_control); + self.curve.tangents_in.push(self.tangent_in); + self.curve.tangents_out.push(self.tangent_out); + + index = self.curve.keyframes.len() - 1; + } + + self.curve.adjust_tangents_with_neighbors(index); + Ok(index as KeyframeIndex) + } +} + +#[cfg(test)] +mod tests { + use super::*; + // TODO: Tests for creating, evaluating and editing the `CurveVariable` + + #[test] + fn set_keyframe_value() { + let mut curve = + CurveVariable::with_auto_tangents(vec![0.0, 1.0, 2.0], vec![0.0, 1.0, 0.0]).unwrap(); + curve.set_value(0, 1.0); + curve.set_value(1, 0.0); + curve.set_value(2, 1.0); + + let ground_truth: Vec = vec![ + 1.0, + 0.80658436, + 0.5144033, + 0.22222212, + 0.028806567, + 0.028806612, + 0.22222227, + 0.5144033, + 0.80658436, + 1.0, + ]; + let samples = (0..10) + .into_iter() + .map(|i| curve.sample(2.0 * i as f32 / 9.0)) + .collect::>(); + + assert_eq!(ground_truth.len(), samples.len()); + assert!(ground_truth + .iter() + .zip(samples.iter()) + .all(|(a, b)| (a - b).abs() < std::f32::EPSILON)); + } + + #[test] + fn set_keyframe_time() { + let mut curve = + CurveVariable::with_auto_tangents(vec![0.0, 1.0, 2.0, 3.0], vec![1.0, 0.0, 0.0, 0.0]) + .unwrap(); + + // Don't change keyframe + assert_eq!(curve.set_time(0, 0.0), None); + assert_eq!(curve.set_time(1, 1.0), None); + assert_eq!(curve.set_time(2, 2.0), None); + assert_eq!(curve.set_time(3, 3.0), None); + assert_eq!(curve.set_time(0, 0.5), None); + + // Change keyframe + assert_eq!(curve.set_time(0, 1.5), Some(1)); + assert!((*curve.get_value(1) - 1.0).abs() < std::f32::EPSILON); + + assert_eq!(curve.set_time(1, 2.5), Some(2)); + assert!((*curve.get_value(2) - 1.0).abs() < std::f32::EPSILON); + + assert_eq!(curve.set_time(2, 0.0), Some(0)); + assert!((*curve.get_value(0) - 1.0).abs() < std::f32::EPSILON); + } +} diff --git a/crates/bevy_math/src/curves/variable_linear.rs b/crates/bevy_math/src/curves/variable_linear.rs new file mode 100644 index 0000000000000..0abc97a312399 --- /dev/null +++ b/crates/bevy_math/src/curves/variable_linear.rs @@ -0,0 +1,342 @@ +use std::cmp::Ordering; + +use crate::{ + curves::{Curve, CurveError, KeyframeIndex}, + interpolation::Lerp, +}; + +// TODO: Curve/Clip need a validation during deserialization because they are +// structured as SOA (struct of arrays), so the vec's length must match + +// TODO: impl Serialize, Deserialize +/// Curve with sparse keyframes frames, in another words a curve with variable frame rate; +/// +/// This is a very useful curve, because it can accommodate the output of a linear reduction keyframe algorithm +/// to lower the memory foot print. As a down side it requires the use of a keyframe cursor, and +/// loses performance when the curve frame rate is higher than the curve sampling frame rate; +/// +/// It can't handle discontinuities, as in two keyframes with the same timestamp. +/// +/// **NOTE** Keyframes count is limited by the [`KeyframeIndex`] size. +#[derive(Default, Debug, Clone)] +pub struct CurveVariableLinear { + time_stamps: Vec, + keyframes: Vec, +} + +impl CurveVariableLinear { + pub fn new(samples: Vec, values: Vec) -> Result { + let length = samples.len(); + + // Make sure both have the same length + if length != values.len() { + return Err(CurveError::MismatchedLength); + } + + if values.len() > KeyframeIndex::MAX as usize { + return Err(CurveError::KeyframeLimitReached( + KeyframeIndex::MAX as usize, + )); + } + + // Make sure time stamps are ordered + if !samples + .iter() + .zip(samples.iter().skip(1)) + .all(|(a, b)| a < b) + { + return Err(CurveError::NotSorted); + } + + Ok(Self { + time_stamps: samples, + keyframes: values, + }) + } + + pub fn from_line(time0: f32, time1: f32, value0: T, value1: T) -> Self { + if time0 < time1 { + Self { + time_stamps: vec![time0, time1], + keyframes: vec![value0, value1], + } + } else { + Self { + time_stamps: vec![time1, time0], + keyframes: vec![value1, value0], + } + } + } + + pub fn from_constant(value: T) -> Self { + Self { + time_stamps: vec![0.0], + keyframes: vec![value], + } + } + + /// Inserts a new keyframe + /// + /// Panics if the keyframe limit given by `KeyframeIndex::MAX` is reached. + pub fn insert(&mut self, time: f32, value: T) { + assert!( + self.keyframes.len() < KeyframeIndex::MAX as usize, + "reached keyframe limit" + ); + + if let Some(index) = self.time_stamps.iter().position(|t| time < *t) { + self.time_stamps.insert(index, time); + self.keyframes.insert(index, value); + } else { + self.time_stamps.push(time); + self.keyframes.push(value); + } + } + + /// Removes a keyframe at the given index + /// + /// # Panics + /// + /// Panics if `at` is out of bounds. + pub fn remove(&mut self, at: KeyframeIndex) -> (f32, T) { + let index = at as usize; + (self.time_stamps.remove(index), self.keyframes.remove(index)) + } + + /// Sets the given keyframe value + /// + /// # Panics + /// + /// Panics if `at` is out of bounds. + #[inline] + pub fn set_value(&mut self, at: KeyframeIndex, value: T) { + self.keyframes[at as usize] = value; + } + + /// Moves the given keyframe to a different point in time + /// + /// # Panics + /// + /// Panics if `at` is out of bounds. + pub fn set_time(&mut self, at: KeyframeIndex, time: f32) -> Option { + let i = at as usize; + + let mut j = i; + let last = self.time_stamps.len() - 1; + if self.time_stamps[j] < time { + // Forward search + loop { + if j == last { + break; + } + + let temp = j + 1; + if self.time_stamps[temp] > time { + break; + } + + j = temp; + } + } else { + // Backward search + loop { + if j == 0 { + break; + } + + let temp = j - 1; + if self.time_stamps[temp] < time { + break; + } + + j = temp; + } + } + + match i.cmp(&j) { + Ordering::Greater => { + // Move backward + let k = i + 1; + self.time_stamps[j..k].rotate_right(1); + self.keyframes[j..k].rotate_right(1); + } + Ordering::Less => { + // Move forward + let k = j + 1; + self.time_stamps[i..k].rotate_left(1); + self.keyframes[i..k].rotate_left(1); + } + Ordering::Equal => { + // Just update the keyframe time + self.time_stamps[i] = time; + return None; + } + } + + Some(j as KeyframeIndex) + } + + /// Gets keyframe value at the given index + /// + /// # Panics + /// + /// Panics if `at` is out of bounds. + #[inline] + pub fn get_value(&self, at: KeyframeIndex) -> &T { + &self.keyframes[at as usize] + } + + /// Gets keyframe time at the given index + /// + /// # Panics + /// + /// Panics if `at` is out of bounds. + #[inline] + pub fn get_time(&self, at: KeyframeIndex) -> f32 { + self.time_stamps[at as usize] + } + + pub fn set_time_offset(&mut self, time_offset: f32) { + self.time_stamps.iter_mut().for_each(|t| *t += time_offset); + } + + pub fn iter(&self) -> impl Iterator { + self.time_stamps.iter().copied().zip(self.keyframes.iter()) + } + + pub fn iter_mut(&mut self) -> impl Iterator { + self.time_stamps + .iter() + .copied() + .zip(self.keyframes.iter_mut()) + } +} + +impl Curve for CurveVariableLinear +where + T: Lerp + Clone + 'static, +{ + type Output = T; + + fn duration(&self) -> f32 { + self.time_stamps.last().copied().unwrap_or(0.0) + } + + #[inline] + fn time_offset(&self) -> f32 { + self.time_stamps[0] + } + + #[inline] + fn len(&self) -> usize { + self.keyframes.len() + } + + fn sample(&self, time: f32) -> Self::Output { + // Index guessing gives a small search optimization + let index = if time < self.duration() * 0.5 { + 0 + } else { + self.time_stamps.len() - 1 + }; + + self.sample_with_cursor(index as KeyframeIndex, time).1 + } + + fn sample_with_cursor( + &self, + mut cursor: KeyframeIndex, + time: f32, + ) -> (KeyframeIndex, Self::Output) { + // Adjust for the current keyframe index + let last_cursor = (self.time_stamps.len() - 1) as KeyframeIndex; + + cursor = cursor.max(0).min(last_cursor); + if self.time_stamps[cursor as usize] < time { + // Forward search + loop { + if cursor == last_cursor { + return (last_cursor, self.keyframes[last_cursor as usize].clone()); + } + cursor += 1; + + if self.time_stamps[cursor as usize] >= time { + break; + } + } + } else { + // Backward search + loop { + if cursor == 0 { + return (0, self.keyframes[0].clone()); + } + + let i = cursor - 1; + if self.time_stamps[i as usize] <= time { + break; + } + + cursor = i; + } + } + + // Lerp the value + let i = cursor - 1; + let previous_time = self.time_stamps[i as usize]; + let t = (time - previous_time) / (self.time_stamps[cursor as usize] - previous_time); + debug_assert!( + (0.0..=1.0).contains(&t), + "t = {} but should be normalized", + t + ); // Checks if it's required to normalize t + let value = T::lerp_unclamped( + &self.keyframes[i as usize], + &self.keyframes[cursor as usize], + t, + ); + + (cursor, value) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn curve_evaluation() { + let curve = CurveVariableLinear::new( + vec![0.0, 0.25, 0.5, 0.75, 1.0], + vec![0.0, 0.5, 1.0, 1.5, 2.0], + ) + .unwrap(); + assert!((curve.sample(0.5) - 1.0).abs() < f32::EPSILON); + + let mut i0 = 0; + let mut e0 = 0.0; + for v in &[0.1, 0.3, 0.7, 0.4, 0.2, 0.0, 0.4, 0.85, 1.0] { + let v = *v; + let (i1, e1) = curve.sample_with_cursor(i0, v); + assert!((e1 - (2.0 * v)).abs() < f32::EPSILON); + if e1 > e0 { + assert!(i1 >= i0); + } else { + assert!(i1 <= i0); + } + e0 = e1; + i0 = i1; + } + } + + #[test] + #[should_panic] + fn curve_bad_length() { + let _ = CurveVariableLinear::new(vec![0.0, 0.5, 1.0], vec![0.0, 1.0]).unwrap(); + } + + #[test] + #[should_panic] + fn curve_time_samples_not_sorted() { + let _ = CurveVariableLinear::new(vec![0.0, 1.5, 1.0], vec![0.0, 1.0, 2.0]).unwrap(); + } +} diff --git a/crates/bevy_math/src/interpolation/interpolate.rs b/crates/bevy_math/src/interpolation/interpolate.rs new file mode 100644 index 0000000000000..b7496e0b2da3f --- /dev/null +++ b/crates/bevy_math/src/interpolation/interpolate.rs @@ -0,0 +1,224 @@ +use super::{utils, Lerp}; +use crate::{Quat, Vec2, Vec3, Vec3A, Vec4}; + +// http://archive.gamedev.net/archive/reference/articles/article1497.html (bit old) + +#[derive(Debug, Copy, Clone)] +pub struct TangentIgnore; + +/// Defines which function will be used to interpolate from the current keyframe to the next one +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Interpolation { + Step, + Linear, + Hermite, +} + +pub trait Interpolate: Lerp + Clone { + /// Tangent used for the hermite interpolation + type Tangent: Copy; + + const FLAT_TANGENT: Self::Tangent; + + /// Interpolates between two keyframes using a predefined function + /// controlled by the factor `u` clamped in the range from 0 to 1. + /// + /// **NOTE** `delta_time` refers to the time difference between the keyframes. + #[inline] + fn interpolate( + value0: &Self, + tangent0: &Self::Tangent, + value1: &Self, + tangent1: &Self::Tangent, + interp: Interpolation, + u: f32, + delta_time: f32, + ) -> Self { + Self::interpolate_unclamped( + value0, + tangent0, + value1, + tangent1, + interp, + u.clamp(0.0, 1.0), + delta_time, + ) + } + + /// Interpolates between two keyframes using a predefined function + /// controlled by the factor `u` whiting the 0 to 1 range. + /// + /// **NOTE** `delta_time` refers to the time difference between the keyframes. + fn interpolate_unclamped( + value0: &Self, + tangent0: &Self::Tangent, + value1: &Self, + tangent1: &Self::Tangent, + interp: Interpolation, + u: f32, + delta_time: f32, + ) -> Self; + + fn auto_tangent( + time0: f32, + time1: f32, + time2: f32, + value0: &Self, + value1: &Self, + value2: &Self, + ) -> Self::Tangent; +} + +impl Interpolate for bool { + type Tangent = TangentIgnore; + const FLAT_TANGENT: Self::Tangent = TangentIgnore; + + #[inline] + fn interpolate_unclamped( + value0: &Self, + _: &Self::Tangent, + value1: &Self, + _: &Self::Tangent, + _: Interpolation, + u: f32, + _: f32, + ) -> Self { + utils::step_unclamped(value0, value1, u) + } + + fn auto_tangent(_: f32, _: f32, _: f32, _: &Self, _: &Self, _: &Self) -> Self::Tangent { + TangentIgnore + } +} + +macro_rules! interpolate { + ($ty:ty, $flat:expr) => { + impl Interpolate for $ty { + type Tangent = Self; + const FLAT_TANGENT: Self::Tangent = $flat; + + fn interpolate_unclamped( + value0: &Self, + tangent0: &Self::Tangent, + value1: &Self, + tangent1: &Self::Tangent, + interp: Interpolation, + u: f32, + delta_time: f32, + ) -> Self { + match interp { + Interpolation::Step => utils::step_unclamped(value0, value1, u), + Interpolation::Linear => utils::lerp_unclamped(*value0, *value1, u), + Interpolation::Hermite => utils::hermite_unclamped( + *value0, *tangent0, *value1, *tangent1, u, delta_time, + ), + } + } + + #[inline] + fn auto_tangent( + time0: f32, + time1: f32, + time2: f32, + value0: &Self, + value1: &Self, + value2: &Self, + ) -> Self::Tangent { + utils::auto_tangent(time0, time1, time2, *value0, *value1, *value2) + } + } + }; +} + +interpolate!(f32, 0.0); +interpolate!(Vec2, Vec2::ZERO); +interpolate!(Vec3, Vec3::ZERO); +interpolate!(Vec3A, Vec3A::ZERO); +interpolate!(Vec4, Vec4::ZERO); + +// TODO: Color can't be interpolated because color operations are undefined, see pr #1870 +// impl Interpolate for Color { +// type Tangent = Self; + +// fn interpolate(value0: &Self, value1: &Self, interp: Interpolation, u: f32, delta_time: f32) -> Self { +// match interp { +// Interpolation::Step => utils::step(value0, value1, u), +// Interpolation::Linear => utils::lerp(*value0, *value1, u), +// Interpolation::Smooth { right, left } => utils::hermite_unclamped::( +// (*value0).into(), +// (*right).into(), +// (*value1).into(), +// (*left).into(), +// u, +// delta_time, +// ) +// .into(), +// } +// } + +// fn auto_tangent(time0: f32, time1: f32, time2: f32, value0: Self, value1: Self, value2: Self) -> Self::Tangent { +// utils::auto_tangent(time0, time1, time2, value0, value1, value2) +// } +// } + +impl Interpolate for Quat { + type Tangent = Self; + const FLAT_TANGENT: Self::Tangent = unsafe { std::mem::transmute([0.0f32; 4]) }; + + /// Performs an nlerp, because it's much cheaper and easer to combine with other animations, + /// reference: http://number-none.com/product/Understanding%20Slerp,%20Then%20Not%20Using%20It/ + fn interpolate_unclamped( + value0: &Self, + tangent0: &Self::Tangent, + value1: &Self, + tangent1: &Self::Tangent, + interp: Interpolation, + u: f32, + delta_time: f32, + ) -> Self { + match interp { + Interpolation::Step => utils::step_unclamped(value0, value1, u), + Interpolation::Linear => { + // Make sure is always the short path, look at this: https://github.com/mgeier/quaternion-nursery + let mut value1 = *value1; + if value0.dot(value1) < 0.0 { + value1 = -value1; + } + + let q = utils::lerp_unclamped::((*value0).into(), value1.into(), u); + let d = utils::approx_rsqrt(q.dot(q)); + Quat::from_vec4(q * d) + } + Interpolation::Hermite => { + // Make sure is always the short path, look at this: https://github.com/mgeier/quaternion-nursery + let mut value1 = *value1; + if value0.dot(value1) < 0.0 { + value1 = -value1; + } + + let q = utils::hermite_unclamped::( + (*value0).into(), + (*tangent0).into(), + value1.into(), + (*tangent1).into(), + u, + delta_time, + ); + let d = utils::approx_rsqrt(q.dot(q)); + Quat::from_vec4(q * d) + } + } + } + + #[inline] + fn auto_tangent( + time0: f32, + time1: f32, + time2: f32, + value0: &Self, + value1: &Self, + value2: &Self, + ) -> Self::Tangent { + utils::auto_tangent(time0, time1, time2, *value0, *value1, *value2) + } +} diff --git a/crates/bevy_math/src/interpolation/lerp.rs b/crates/bevy_math/src/interpolation/lerp.rs new file mode 100644 index 0000000000000..ad0bb911696b4 --- /dev/null +++ b/crates/bevy_math/src/interpolation/lerp.rs @@ -0,0 +1,95 @@ +use crate::{interpolation::utils::*, Quat, Vec2, Vec3, Vec3A, Vec4}; + +/// Defines how a particular type will be interpolated +pub trait Lerp: Sized { + /// Lerp, `u` is unclamped + fn lerp_unclamped(value0: &Self, value1: &Self, u: f32) -> Self; + + /// Lerp, `u` is clamped in [0; 1] range + fn lerp(value0: &Self, value1: &Self, u: f32) -> Self { + let u = u.clamp(0.0, 1.0); + Self::lerp_unclamped(value0, value1, u) + } +} + +impl Lerp for bool { + #[inline] + fn lerp_unclamped(value0: &Self, value1: &Self, u: f32) -> Self { + step_unclamped(value0, value1, u) + } +} + +impl Lerp for f32 { + #[inline] + fn lerp_unclamped(value0: &Self, value1: &Self, u: f32) -> Self { + (*value0) * (1.0 - u) + (*value1) * u + } +} + +impl Lerp for Vec2 { + #[inline] + fn lerp_unclamped(value0: &Self, value1: &Self, u: f32) -> Self { + (*value0) * (1.0 - u) + (*value1) * u + } +} + +/// **NOTE** Prefer [`Vec3A`] or [`Vec4`] whenever possible, using [`Vec3`] is 2 times slower +impl Lerp for Vec3 { + #[inline] + fn lerp_unclamped(value0: &Self, value1: &Self, u: f32) -> Self { + (*value0) * (1.0 - u) + (*value1) * u + } +} + +impl Lerp for Vec3A { + #[inline] + fn lerp_unclamped(value0: &Self, value1: &Self, u: f32) -> Self { + (*value0) * (1.0 - u) + (*value1) * u + } +} + +impl Lerp for Vec4 { + #[inline] + fn lerp_unclamped(value0: &Self, value1: &Self, u: f32) -> Self { + (*value0) * (1.0 - u) + (*value1) * u + } +} + +// TODO: Color can't be interpolated because color operations are undefined, see pr #1870 +// impl Lerp for Color { +// #[inline] +// fn lerp_unclamped(value0: &Self, value1: &Self, u: f32) -> Self { +// // ? NOTE: Make sure alpha is interpolated (pr #1870 Mul and Add doesn't include alpha) +// (*value0) * (1.0 - t) + (*value1) * t +// } +// } + +impl Lerp for Quat { + /// Performs an nlerp, because it's cheaper and easier to combine with other animations, + /// reference: http://number-none.com/product/Understanding%20Slerp,%20Then%20Not%20Using%20It/ + #[inline] + fn lerp_unclamped(a: &Self, b: &Self, v: f32) -> Self { + let mut b = *b; + + // Make sure is always the short path, look at this: https://github.com/mgeier/quaternion-nursery + if a.dot(b) < 0.0 { + b = -b; + } + + let a: Vec4 = (*a).into(); + let b: Vec4 = b.into(); + + let rot = Vec4::lerp_unclamped(&a, &b, v); + let inv_mag = approx_rsqrt(rot.dot(rot)); + Quat::from_vec4(rot * inv_mag) + } +} + +impl Lerp for Option { + fn lerp_unclamped(a: &Self, b: &Self, v: f32) -> Self { + match (a, b) { + (Some(a), Some(b)) => Some(T::lerp_unclamped(a, b, v)), + _ => step_unclamped(a, b, v), // change from `Some(T)` to `None` and vice versa + } + } +} diff --git a/crates/bevy_math/src/interpolation/mod.rs b/crates/bevy_math/src/interpolation/mod.rs new file mode 100644 index 0000000000000..15525e32003ee --- /dev/null +++ b/crates/bevy_math/src/interpolation/mod.rs @@ -0,0 +1,6 @@ +mod interpolate; +mod lerp; +pub mod utils; + +pub use interpolate::*; +pub use lerp::*; diff --git a/crates/bevy_math/src/interpolation/utils.rs b/crates/bevy_math/src/interpolation/utils.rs new file mode 100644 index 0000000000000..2461225129bb6 --- /dev/null +++ b/crates/bevy_math/src/interpolation/utils.rs @@ -0,0 +1,92 @@ +use std::ops::{Add, Div, Mul, Sub}; + +/// Fast approximated reciprocal square root +#[inline] +pub fn approx_rsqrt(x: f32) -> f32 { + #[cfg(target_feature = "sse")] + { + // use SEE _mm_rsqrt_ss intrinsic which has a better accuracy and + #[cfg(target_arch = "x86")] + use core::arch::x86::*; + #[cfg(target_arch = "x86_64")] + use core::arch::x86_64::*; + unsafe { + let y = _mm_rsqrt_ss(_mm_set1_ps(x)); + *(&y as *const _ as *const f32) + } + } + #[cfg(not(target_feature = "sse"))] + { + // Fall back to Quake 3 fast inverse sqrt, is has a higher error but still good enough and faster than `.sqrt().recip()`, + // implementation borrowed from Piston under the MIT License: [https://github.com/PistonDevelopers/skeletal_animation] + let x2: f32 = x * 0.5; + let mut y: f32 = x; + + let mut i: i32 = y.to_bits() as i32; + i = 0x5f3759df - (i >> 1); + y = f32::from_bits(i as u32); + + y = y * (1.5 - (x2 * y * y)); + y + } +} + +#[inline] +pub fn step_unclamped(value0: &T, value1: &T, u: f32) -> T { + if u < (1.0 - 1e-9) { + value0.clone() + } else { + value1.clone() + } +} + +#[inline] +pub fn lerp_unclamped(value0: T, value1: T, u: f32) -> T +where + T: Add + Mul, +{ + value0 * (1.0 - u) + value1 * u +} + +/// Performs the cubic hermite spline interpolation based on the factor `u` whiting the 0 to 1 range. +/// The curve shape is defined by the keyframes values, tangents and by the delta time between the keyframes. +/// +/// Source: http://archive.gamedev.net/archive/reference/articles/article1497.html +#[inline] +pub fn hermite_unclamped( + value0: T, + tangent0: T, + value1: T, + tangent1: T, + u: f32, + delta_time: f32, +) -> T +where + T: Add + Sub + Mul, +{ + let u2 = u * u; + let u3 = u2 * u; + let _3u2 = 3.0 * u2; + let _2u3 = 2.0 * u3; + + value0 * (_2u3 - _3u2 + 1.0) + + value1 * (_3u2 - _2u3) + + tangent0 * delta_time * (u3 - 2.0 * u2 + u) + + tangent1 * delta_time * (u3 - u2) +} + +/// Finds the tangent gradients for `k1` the hermite spline, takes the a keyframe value his point in time +/// as well as the surrounding keyframes values and time stamps. +/// +/// Source: http://archive.gamedev.net/archive/reference/articles/article1497.html +#[inline] +pub fn auto_tangent(time0: f32, time1: f32, time2: f32, value0: T, value1: T, value2: T) -> T +where + T: Copy + Add + Sub + Mul + Div, +{ + // k'(t) = ½[k(t) - k(t-1)]/δx1 + ½[k(t+1) - k(t)]/δx2 + ((value1 - value0) / (time1 - time0).max(1e-9) + (value2 - value1) / (time2 - time1).max(1e-9)) + * 0.5 +} + +// https://www.cubic.org/docs/hermite.htm diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index f209224d9f396..66568ca6141c8 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -1,6 +1,9 @@ mod face_toward; mod geometry; +pub mod curves; +pub mod interpolation; + pub use face_toward::*; pub use geometry::*; pub use glam::*; diff --git a/examples/animations/smooth_curves.rs b/examples/animations/smooth_curves.rs new file mode 100644 index 0000000000000..5c73f33c3f109 --- /dev/null +++ b/examples/animations/smooth_curves.rs @@ -0,0 +1,177 @@ +use bevy::{ + math::{ + curves::{Curve, KeyframeIndex, CurveVariable, TangentControl}, + interpolation::Interpolation, + }, + prelude::*, + render::pipeline::PrimitiveTopology, +}; + +struct CurveCursorTag; + +struct CurveTargetTag; + +#[derive(Default)] +struct CurveMesh { + timer: Timer, + curve: CurveVariable, +} + +fn main() { + App::build() + .insert_resource(CurveMesh::default()) + .add_plugins(DefaultPlugins) + .add_startup_system(setup.system()) + .add_system(animate.system()) + .run(); +} + +fn values(length: usize, default: T) -> Vec { + let mut v = vec![]; + v.resize(length, default); + v +} + +fn line(a: [f32; 3], b: [f32; 3]) -> Mesh { + let mut mesh = Mesh::new(PrimitiveTopology::LineStrip); + mesh.set_attribute(Mesh::ATTRIBUTE_POSITION, vec![a, b]); + mesh.set_attribute(Mesh::ATTRIBUTE_NORMAL, values(2, [0.0f32, 0.0, 1.0])); + mesh.set_attribute(Mesh::ATTRIBUTE_TANGENT, values(2, [0.0f32, 1.0, 0.0])); + mesh.set_attribute(Mesh::ATTRIBUTE_UV_0, values(2, [0.0f32; 2])); + mesh +} + +fn setup( + mut commands: Commands, + mut curve_mesh: ResMut, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // Create curve + curve_mesh.curve = CurveVariable::with_tangents_and_mode( + vec![0.0, 1.0, 1.3, 1.6, 1.7, 1.8, 1.9, 2.0], + vec![3.0, 0.0, 1.0, 0.0, 0.5, 0.0, 0.25, 0.0], + TangentControl::Auto, + Interpolation::Hermite, + ) + .unwrap(); + // Create timer + curve_mesh.timer = Timer::from_seconds(2.5, true); + + // Create curve mesh + const DIVS: usize = 1024; + let mut mesh = Mesh::new(PrimitiveTopology::LineStrip); + mesh.set_attribute( + Mesh::ATTRIBUTE_POSITION, + (0..DIVS) + .into_iter() + .map(|i| { + let time = (i as f32 / (DIVS - 1) as f32) * curve_mesh.curve.duration(); + [time, curve_mesh.curve.sample(time), 0.0] + }) + .collect::>(), + ); + mesh.set_attribute(Mesh::ATTRIBUTE_NORMAL, values(DIVS, [0.0f32, 0.0, 1.0])); + mesh.set_attribute(Mesh::ATTRIBUTE_TANGENT, values(DIVS, [0.0f32, 1.0, 0.0])); + mesh.set_attribute(Mesh::ATTRIBUTE_UV_0, values(DIVS, [0.0f32; 2])); + let mesh = meshes.add(mesh); + + let material = materials.add(Color::RED.into()); + let keyframe_mesh = meshes.add(Mesh::from(shape::Cube { size: 0.05 })); + + // Animated sphere + commands + .spawn_bundle(PbrBundle { + mesh, + transform: Transform::from_translation(Vec3::new(-3.5, 0.0, 0.0)), + material: material.clone(), + ..Default::default() + }) + .with_children(|parent| { + // Create keyframes + let tangent_material = materials.add(Color::BLACK.into()); + for (index, (t, k)) in curve_mesh.curve.iter().enumerate() { + parent + .spawn_bundle(PbrBundle { + mesh: keyframe_mesh.clone(), + transform: Transform::from_translation(Vec3::new(t, *k, 0.0)), + material: material.clone(), + ..Default::default() + }) + .with_children(|parent| { + // tangents + let (a, b) = curve_mesh.curve.get_in_out_tangent(index as KeyframeIndex); + let (ay, ax) = a.atan().sin_cos(); + let (by, bx) = b.atan().sin_cos(); + parent.spawn_bundle(PbrBundle { + mesh: meshes.add(line([0.0, 0.0, 0.0], [ax * -0.2, ay * -0.2, 0.0])), + material: tangent_material.clone(), + ..Default::default() + }); + parent.spawn_bundle(PbrBundle { + mesh: meshes.add(line([0.0, 0.0, 0.0], [bx * 0.2, by * 0.2, 0.0])), + material: tangent_material.clone(), + ..Default::default() + }); + }); + } + + // Create time cursor + parent + .spawn_bundle(PbrBundle { + mesh: meshes.add(line([0.0, 4.0, 0.0], [0.0, -2.0, 0.0])), + material: materials.add(Color::BLUE.into()), + ..Default::default() + }) + .insert(CurveCursorTag); + }); + + commands + .spawn_bundle(PbrBundle { + mesh: meshes.add(Mesh::from(shape::Icosphere { + radius: 0.5, + subdivisions: 3, + })), + transform: Transform::from_translation(Vec3::new(2.0, 0.0, 0.0)), + material: materials.add(Color::BEIGE.into()), + ..Default::default() + }) + .insert(CurveTargetTag); + + // Camera and Light + commands.spawn_bundle(PointLightBundle { + transform: Transform::from_translation(Vec3::new(4.0, 8.0, 4.0)), + ..Default::default() + }); + + commands.spawn_bundle(PerspectiveCameraBundle { + transform: Transform::from_matrix(Mat4::face_toward( + Vec3::new(-3.0, 5.0, 8.0), + Vec3::new(0.0, 0.0, 0.0), + Vec3::new(0.0, 1.0, 0.0), + )), + ..Default::default() + }); +} + +fn animate( + mut curve_mesh: ResMut, + time: Res