diff --git a/crates/bevy_animation/src/curves.rs b/crates/bevy_animation/src/curves.rs new file mode 100644 index 0000000000000..db64cfdab4482 --- /dev/null +++ b/crates/bevy_animation/src/curves.rs @@ -0,0 +1,702 @@ +//! Curve structures used by the animation system. + +use bevy_math::{ + curve::{cores::*, iterable::IterableCurve, *}, + Quat, Vec3, Vec4, VectorSpace, +}; +use bevy_reflect::Reflect; +use thiserror::Error; + +/// A keyframe-defined curve that "interpolates" by stepping at `t = 1.0` to the next keyframe. +#[derive(Debug, Clone, Reflect)] +pub struct SteppedKeyframeCurve { + core: UnevenCore, +} + +impl Curve for SteppedKeyframeCurve +where + T: Clone, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + self.core + .sample_with(t, |x, y, t| if t >= 1.0 { y.clone() } else { x.clone() }) + } +} + +impl SteppedKeyframeCurve { + /// Create a new [`SteppedKeyframeCurve`]. If the curve could not be constructed from the + /// given data, an error is returned. + #[inline] + pub fn new(timed_samples: impl IntoIterator) -> Result { + Ok(Self { + core: UnevenCore::new(timed_samples)?, + }) + } +} + +/// A keyframe-defined curve that uses cubic spline interpolation, backed by a contiguous buffer. +#[derive(Debug, Clone, Reflect)] +pub struct CubicKeyframeCurve { + // Note: the sample width here should be 3. + core: ChunkedUnevenCore, +} + +impl Curve for CubicKeyframeCurve +where + V: VectorSpace, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> V { + match self.core.sample_interp_timed(t) { + // In all the cases where only one frame matters, defer to the position within it. + InterpolationDatum::Exact((_, v)) + | InterpolationDatum::LeftTail((_, v)) + | InterpolationDatum::RightTail((_, v)) => v[1], + + InterpolationDatum::Between((t0, u), (t1, v), s) => { + cubic_spline_interpolation(u[1], u[2], v[0], v[1], s, t1 - t0) + } + } + } +} + +impl CubicKeyframeCurve { + /// Create a new [`CubicKeyframeCurve`] from keyframe `times` and their associated `values`. + /// Because 3 values are needed to perform cubic interpolation, `values` must have triple the + /// length of `times` — each consecutive triple `a_k, v_k, b_k` associated to time `t_k` + /// consists of: + /// - The in-tangent `a_k` for the sample at time `t_k` + /// - The actual value `v_k` for the sample at time `t_k` + /// - The out-tangent `b_k` for the sample at time `t_k` + /// + /// For example, for a curve built from two keyframes, the inputs would have the following form: + /// - `times`: `[t_0, t_1]` + /// - `values`: `[a_0, v_0, b_0, a_1, v_1, b_1]` + #[inline] + pub fn new( + times: impl IntoIterator, + values: impl IntoIterator, + ) -> Result { + Ok(Self { + core: ChunkedUnevenCore::new(times, values, 3)?, + }) + } +} + +/// A curve specifying the translation component of a [`Transform`] in animation. The variants are +/// broken down by interpolation mode (with the exception of `Constant`, which never interpolates). +/// +/// This type is, itself, a `Curve`, and it internally uses the provided sampling modes; each +/// variant "knows" its own interpolation mode. +/// +/// [`Transform`]: bevy_transform::components::Transform + +#[derive(Clone, Debug, Reflect)] +pub enum TranslationCurve { + /// A curve which takes a constant value over its domain. Notably, this is how animations with + /// only a single keyframe are interpreted. + Constant(ConstantCurve), + + /// A curve which interpolates linearly between keyframes. + Linear(UnevenSampleAutoCurve), + + /// A curve which interpolates between keyframes in steps. + Step(SteppedKeyframeCurve), + + /// A curve which interpolates between keyframes by using auxiliary tangent data to join + /// adjacent keyframes with a cubic Hermite spline, which is then sampled. + CubicSpline(CubicKeyframeCurve), +} + +impl Curve for TranslationCurve { + #[inline] + fn domain(&self) -> Interval { + match self { + TranslationCurve::Constant(c) => c.domain(), + TranslationCurve::Linear(c) => c.domain(), + TranslationCurve::Step(c) => c.domain(), + TranslationCurve::CubicSpline(c) => c.domain(), + } + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> Vec3 { + match self { + TranslationCurve::Constant(c) => c.sample_unchecked(t), + TranslationCurve::Linear(c) => c.sample_unchecked(t), + TranslationCurve::Step(c) => c.sample_unchecked(t), + TranslationCurve::CubicSpline(c) => c.sample_unchecked(t), + } + } +} + +impl TranslationCurve { + /// The time of the last keyframe for this animation curve. If the curve is constant, None + /// is returned instead. + #[inline] + pub fn duration(&self) -> Option { + match self { + TranslationCurve::Constant(_) => None, + TranslationCurve::Linear(c) => Some(c.domain().end()), + TranslationCurve::Step(c) => Some(c.domain().end()), + TranslationCurve::CubicSpline(c) => Some(c.domain().end()), + } + } +} + +/// A curve specifying the scale component of a [`Transform`] in animation. The variants are +/// broken down by interpolation mode (with the exception of `Constant`, which never interpolates). +/// +/// This type is, itself, a `Curve`, and it internally uses the provided sampling modes; each +/// variant "knows" its own interpolation mode. +/// +/// [`Transform`]: bevy_transform::components::Transform +#[derive(Clone, Debug, Reflect)] +pub enum ScaleCurve { + /// A curve which takes a constant value over its domain. Notably, this is how animations with + /// only a single keyframe are interpreted. + Constant(ConstantCurve), + + /// A curve which interpolates linearly between keyframes. + Linear(UnevenSampleAutoCurve), + + /// A curve which interpolates between keyframes in steps. + Step(SteppedKeyframeCurve), + + /// A curve which interpolates between keyframes by using auxiliary tangent data to join + /// adjacent keyframes with a cubic Hermite spline, which is then sampled. + CubicSpline(CubicKeyframeCurve), +} + +impl Curve for ScaleCurve { + #[inline] + fn domain(&self) -> Interval { + match self { + ScaleCurve::Constant(c) => c.domain(), + ScaleCurve::Linear(c) => c.domain(), + ScaleCurve::Step(c) => c.domain(), + ScaleCurve::CubicSpline(c) => c.domain(), + } + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> Vec3 { + match self { + ScaleCurve::Constant(c) => c.sample_unchecked(t), + ScaleCurve::Linear(c) => c.sample_unchecked(t), + ScaleCurve::Step(c) => c.sample_unchecked(t), + ScaleCurve::CubicSpline(c) => c.sample_unchecked(t), + } + } +} + +impl ScaleCurve { + /// The time of the last keyframe for this animation curve. If the curve is constant, None + /// is returned instead. + #[inline] + pub fn duration(&self) -> Option { + match self { + ScaleCurve::Constant(_) => None, + ScaleCurve::Linear(c) => Some(c.domain().end()), + ScaleCurve::Step(c) => Some(c.domain().end()), + ScaleCurve::CubicSpline(c) => Some(c.domain().end()), + } + } +} + +/// A curve specifying the scale component of a [`Transform`] in animation. The variants are +/// broken down by interpolation mode (with the exception of `Constant`, which never interpolates). +/// +/// This type is, itself, a `Curve`, and it internally uses the provided sampling modes; each +/// variant "knows" its own interpolation mode. +/// +/// [`Transform`]: bevy_transform::components::Transform +#[derive(Clone, Debug, Reflect)] +pub enum RotationCurve { + /// A curve which takes a constant value over its domain. Notably, this is how animations with + /// only a single keyframe are interpreted. + Constant(ConstantCurve), + + /// A curve which uses spherical linear interpolation between keyframes. + SphericalLinear(UnevenSampleAutoCurve), + + /// A curve which interpolates between keyframes in steps. + Step(SteppedKeyframeCurve), + + /// A curve which interpolates between keyframes by using auxiliary tangent data to join + /// adjacent keyframes with a cubic Hermite spline. For quaternions, this means interpolating + /// the underlying 4-vectors, sampling, and normalizing the result. + CubicSpline(CubicKeyframeCurve), +} + +impl Curve for RotationCurve { + #[inline] + fn domain(&self) -> Interval { + match self { + RotationCurve::Constant(c) => c.domain(), + RotationCurve::SphericalLinear(c) => c.domain(), + RotationCurve::Step(c) => c.domain(), + RotationCurve::CubicSpline(c) => c.domain(), + } + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> Quat { + match self { + RotationCurve::Constant(c) => c.sample_unchecked(t), + RotationCurve::SphericalLinear(c) => c.sample_unchecked(t), + RotationCurve::Step(c) => c.sample_unchecked(t), + RotationCurve::CubicSpline(c) => c + .map(|x| Quat::from_vec4(x).normalize()) + .sample_unchecked(t), + } + } +} + +impl RotationCurve { + /// The time of the last keyframe for this animation curve. If the curve is constant, None + /// is returned instead. + #[inline] + pub fn duration(&self) -> Option { + match self { + RotationCurve::Constant(_) => None, + RotationCurve::SphericalLinear(c) => Some(c.domain().end()), + RotationCurve::Step(c) => Some(c.domain().end()), + RotationCurve::CubicSpline(c) => Some(c.domain().end()), + } + } +} + +/// A keyframe-defined curve that uses linear interpolation over many samples at once, backed +/// by a contiguous buffer. +#[derive(Debug, Clone, Reflect)] +pub struct WideLinearKeyframeCurve { + // Here the sample width is the number of things to simultaneously interpolate. + core: ChunkedUnevenCore, +} + +impl IterableCurve for WideLinearKeyframeCurve +where + T: VectorSpace, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample_iter_unchecked<'a>(&self, t: f32) -> impl Iterator + where + Self: 'a, + { + match self.core.sample_interp(t) { + InterpolationDatum::Exact(v) + | InterpolationDatum::LeftTail(v) + | InterpolationDatum::RightTail(v) => TwoIterators::Left(v.iter().copied()), + + InterpolationDatum::Between(u, v, s) => { + let interpolated = u.iter().zip(v.iter()).map(move |(x, y)| x.lerp(*y, s)); + TwoIterators::Right(interpolated) + } + } + } +} + +impl WideLinearKeyframeCurve { + /// Create a new [`WideLinearKeyframeCurve`]. An error will be returned if: + /// - `values` has length zero. + /// - `times` has less than `2` unique valid entries. + /// - The length of `values` is not divisible by that of `times` (once sorted, filtered, + /// and deduplicated). + #[inline] + pub fn new( + times: impl IntoIterator, + values: impl IntoIterator, + ) -> Result { + Ok(Self { + core: ChunkedUnevenCore::new_width_inferred(times, values)?, + }) + } +} + +/// A keyframe-defined curve that uses stepped "interpolation" over many samples at once, backed +/// by a contiguous buffer. +#[derive(Debug, Clone, Reflect)] +pub struct WideSteppedKeyframeCurve { + // Here the sample width is the number of things to simultaneously interpolate. + core: ChunkedUnevenCore, +} + +impl IterableCurve for WideSteppedKeyframeCurve +where + T: Clone, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample_iter_unchecked<'a>(&self, t: f32) -> impl Iterator + where + Self: 'a, + { + match self.core.sample_interp(t) { + InterpolationDatum::Exact(v) + | InterpolationDatum::LeftTail(v) + | InterpolationDatum::RightTail(v) => TwoIterators::Left(v.iter().cloned()), + + InterpolationDatum::Between(u, v, s) => { + let interpolated = + u.iter() + .zip(v.iter()) + .map(move |(x, y)| if s >= 1.0 { y.clone() } else { x.clone() }); + TwoIterators::Right(interpolated) + } + } + } +} + +impl WideSteppedKeyframeCurve { + /// Create a new [`WideSteppedKeyframeCurve`]. An error will be returned if: + /// - `values` has length zero. + /// - `times` has less than `2` unique valid entries. + /// - The length of `values` is not divisible by that of `times` (once sorted, filtered, + /// and deduplicated). + #[inline] + pub fn new( + times: impl IntoIterator, + values: impl IntoIterator, + ) -> Result { + Ok(Self { + core: ChunkedUnevenCore::new_width_inferred(times, values)?, + }) + } +} + +/// A keyframe-defined curve that uses cubic interpolation over many samples at once, backed by a +/// contiguous buffer. +#[derive(Debug, Clone, Reflect)] +pub struct WideCubicKeyframeCurve { + core: ChunkedUnevenCore, +} + +impl IterableCurve for WideCubicKeyframeCurve +where + T: VectorSpace, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample_iter_unchecked<'a>(&self, t: f32) -> impl Iterator + where + Self: 'a, + { + match self.core.sample_interp_timed(t) { + InterpolationDatum::Exact((_, v)) + | InterpolationDatum::LeftTail((_, v)) + | InterpolationDatum::RightTail((_, v)) => { + // Pick out the part of this that actually represents the position (instead of tangents), + // which is the middle third. + let width = self.core.width(); + TwoIterators::Left(v[width..(width * 2)].iter().copied()) + } + + InterpolationDatum::Between((t0, u), (t1, v), s) => TwoIterators::Right( + cubic_spline_interpolate_slices(self.core.width() / 3, u, v, s, t1 - t0), + ), + } + } +} + +/// An error indicating that a multisampling keyframe curve could not be constructed. +#[derive(Debug, Error)] +#[error("Unable to construct a curve using this data")] +pub enum WideKeyframeCurveError { + /// The number of given values was not divisible by a multiple of the number of keyframes. + #[error("The number of values ({values_given}) was expected to be divisible by {divisor}")] + LengthMismatch { + /// The number of values given. + values_given: usize, + /// The number that `values_given` was supposed to be divisible by. + divisor: usize, + }, + + /// An error was returned by the internal core constructor. + CoreError(#[from] ChunkedUnevenCoreError), +} + +impl WideCubicKeyframeCurve { + /// Create a new [`WideCubicKeyframeCurve`]. An error will be returned if: + /// - `values` has length zero. + /// - `times` has less than `2` unique valid entries. + /// - The length of `values` is not divisible by three times that of `times` (once sorted, + /// filtered, and deduplicated). + #[inline] + pub fn new( + times: impl IntoIterator, + values: impl IntoIterator, + ) -> Result { + let times: Vec = times.into_iter().collect(); + let values: Vec = values.into_iter().collect(); + let divisor = times.len() * 3; + + if values.len() % divisor != 0 { + return Err(WideKeyframeCurveError::LengthMismatch { + values_given: values.len(), + divisor, + }); + } + + Ok(Self { + core: ChunkedUnevenCore::new_width_inferred(times, values)?, + }) + } +} + +/// A curve specifying the [`MorphWeights`] for a mesh in animation. The variants are broken +/// down by interpolation mode (with the exception of `Constant`, which never interpolates). +/// +/// This type is, itself, a `Curve>`; however, in order to avoid allocation, it is +/// recommended to use its implementation of the [`IterableCurve`] trait, which allows iterating +/// directly over information derived from the curve without allocating. +/// +/// [`MorphWeights`]: bevy_render::prelude::MorphWeights +#[derive(Debug, Clone, Reflect)] +pub enum WeightsCurve { + /// A curve which takes a constant value over its domain. Notably, this is how animations with + /// only a single keyframe are interpreted. + Constant(ConstantCurve>), + + /// A curve which interpolates weights linearly between keyframes. + Linear(WideLinearKeyframeCurve), + + /// A curve which interpolates weights between keyframes in steps. + Step(WideSteppedKeyframeCurve), + + /// A curve which interpolates between keyframes by using auxiliary tangent data to join + /// adjacent keyframes with a cubic Hermite spline, which is then sampled. + CubicSpline(WideCubicKeyframeCurve), +} + +impl IterableCurve for WeightsCurve { + #[inline] + fn domain(&self) -> Interval { + match self { + WeightsCurve::Constant(c) => IterableCurve::domain(c), + WeightsCurve::Linear(c) => c.domain(), + WeightsCurve::Step(c) => c.domain(), + WeightsCurve::CubicSpline(c) => c.domain(), + } + } + + #[inline] + fn sample_iter_unchecked<'a>(&self, t: f32) -> impl Iterator + where + Self: 'a, + { + match self { + WeightsCurve::Constant(c) => FourIterators::First(c.sample_iter_unchecked(t)), + WeightsCurve::Linear(c) => FourIterators::Second(c.sample_iter_unchecked(t)), + WeightsCurve::Step(c) => FourIterators::Third(c.sample_iter_unchecked(t)), + WeightsCurve::CubicSpline(c) => FourIterators::Fourth(c.sample_iter_unchecked(t)), + } + } +} + +impl WeightsCurve { + /// The time of the last keyframe for this animation curve. If the curve is constant, None + /// is returned instead. + #[inline] + pub fn duration(&self) -> Option { + match self { + WeightsCurve::Constant(_) => None, + WeightsCurve::Linear(c) => Some(c.domain().end()), + WeightsCurve::Step(c) => Some(c.domain().end()), + WeightsCurve::CubicSpline(c) => Some(c.domain().end()), + } + } +} + +/// A curve for animating either a the component of a [`Transform`] (translation, rotation, scale) +/// or the [`MorphWeights`] of morph targets for a mesh. +/// +/// Each variant yields a [`Curve`] over the data that it parametrizes. +/// +/// This follows the [glTF design]. +/// [glTF design]: +/// +/// [`Transform`]: bevy_transform::components::Transform +/// [`MorphWeights`]: bevy_render::prelude::MorphWeights +#[derive(Debug, Clone, Reflect)] +pub enum VariableCurve { + /// A [`TranslationCurve`] for animating the `translation` component of a [`Transform`]. + /// + /// [`Transform`]: bevy_transform::components::Transform + Translation(TranslationCurve), + + /// A [`RotationCurve`] for animating the `rotation` component of a [`Transform`]. + /// + /// [`Transform`]: bevy_transform::components::Transform + Rotation(RotationCurve), + + /// A [`ScaleCurve`] for animating the `scale` component of a [`Transform`]. + /// + /// [`Transform`]: bevy_transform::components::Transform + Scale(ScaleCurve), + + /// A [`WeightsCurve`] for animating [`MorphWeights`] of a mesh. + /// + /// [`MorphWeights`]: bevy_render::prelude::MorphWeights + Weights(WeightsCurve), +} + +impl VariableCurve { + /// The domain of this curve as an interval. + #[inline] + pub fn domain(&self) -> Interval { + match self { + VariableCurve::Translation(c) => c.domain(), + VariableCurve::Rotation(c) => c.domain(), + VariableCurve::Scale(c) => c.domain(), + VariableCurve::Weights(c) => c.domain(), + } + } + + /// The time of the last keyframe for this animation curve. If the curve is constant, None + /// is returned instead. + #[inline] + pub fn duration(&self) -> Option { + match self { + VariableCurve::Translation(c) => c.duration(), + VariableCurve::Rotation(c) => c.duration(), + VariableCurve::Scale(c) => c.duration(), + VariableCurve::Weights(c) => c.duration(), + } + } +} + +impl From for VariableCurve { + fn from(curve: TranslationCurve) -> Self { + Self::Translation(curve) + } +} + +impl From for VariableCurve { + fn from(curve: RotationCurve) -> Self { + Self::Rotation(curve) + } +} + +impl From for VariableCurve { + fn from(curve: ScaleCurve) -> Self { + Self::Scale(curve) + } +} + +impl From for VariableCurve { + fn from(curve: WeightsCurve) -> Self { + Self::Weights(curve) + } +} + +//---------// +// HELPERS // +//---------// + +enum TwoIterators { + Left(A), + Right(B), +} + +impl Iterator for TwoIterators +where + A: Iterator, + B: Iterator, +{ + type Item = T; + + fn next(&mut self) -> Option { + match self { + TwoIterators::Left(a) => a.next(), + TwoIterators::Right(b) => b.next(), + } + } +} + +enum FourIterators { + First(A), + Second(B), + Third(C), + Fourth(D), +} + +impl Iterator for FourIterators +where + A: Iterator, + B: Iterator, + C: Iterator, + D: Iterator, +{ + type Item = T; + + fn next(&mut self) -> Option { + match self { + FourIterators::First(a) => a.next(), + FourIterators::Second(b) => b.next(), + FourIterators::Third(c) => c.next(), + FourIterators::Fourth(d) => d.next(), + } + } +} + +/// Helper function for cubic spline interpolation. +fn cubic_spline_interpolation( + value_start: T, + tangent_out_start: T, + tangent_in_end: T, + value_end: T, + lerp: f32, + step_duration: f32, +) -> T +where + T: VectorSpace, +{ + value_start * (2.0 * lerp.powi(3) - 3.0 * lerp.powi(2) + 1.0) + + tangent_out_start * (step_duration) * (lerp.powi(3) - 2.0 * lerp.powi(2) + lerp) + + value_end * (-2.0 * lerp.powi(3) + 3.0 * lerp.powi(2)) + + tangent_in_end * step_duration * (lerp.powi(3) - lerp.powi(2)) +} + +fn cubic_spline_interpolate_slices<'a, T: VectorSpace>( + width: usize, + first: &'a [T], + second: &'a [T], + s: f32, + step_between: f32, +) -> impl Iterator + 'a { + (0..width).map(move |idx| { + cubic_spline_interpolation( + first[idx + width], + first[idx + (width * 2)], + second[idx + width], + second[idx], + s, + step_between, + ) + }) +} diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index e8e394b957df6..d68cd1e007e06 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -8,15 +8,18 @@ //! Animation for the game engine Bevy mod animatable; +pub mod curves; mod graph; mod transition; mod util; +use bevy_math::curve::{iterable::IterableCurve, Curve}; +use curves::VariableCurve; + use std::cell::RefCell; use std::collections::BTreeMap; use std::hash::{Hash, Hasher}; use std::iter; -use std::ops::{Add, Mul}; use bevy_app::{App, Plugin, PostUpdate}; use bevy_asset::{Asset, AssetApp, Assets, Handle}; @@ -24,7 +27,7 @@ use bevy_core::Name; use bevy_ecs::entity::MapEntities; use bevy_ecs::prelude::*; use bevy_ecs::reflect::ReflectMapEntities; -use bevy_math::{FloatExt, Quat, Vec3}; +use bevy_math::FloatExt; use bevy_reflect::Reflect; use bevy_render::mesh::morph::MorphWeights; use bevy_time::Time; @@ -46,8 +49,8 @@ use uuid::Uuid; pub mod prelude { #[doc(hidden)] pub use crate::{ - animatable::*, graph::*, transition::*, AnimationClip, AnimationPlayer, AnimationPlugin, - Interpolation, Keyframes, VariableCurve, + animatable::*, curves::VariableCurve, graph::*, transition::*, AnimationClip, + AnimationPlayer, AnimationPlugin, }; } @@ -58,140 +61,6 @@ use crate::transition::{advance_transitions, expire_completed_transitions}; /// [UUID namespace]: https://en.wikipedia.org/wiki/Universally_unique_identifier#Versions_3_and_5_(namespace_name-based) pub static ANIMATION_TARGET_NAMESPACE: Uuid = Uuid::from_u128(0x3179f519d9274ff2b5966fd077023911); -/// List of keyframes for one of the attribute of a [`Transform`]. -#[derive(Reflect, Clone, Debug)] -pub enum Keyframes { - /// Keyframes for rotation. - Rotation(Vec), - /// Keyframes for translation. - Translation(Vec), - /// Keyframes for scale. - Scale(Vec), - /// Keyframes for morph target weights. - /// - /// Note that in `.0`, each contiguous `target_count` values is a single - /// keyframe representing the weight values at given keyframe. - /// - /// This follows the [glTF design]. - /// - /// [glTF design]: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#animations - Weights(Vec), -} - -impl Keyframes { - /// Returns the number of keyframes. - pub fn len(&self) -> usize { - match self { - Keyframes::Weights(vec) => vec.len(), - Keyframes::Translation(vec) | Keyframes::Scale(vec) => vec.len(), - Keyframes::Rotation(vec) => vec.len(), - } - } - - /// Returns true if the number of keyframes is zero. - pub fn is_empty(&self) -> bool { - self.len() == 0 - } -} - -/// Describes how an attribute of a [`Transform`] or [`MorphWeights`] should be animated. -/// -/// `keyframe_timestamps` and `keyframes` should have the same length. -#[derive(Reflect, Clone, Debug)] -pub struct VariableCurve { - /// Timestamp for each of the keyframes. - pub keyframe_timestamps: Vec, - /// List of the keyframes. - /// - /// The representation will depend on the interpolation type of this curve: - /// - /// - for `Interpolation::Step` and `Interpolation::Linear`, each keyframe is a single value - /// - for `Interpolation::CubicSpline`, each keyframe is made of three values for `tangent_in`, - /// `keyframe_value` and `tangent_out` - pub keyframes: Keyframes, - /// Interpolation method to use between keyframes. - pub interpolation: Interpolation, -} - -impl VariableCurve { - /// Find the index of the keyframe at or before the current time. - /// - /// Returns [`None`] if the curve is finished or not yet started. - /// To be more precise, this returns [`None`] if the frame is at or past the last keyframe: - /// we cannot get the *next* keyframe to interpolate to in that case. - pub fn find_current_keyframe(&self, seek_time: f32) -> Option { - // 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()); - - // Subtract one for zero indexing! - let last_keyframe = self.keyframe_timestamps.len() - 1; - - // 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. - let step_start = match search_result { - // An exact match was found, and it is the last keyframe (or something has gone terribly wrong). - // This means that the curve is finished. - Ok(n) if n >= last_keyframe => return None, - // An exact match was found, and it is not the last keyframe. - Ok(i) => i, - // No exact match was found, and the seek_time is before the start of the animation. - // This occurs because the binary search returns the index of where we could insert a value - // without disrupting the order of the vector. - // If the value is less than the first element, the index will be 0. - Err(0) => return None, - // No exact match was found, and it was after the last keyframe. - // The curve is finished. - Err(n) if n > last_keyframe => return None, - // No exact match was found, so return the previous keyframe to interpolate from. - Err(i) => i - 1, - }; - - // Consumers need to be able to interpolate between the return keyframe and the next - assert!(step_start < self.keyframe_timestamps.len()); - - 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. -#[derive(Reflect, Clone, Debug)] -pub enum Interpolation { - /// Linear interpolation between the two closest keyframes. - Linear, - /// Step interpolation, the value of the start keyframe is used. - Step, - /// Cubic spline interpolation. The value of the two closest keyframes is used, with the out - /// tangent of the start keyframe and the in tangent of the end keyframe. - CubicSpline, -} - /// A list of [`VariableCurve`]s and the [`AnimationTargetId`]s to which they /// apply. /// @@ -326,9 +195,7 @@ impl AnimationClip { /// curve covers. pub fn add_curve_to_target(&mut self, target_id: AnimationTargetId, curve: VariableCurve) { // Update the duration of the animation by this curve duration if it's longer - self.duration = self - .duration - .max(*curve.keyframe_timestamps.last().unwrap_or(&0.0)); + self.duration = self.duration.max(curve.duration().unwrap_or(0.0)); self.curves.entry(target_id).or_default().push(curve); } } @@ -891,250 +758,47 @@ pub fn animate_targets( } impl AnimationTargetContext<'_> { - /// Applies a clip to a single animation target according to the - /// [`AnimationTargetContext`]. + /// Applies a clip to a single animation target according to the [`AnimationTargetContext`]. fn apply(&mut self, curves: &[VariableCurve], weight: f32, seek_time: f32) { for curve in curves { - // Some curves have only one keyframe used to set a transform - if curve.keyframe_timestamps.len() == 1 { - self.apply_single_keyframe(curve, weight); - 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).clamp(0.0, 1.0); - - self.apply_tweened_keyframe( - curve, - step_start, - lerp, - weight, - timestamp_end - timestamp_start, - ); - } - } - - fn apply_single_keyframe(&mut self, curve: &VariableCurve, weight: f32) { - match &curve.keyframes { - Keyframes::Rotation(keyframes) => { - if let Some(ref mut transform) = self.transform { - transform.rotation = transform.rotation.slerp(keyframes[0], weight); + match curve { + VariableCurve::Translation(translation_curve) => { + if let Some(ref mut transform) = self.transform { + transform.translation = transform + .translation + .lerp(translation_curve.sample_unchecked(seek_time), weight); + } } - } - - Keyframes::Translation(keyframes) => { - if let Some(ref mut transform) = self.transform { - transform.translation = transform.translation.lerp(keyframes[0], weight); + VariableCurve::Rotation(rotation_curve) => { + if let Some(ref mut transform) = self.transform { + transform.rotation = transform + .rotation + .slerp(rotation_curve.sample_unchecked(seek_time), weight); + } } - } - - Keyframes::Scale(keyframes) => { - if let Some(ref mut transform) = self.transform { - transform.scale = transform.scale.lerp(keyframes[0], weight); + VariableCurve::Scale(scale_curve) => { + if let Some(ref mut transform) = self.transform { + transform.scale = transform + .scale + .lerp(scale_curve.sample_unchecked(seek_time), weight); + } } - } - - Keyframes::Weights(keyframes) => { - let Some(ref mut morphs) = self.morph_weights else { - error!( - "Tried to animate morphs on {:?} ({:?}), but no `MorphWeights` was found", - self.entity, self.name, + VariableCurve::Weights(weights_curve) => { + let Some(ref mut morphs) = self.morph_weights else { + error!( + "Tried to animate morphs on {:?} ({:?}), but no `MorphWeights` was found", + self.entity, self.name, + ); + return; + }; + + lerp_morph_weights( + morphs.weights_mut(), + weights_curve.sample_iter_unchecked(seek_time), + weight, ); - return; - }; - - let target_count = morphs.weights().len(); - lerp_morph_weights( - morphs.weights_mut(), - get_keyframe(target_count, keyframes, 0).iter().copied(), - weight, - ); - } - } - } - - fn apply_tweened_keyframe( - &mut self, - curve: &VariableCurve, - step_start: usize, - lerp: f32, - weight: f32, - duration: f32, - ) { - match (&curve.interpolation, &curve.keyframes) { - (Interpolation::Step, Keyframes::Rotation(keyframes)) => { - if let Some(ref mut transform) = self.transform { - transform.rotation = transform.rotation.slerp(keyframes[step_start], weight); - } - } - - (Interpolation::Linear, Keyframes::Rotation(keyframes)) => { - let Some(ref mut transform) = self.transform else { - return; - }; - - let rot_start = keyframes[step_start]; - let rot_end = keyframes[step_start + 1]; - - // Rotations are using a spherical linear interpolation - let rot = rot_start.slerp(rot_end, lerp); - transform.rotation = transform.rotation.slerp(rot, weight); - } - - (Interpolation::CubicSpline, Keyframes::Rotation(keyframes)) => { - let Some(ref mut transform) = self.transform else { - return; - }; - - let value_start = keyframes[step_start * 3 + 1]; - let tangent_out_start = keyframes[step_start * 3 + 2]; - let tangent_in_end = keyframes[(step_start + 1) * 3]; - let value_end = keyframes[(step_start + 1) * 3 + 1]; - let result = cubic_spline_interpolation( - value_start, - tangent_out_start, - tangent_in_end, - value_end, - lerp, - duration, - ); - transform.rotation = transform.rotation.slerp(result.normalize(), weight); - } - - (Interpolation::Step, Keyframes::Translation(keyframes)) => { - if let Some(ref mut transform) = self.transform { - transform.translation = - transform.translation.lerp(keyframes[step_start], weight); - } - } - - (Interpolation::Linear, Keyframes::Translation(keyframes)) => { - let Some(ref mut transform) = self.transform else { - return; - }; - - let translation_start = keyframes[step_start]; - let translation_end = keyframes[step_start + 1]; - let result = translation_start.lerp(translation_end, lerp); - transform.translation = transform.translation.lerp(result, weight); - } - - (Interpolation::CubicSpline, Keyframes::Translation(keyframes)) => { - let Some(ref mut transform) = self.transform else { - return; - }; - - let value_start = keyframes[step_start * 3 + 1]; - let tangent_out_start = keyframes[step_start * 3 + 2]; - let tangent_in_end = keyframes[(step_start + 1) * 3]; - let value_end = keyframes[(step_start + 1) * 3 + 1]; - let result = cubic_spline_interpolation( - value_start, - tangent_out_start, - tangent_in_end, - value_end, - lerp, - duration, - ); - transform.translation = transform.translation.lerp(result, weight); - } - - (Interpolation::Step, Keyframes::Scale(keyframes)) => { - if let Some(ref mut transform) = self.transform { - transform.scale = transform.scale.lerp(keyframes[step_start], weight); } } - - (Interpolation::Linear, Keyframes::Scale(keyframes)) => { - let Some(ref mut transform) = self.transform else { - return; - }; - - let scale_start = keyframes[step_start]; - let scale_end = keyframes[step_start + 1]; - let result = scale_start.lerp(scale_end, lerp); - transform.scale = transform.scale.lerp(result, weight); - } - - (Interpolation::CubicSpline, Keyframes::Scale(keyframes)) => { - let Some(ref mut transform) = self.transform else { - return; - }; - - let value_start = keyframes[step_start * 3 + 1]; - let tangent_out_start = keyframes[step_start * 3 + 2]; - let tangent_in_end = keyframes[(step_start + 1) * 3]; - let value_end = keyframes[(step_start + 1) * 3 + 1]; - let result = cubic_spline_interpolation( - value_start, - tangent_out_start, - tangent_in_end, - value_end, - lerp, - duration, - ); - transform.scale = transform.scale.lerp(result, weight); - } - - (Interpolation::Step, Keyframes::Weights(keyframes)) => { - let Some(ref mut morphs) = self.morph_weights else { - return; - }; - - let target_count = morphs.weights().len(); - let morph_start = get_keyframe(target_count, keyframes, step_start); - lerp_morph_weights(morphs.weights_mut(), morph_start.iter().copied(), weight); - } - - (Interpolation::Linear, Keyframes::Weights(keyframes)) => { - let Some(ref mut morphs) = self.morph_weights else { - return; - }; - - let target_count = morphs.weights().len(); - let morph_start = get_keyframe(target_count, keyframes, step_start); - let morph_end = get_keyframe(target_count, keyframes, step_start + 1); - let result = morph_start - .iter() - .zip(morph_end) - .map(|(a, b)| a.lerp(*b, lerp)); - lerp_morph_weights(morphs.weights_mut(), result, weight); - } - - (Interpolation::CubicSpline, Keyframes::Weights(keyframes)) => { - let Some(ref mut morphs) = self.morph_weights else { - return; - }; - - let target_count = morphs.weights().len(); - let morph_start = get_keyframe(target_count, keyframes, step_start * 3 + 1); - let tangents_out_start = get_keyframe(target_count, keyframes, step_start * 3 + 2); - let tangents_in_end = get_keyframe(target_count, keyframes, (step_start + 1) * 3); - let morph_end = get_keyframe(target_count, keyframes, (step_start + 1) * 3 + 1); - let result = morph_start - .iter() - .zip(tangents_out_start) - .zip(tangents_in_end) - .zip(morph_end) - .map( - |(((&value_start, &tangent_out_start), &tangent_in_end), &value_end)| { - cubic_spline_interpolation( - value_start, - tangent_out_start, - tangent_in_end, - value_end, - lerp, - duration, - ) - }, - ); - lerp_morph_weights(morphs.weights_mut(), result, weight); - } } } } @@ -1148,39 +812,6 @@ fn lerp_morph_weights(weights: &mut [f32], keyframe: impl Iterator, } } -/// Extract a keyframe from a list of keyframes by index. -/// -/// # Panics -/// -/// When `key_index * target_count` is larger than `keyframes` -/// -/// This happens when `keyframes` is not formatted as described in -/// [`Keyframes::Weights`]. A possible cause is [`AnimationClip`] not being -/// meant to be used for the [`MorphWeights`] of the entity it's being applied to. -fn get_keyframe(target_count: usize, keyframes: &[f32], key_index: usize) -> &[f32] { - let start = target_count * key_index; - let end = target_count * (key_index + 1); - &keyframes[start..end] -} - -/// Helper function for cubic spline interpolation. -fn cubic_spline_interpolation( - value_start: T, - tangent_out_start: T, - tangent_in_end: T, - value_end: T, - lerp: f32, - step_duration: f32, -) -> T -where - T: Mul + Add, -{ - value_start * (2.0 * lerp.powi(3) - 3.0 * lerp.powi(2) + 1.0) - + tangent_out_start * (step_duration) * (lerp.powi(3) - 2.0 * lerp.powi(2) + lerp) - + value_end * (-2.0 * lerp.powi(3) + 3.0 * lerp.powi(2)) - + tangent_in_end * step_duration * (lerp.powi(3) - lerp.powi(2)) -} - /// Adds animation support to an app #[derive(Default)] pub struct AnimationPlugin; @@ -1256,152 +887,3 @@ impl AnimationGraphEvaluator { self.weights.extend(iter::repeat(0.0).take(node_count)); } } - -#[cfg(test)] -mod tests { - use crate::VariableCurve; - use bevy_math::Vec3; - - fn test_variable_curve() -> VariableCurve { - let keyframe_timestamps = vec![1.0, 2.0, 3.0, 4.0]; - let keyframes = vec![ - Vec3::ONE * 0.0, - Vec3::ONE * 3.0, - Vec3::ONE * 6.0, - Vec3::ONE * 9.0, - ]; - let interpolation = crate::Interpolation::Linear; - - let variable_curve = VariableCurve { - keyframe_timestamps, - keyframes: crate::Keyframes::Translation(keyframes), - interpolation, - }; - - assert!(variable_curve.keyframe_timestamps.len() == variable_curve.keyframes.len()); - - // f32 doesn't impl Ord so we can't easily sort it - let mut maybe_last_timestamp = None; - for current_timestamp in &variable_curve.keyframe_timestamps { - assert!(current_timestamp.is_finite()); - - if let Some(last_timestamp) = maybe_last_timestamp { - assert!(current_timestamp > last_timestamp); - } - maybe_last_timestamp = Some(current_timestamp); - } - - variable_curve - } - - #[test] - fn find_current_keyframe_is_in_bounds() { - let curve = test_variable_curve(); - let min_time = *curve.keyframe_timestamps.first().unwrap(); - // We will always get none at times at or past the second last keyframe - let second_last_keyframe = curve.keyframe_timestamps.len() - 2; - let max_time = curve.keyframe_timestamps[second_last_keyframe]; - let elapsed_time = max_time - min_time; - - let n_keyframes = curve.keyframe_timestamps.len(); - let n_test_points = 5; - - for i in 0..=n_test_points { - // Get a value between 0 and 1 - let normalized_time = i as f32 / n_test_points as f32; - let seek_time = min_time + normalized_time * elapsed_time; - assert!(seek_time >= min_time); - assert!(seek_time <= max_time); - - let maybe_current_keyframe = curve.find_current_keyframe(seek_time); - assert!( - maybe_current_keyframe.is_some(), - "Seek time: {seek_time}, Min time: {min_time}, Max time: {max_time}" - ); - - // We cannot return the last keyframe, - // because we want to interpolate between the current and next keyframe - assert!(maybe_current_keyframe.unwrap() < n_keyframes); - } - } - - #[test] - fn find_current_keyframe_returns_none_on_unstarted_animations() { - let curve = test_variable_curve(); - let min_time = *curve.keyframe_timestamps.first().unwrap(); - let seek_time = 0.0; - assert!(seek_time < min_time); - - let maybe_keyframe = curve.find_current_keyframe(seek_time); - assert!( - maybe_keyframe.is_none(), - "Seek time: {seek_time}, Minimum time: {min_time}" - ); - } - - #[test] - fn find_current_keyframe_returns_none_on_finished_animation() { - let curve = test_variable_curve(); - let max_time = *curve.keyframe_timestamps.last().unwrap(); - - assert!(max_time < f32::INFINITY); - let maybe_keyframe = curve.find_current_keyframe(f32::INFINITY); - assert!(maybe_keyframe.is_none()); - - let maybe_keyframe = curve.find_current_keyframe(max_time); - assert!(maybe_keyframe.is_none()); - } - - #[test] - fn second_last_keyframe_is_found_correctly() { - let curve = test_variable_curve(); - - // Exact time match - let second_last_keyframe = curve.keyframe_timestamps.len() - 2; - let second_last_time = curve.keyframe_timestamps[second_last_keyframe]; - let maybe_keyframe = curve.find_current_keyframe(second_last_time); - assert!(maybe_keyframe.unwrap() == second_last_keyframe); - - // Inexact match, between the last and second last frames - let seek_time = second_last_time + 0.001; - let last_time = curve.keyframe_timestamps[second_last_keyframe + 1]; - assert!(seek_time < last_time); - - let maybe_keyframe = curve.find_current_keyframe(seek_time); - assert!(maybe_keyframe.unwrap() == second_last_keyframe); - } - - #[test] - fn exact_keyframe_matches_are_found_correctly() { - let curve = test_variable_curve(); - let second_last_keyframe = curve.keyframes.len() - 2; - - for i in 0..=second_last_keyframe { - let seek_time = curve.keyframe_timestamps[i]; - - let keyframe = curve.find_current_keyframe(seek_time).unwrap(); - assert!(keyframe == i); - } - } - - #[test] - fn exact_and_inexact_keyframes_correspond() { - let curve = test_variable_curve(); - - let second_last_keyframe = curve.keyframes.len() - 2; - - for i in 0..=second_last_keyframe { - let seek_time = curve.keyframe_timestamps[i]; - - let exact_keyframe = curve.find_current_keyframe(seek_time).unwrap(); - - let inexact_seek_time = seek_time + 0.0001; - let final_time = *curve.keyframe_timestamps.last().unwrap(); - assert!(inexact_seek_time < final_time); - - let inexact_keyframe = curve.find_current_keyframe(inexact_seek_time).unwrap(); - - assert!(exact_keyframe == inexact_keyframe); - } - } -} diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index 3ae1c6957927a..48dcdb9386cd9 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -263,7 +263,9 @@ async fn load_gltf<'a, 'b, 'c>( #[cfg(feature = "bevy_animation")] let (animations, named_animations, animation_roots) = { - use bevy_animation::{Interpolation, Keyframes}; + use bevy_animation::curves::*; + use bevy_math::curve::{constant_curve, Interval, UnevenSampleAutoCurve}; + use bevy_math::{Quat, Vec4}; use gltf::animation::util::ReadOutputs; let mut animations = vec![]; let mut named_animations = HashMap::default(); @@ -271,12 +273,8 @@ async fn load_gltf<'a, 'b, 'c>( for animation in gltf.animations() { let mut animation_clip = bevy_animation::AnimationClip::default(); for channel in animation.channels() { - let interpolation = match channel.sampler().interpolation() { - gltf::animation::Interpolation::Linear => Interpolation::Linear, - gltf::animation::Interpolation::Step => Interpolation::Step, - gltf::animation::Interpolation::CubicSpline => Interpolation::CubicSpline, - }; let node = channel.target().node(); + let interpolation = channel.sampler().interpolation(); let reader = channel.reader(|buffer| Some(&buffer_data[buffer.index()])); let keyframe_timestamps: Vec = if let Some(inputs) = reader.read_inputs() { match inputs { @@ -291,19 +289,155 @@ async fn load_gltf<'a, 'b, 'c>( return Err(GltfError::MissingAnimationSampler(animation.index())); }; - let keyframes = if let Some(outputs) = reader.read_outputs() { + if keyframe_timestamps.is_empty() { + warn!("Tried to load animation with no keyframe timestamps"); + continue; + } + + let maybe_curve: Option = if let Some(outputs) = + reader.read_outputs() + { match outputs { ReadOutputs::Translations(tr) => { - Keyframes::Translation(tr.map(Vec3::from).collect()) + let translations: Vec = tr.map(Vec3::from).collect(); + if keyframe_timestamps.len() == 1 { + Some(VariableCurve::Translation(TranslationCurve::Constant( + constant_curve(Interval::EVERYWHERE, translations[0]), + ))) + } else { + match interpolation { + gltf::animation::Interpolation::Linear => { + UnevenSampleAutoCurve::new( + keyframe_timestamps.into_iter().zip(translations), + ) + .ok() + .map(|c| { + VariableCurve::Translation(TranslationCurve::Linear(c)) + }) + } + gltf::animation::Interpolation::Step => { + SteppedKeyframeCurve::new( + keyframe_timestamps.into_iter().zip(translations), + ) + .ok() + .map(|c| { + VariableCurve::Translation(TranslationCurve::Step(c)) + }) + } + gltf::animation::Interpolation::CubicSpline => { + CubicKeyframeCurve::new(keyframe_timestamps, translations) + .ok() + .map(|c| { + VariableCurve::Translation( + TranslationCurve::CubicSpline(c), + ) + }) + } + } + } + } + ReadOutputs::Rotations(rots) => { + let rotations: Vec = + rots.into_f32().map(bevy_math::Quat::from_array).collect(); + if keyframe_timestamps.len() == 1 { + Some(VariableCurve::Rotation(RotationCurve::Constant( + constant_curve(Interval::EVERYWHERE, rotations[0]), + ))) + } else { + match interpolation { + gltf::animation::Interpolation::Linear => { + UnevenSampleAutoCurve::new( + keyframe_timestamps.into_iter().zip(rotations), + ) + .ok() + .map(|c| { + VariableCurve::Rotation(RotationCurve::SphericalLinear( + c, + )) + }) + } + gltf::animation::Interpolation::Step => { + SteppedKeyframeCurve::new( + keyframe_timestamps.into_iter().zip(rotations), + ) + .ok() + .map(|c| VariableCurve::Rotation(RotationCurve::Step(c))) + } + gltf::animation::Interpolation::CubicSpline => { + CubicKeyframeCurve::new( + keyframe_timestamps, + rotations.into_iter().map(Vec4::from), + ) + .ok() + .map(|c| { + VariableCurve::Rotation(RotationCurve::CubicSpline(c)) + }) + } + } + } } - ReadOutputs::Rotations(rots) => Keyframes::Rotation( - rots.into_f32().map(bevy_math::Quat::from_array).collect(), - ), ReadOutputs::Scales(scale) => { - Keyframes::Scale(scale.map(Vec3::from).collect()) + let scales: Vec = scale.map(Vec3::from).collect(); + if keyframe_timestamps.len() == 1 { + Some(VariableCurve::Scale(ScaleCurve::Constant(constant_curve( + Interval::EVERYWHERE, + scales[0], + )))) + } else { + match interpolation { + gltf::animation::Interpolation::Linear => { + UnevenSampleAutoCurve::new( + keyframe_timestamps.into_iter().zip(scales), + ) + .ok() + .map(|c| VariableCurve::Scale(ScaleCurve::Linear(c))) + } + gltf::animation::Interpolation::Step => { + SteppedKeyframeCurve::new( + keyframe_timestamps.into_iter().zip(scales), + ) + .ok() + .map(|c| VariableCurve::Scale(ScaleCurve::Step(c))) + } + gltf::animation::Interpolation::CubicSpline => { + CubicKeyframeCurve::new(keyframe_timestamps, scales) + .ok() + .map(|c| { + VariableCurve::Scale(ScaleCurve::CubicSpline(c)) + }) + } + } + } } ReadOutputs::MorphTargetWeights(weights) => { - Keyframes::Weights(weights.into_f32().collect()) + let weights: Vec = weights.into_f32().collect(); + if keyframe_timestamps.len() == 1 { + Some(VariableCurve::Weights(WeightsCurve::Constant( + constant_curve(Interval::EVERYWHERE, weights), + ))) + } else { + match interpolation { + gltf::animation::Interpolation::Linear => { + WideLinearKeyframeCurve::new(keyframe_timestamps, weights) + .ok() + .map(|c| { + VariableCurve::Weights(WeightsCurve::Linear(c)) + }) + } + gltf::animation::Interpolation::Step => { + WideSteppedKeyframeCurve::new(keyframe_timestamps, weights) + .ok() + .map(|c| VariableCurve::Weights(WeightsCurve::Step(c))) + } + gltf::animation::Interpolation::CubicSpline => { + WideCubicKeyframeCurve::new(keyframe_timestamps, weights) + .ok() + .map(|c| { + VariableCurve::Weights(WeightsCurve::CubicSpline(c)) + }) + } + } + } } } } else { @@ -311,16 +445,18 @@ async fn load_gltf<'a, 'b, 'c>( return Err(GltfError::MissingAnimationSampler(animation.index())); }; + let Some(curve) = maybe_curve else { + warn!( + "Invalid keyframe data for node {}; curve could not be constructed", + node.index() + ); + continue; + }; + if let Some((root_index, path)) = paths.get(&node.index()) { animation_roots.insert(*root_index); - animation_clip.add_curve_to_target( - AnimationTargetId::from_names(path.iter()), - bevy_animation::VariableCurve { - keyframe_timestamps, - keyframes, - interpolation, - }, - ); + animation_clip + .add_curve_to_target(AnimationTargetId::from_names(path.iter()), curve); } else { warn!( "Animation ignored for node {}: part of its hierarchy is missing a name", diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs index 92ad2fdc71a3c..6e637c4ba78a0 100644 --- a/crates/bevy_math/src/curve/cores.rs +++ b/crates/bevy_math/src/curve/cores.rs @@ -496,6 +496,15 @@ pub enum ChunkedUnevenCoreError { /// The actual length of the value buffer. actual: usize, }, + + /// Tried to infer the width, but the ratio of lengths wasn't an integer, so no such length exists. + #[error("The length of the list of values ({values_len}) was not divisible by that of the list of times ({times_len})")] + NonDivisibleLengths { + /// The length of the value buffer. + values_len: usize, + /// The length of the time buffer. + times_len: usize, + }, } impl ChunkedUnevenCore { @@ -504,17 +513,17 @@ impl ChunkedUnevenCore { /// /// Produces an error in any of the following circumstances: /// - `width` is zero. - /// - `times` has less than `2` valid unique entries. + /// - `times` has less than `2` unique valid entries. /// - `values` has the incorrect length relative to `times`. /// /// [type-level documentation]: ChunkedUnevenCore pub fn new( - times: impl Into>, - values: impl Into>, + times: impl IntoIterator, + values: impl IntoIterator, width: usize, ) -> Result { - let times: Vec = times.into(); - let values: Vec = values.into(); + let times = times.into_iter().collect_vec(); + let values = values.into_iter().collect_vec(); if width == 0 { return Err(ChunkedUnevenCoreError::ZeroWidth); @@ -538,6 +547,52 @@ impl ChunkedUnevenCore { Ok(Self { times, values }) } + /// Create a new [`ChunkedUnevenCore`], inferring the width from the sizes of the inputs. + /// The given `times` are sorted, filtered to finite times, and deduplicated. See the + /// [type-level documentation] for more information about this type. Prefer using [`new`] + /// if possible, since that constructor has richer error checking. + /// + /// Produces an error in any of the following circumstances: + /// - `values` has length zero. + /// - `times` has less than `2` unique valid entries. + /// - The length of `values` is not divisible by that of `times` (once sorted, filtered, + /// and deduplicated). + /// + /// The [width] is implicitly taken to be the length of `values` divided by that of `times` + /// (once sorted, filtered, and deduplicated). + /// + /// [type-level documentation]: ChunkedUnevenCore + /// [`new`]: ChunkedUnevenCore::new + /// [width]: ChunkedUnevenCore::width + pub fn new_width_inferred( + times: impl IntoIterator, + values: impl IntoIterator, + ) -> Result { + let times = times.into_iter().collect_vec(); + let values = values.into_iter().collect_vec(); + + let times = filter_sort_dedup_times(times); + + if times.len() < 2 { + return Err(ChunkedUnevenCoreError::NotEnoughSamples { + samples: times.len(), + }); + } + + if values.len() % times.len() != 0 { + return Err(ChunkedUnevenCoreError::NonDivisibleLengths { + values_len: values.len(), + times_len: times.len(), + }); + } + + if values.is_empty() { + return Err(ChunkedUnevenCoreError::ZeroWidth); + } + + Ok(Self { times, values }) + } + /// The domain of the curve derived from this core. /// /// # Panics @@ -626,3 +681,134 @@ pub fn uneven_interp(times: &[f32], t: f32) -> InterpolationDatum { } } } + +#[cfg(test)] +mod tests { + use super::{ChunkedUnevenCore, EvenCore, UnevenCore}; + use crate::curve::{cores::InterpolationDatum, interval}; + use approx::{assert_abs_diff_eq, AbsDiffEq}; + + fn approx_between(datum: InterpolationDatum, start: T, end: T, p: f32) -> bool + where + T: PartialEq, + { + if let InterpolationDatum::Between(m_start, m_end, m_p) = datum { + m_start == start && m_end == end && m_p.abs_diff_eq(&p, 1e-6) + } else { + false + } + } + + fn is_left_tail(datum: InterpolationDatum) -> bool { + matches!(datum, InterpolationDatum::LeftTail(_)) + } + + fn is_right_tail(datum: InterpolationDatum) -> bool { + matches!(datum, InterpolationDatum::RightTail(_)) + } + + fn is_exact(datum: InterpolationDatum, target: T) -> bool + where + T: PartialEq, + { + if let InterpolationDatum::Exact(v) = datum { + v == target + } else { + false + } + } + + #[test] + fn even_sample_interp() { + let even_core = EvenCore::::new( + interval(0.0, 1.0).unwrap(), + // 11 entries -> 10 segments + vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0], + ) + .expect("Failed to construct test core"); + + let datum = even_core.sample_interp(-1.0); + assert!(is_left_tail(datum)); + let datum = even_core.sample_interp(0.0); + assert!(is_left_tail(datum)); + let datum = even_core.sample_interp(1.0); + assert!(is_right_tail(datum)); + let datum = even_core.sample_interp(2.0); + assert!(is_right_tail(datum)); + + let datum = even_core.sample_interp(0.05); + let InterpolationDatum::Between(0.0, 1.0, p) = datum else { + panic!("Sample did not lie in the correct subinterval") + }; + assert_abs_diff_eq!(p, 0.5); + + let datum = even_core.sample_interp(0.05); + assert!(approx_between(datum, &0.0, &1.0, 0.5)); + let datum = even_core.sample_interp(0.33); + assert!(approx_between(datum, &3.0, &4.0, 0.3)); + let datum = even_core.sample_interp(0.78); + assert!(approx_between(datum, &7.0, &8.0, 0.8)); + + let datum = even_core.sample_interp(0.5); + assert!(approx_between(datum, &4.0, &5.0, 1.0) || approx_between(datum, &5.0, &6.0, 0.0)); + let datum = even_core.sample_interp(0.7); + assert!(approx_between(datum, &6.0, &7.0, 1.0) || approx_between(datum, &7.0, &8.0, 0.0)); + } + + #[test] + fn uneven_sample_interp() { + let uneven_core = UnevenCore::::new(vec![ + (0.0, 0.0), + (1.0, 3.0), + (2.0, 9.0), + (4.0, 10.0), + (8.0, -5.0), + ]) + .expect("Failed to construct test core"); + + let datum = uneven_core.sample_interp(-1.0); + assert!(is_left_tail(datum)); + let datum = uneven_core.sample_interp(0.0); + assert!(is_exact(datum, &0.0)); + let datum = uneven_core.sample_interp(8.0); + assert!(is_exact(datum, &(-5.0))); + let datum = uneven_core.sample_interp(9.0); + assert!(is_right_tail(datum)); + + let datum = uneven_core.sample_interp(0.5); + assert!(approx_between(datum, &0.0, &3.0, 0.5)); + let datum = uneven_core.sample_interp(2.5); + assert!(approx_between(datum, &9.0, &10.0, 0.25)); + let datum = uneven_core.sample_interp(7.0); + assert!(approx_between(datum, &10.0, &(-5.0), 0.75)); + + let datum = uneven_core.sample_interp(2.0); + assert!(is_exact(datum, &9.0)); + let datum = uneven_core.sample_interp(4.0); + assert!(is_exact(datum, &10.0)); + } + + #[test] + fn chunked_uneven_sample_interp() { + let core = + ChunkedUnevenCore::new(vec![0.0, 2.0, 8.0], vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0], 2) + .expect("Failed to construct test core"); + + let datum = core.sample_interp(-1.0); + assert!(is_left_tail(datum)); + let datum = core.sample_interp(0.0); + assert!(is_exact(datum, &[0.0, 1.0])); + let datum = core.sample_interp(8.0); + assert!(is_exact(datum, &[4.0, 5.0])); + let datum = core.sample_interp(10.0); + assert!(is_right_tail(datum)); + + let datum = core.sample_interp(1.0); + assert!(approx_between(datum, &[0.0, 1.0], &[2.0, 3.0], 0.5)); + let datum = core.sample_interp(3.0); + assert!(approx_between(datum, &[2.0, 3.0], &[4.0, 5.0], 1.0 / 6.0)); + + let datum = core.sample_interp(2.0); + assert!(is_exact(datum, &[2.0, 3.0])); + } +} diff --git a/crates/bevy_math/src/curve/iterable.rs b/crates/bevy_math/src/curve/iterable.rs new file mode 100644 index 0000000000000..6780aa38b8552 --- /dev/null +++ b/crates/bevy_math/src/curve/iterable.rs @@ -0,0 +1,58 @@ +//! Iterable curves, which sample in the form of an iterator in order to support `Vec`-like +//! output whose length cannot be known statically. + +use super::{ConstantCurve, Interval}; + +/// A curve which provides samples in the form of [`Iterator`]s. +/// +/// This is an abstraction that provides an interface for curves which look like `Curve>` +/// but side-stepping issues with allocation on sampling. This happens when the size of an output +/// array cannot be known statically. +pub trait IterableCurve { + /// The interval over which this curve is parametrized. + fn domain(&self) -> Interval; + + /// Sample a point on this curve at the parameter value `t`, producing an iterator over values. + /// This is the unchecked version of sampling, which should only be used if the sample time `t` + /// is already known to lie within the curve's domain. + /// + /// Values sampled from outside of a curve's domain are generally considered invalid; data which + /// is nonsensical or otherwise useless may be returned in such a circumstance, and extrapolation + /// beyond a curve's domain should not be relied upon. + fn sample_iter_unchecked<'a>(&self, t: f32) -> impl Iterator + where + Self: 'a; + + /// Sample this curve at a specified time `t`, producing an iterator over sampled values. + /// The parameter `t` is clamped to the domain of the curve. + fn sample_iter_clamped(&self, t: f32) -> impl Iterator { + let t_clamped = self.domain().clamp(t); + self.sample_iter_unchecked(t_clamped) + } + + /// Sample this curve at a specified time `t`, producing an iterator over sampled values. + /// If the parameter `t` does not lie in the curve's domain, `None` is returned. + fn sample_iter(&self, t: f32) -> Option> { + if self.domain().contains(t) { + Some(self.sample_iter_unchecked(t)) + } else { + None + } + } +} + +impl IterableCurve for ConstantCurve> +where + T: Clone, +{ + fn domain(&self) -> Interval { + self.domain + } + + fn sample_iter_unchecked<'a>(&self, _t: f32) -> impl Iterator + where + Self: 'a, + { + self.value.iter().cloned() + } +} diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 03cfd7c74c07f..da67439925403 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -4,6 +4,7 @@ pub mod cores; pub mod interval; +pub mod iterable; pub use interval::{interval, Interval}; use itertools::Itertools; diff --git a/examples/animation/animated_transform.rs b/examples/animation/animated_transform.rs index d85adf1028eb7..22cbe59d9145e 100644 --- a/examples/animation/animated_transform.rs +++ b/examples/animation/animated_transform.rs @@ -2,7 +2,9 @@ use std::f32::consts::PI; +use bevy::animation::curves::{RotationCurve, ScaleCurve, TranslationCurve}; use bevy::animation::{AnimationTarget, AnimationTargetId}; +use bevy::math::curve::UnevenSampleAutoCurve; use bevy::prelude::*; fn main() { @@ -51,19 +53,18 @@ fn setup( let planet_animation_target_id = AnimationTargetId::from_name(&planet); animation.add_curve_to_target( planet_animation_target_id, - VariableCurve { - keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0, 4.0], - keyframes: Keyframes::Translation(vec![ - Vec3::new(1.0, 0.0, 1.0), - Vec3::new(-1.0, 0.0, 1.0), - Vec3::new(-1.0, 0.0, -1.0), - Vec3::new(1.0, 0.0, -1.0), + VariableCurve::Translation(TranslationCurve::Linear( + UnevenSampleAutoCurve::new(vec![ + (0.0, Vec3::new(1.0, 0.0, 1.0)), + (1.0, Vec3::new(-1.0, 0.0, 1.0)), + (2.0, Vec3::new(-1.0, 0.0, -1.0)), + (3.0, Vec3::new(1.0, 0.0, -1.0)), // in case seamless looping is wanted, the last keyframe should // be the same as the first one - Vec3::new(1.0, 0.0, 1.0), - ]), - interpolation: Interpolation::Linear, - }, + (4.0, Vec3::new(1.0, 0.0, 1.0)), + ]) + .expect("Failed to build translation curve"), + )), ); // Or it can modify the rotation of the transform. // To find the entity to modify, the hierarchy will be traversed looking for @@ -72,17 +73,16 @@ fn setup( AnimationTargetId::from_names([planet.clone(), orbit_controller.clone()].iter()); animation.add_curve_to_target( orbit_controller_animation_target_id, - VariableCurve { - keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0, 4.0], - keyframes: Keyframes::Rotation(vec![ - Quat::IDENTITY, - Quat::from_axis_angle(Vec3::Y, PI / 2.), - Quat::from_axis_angle(Vec3::Y, PI / 2. * 2.), - Quat::from_axis_angle(Vec3::Y, PI / 2. * 3.), - Quat::IDENTITY, - ]), - interpolation: Interpolation::Linear, - }, + VariableCurve::Rotation(RotationCurve::SphericalLinear( + UnevenSampleAutoCurve::new(vec![ + (0.0, Quat::IDENTITY), + (1.0, Quat::from_axis_angle(Vec3::Y, PI / 2.)), + (2.0, Quat::from_axis_angle(Vec3::Y, PI / 2. * 2.)), + (3.0, Quat::from_axis_angle(Vec3::Y, PI / 2. * 3.)), + (4.0, Quat::IDENTITY), + ]) + .expect("Failed to build rotation curve"), + )), ); // If a curve in an animation is shorter than the other, it will not repeat // until all other curves are finished. In that case, another animation should @@ -92,38 +92,36 @@ fn setup( ); animation.add_curve_to_target( satellite_animation_target_id, - VariableCurve { - keyframe_timestamps: vec![0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0], - keyframes: Keyframes::Scale(vec![ - Vec3::splat(0.8), - Vec3::splat(1.2), - Vec3::splat(0.8), - Vec3::splat(1.2), - Vec3::splat(0.8), - Vec3::splat(1.2), - Vec3::splat(0.8), - Vec3::splat(1.2), - Vec3::splat(0.8), - ]), - interpolation: Interpolation::Linear, - }, + VariableCurve::Scale(ScaleCurve::Linear( + UnevenSampleAutoCurve::new(vec![ + (0.0, Vec3::splat(0.8)), + (0.5, Vec3::splat(1.2)), + (1.0, Vec3::splat(0.8)), + (1.5, Vec3::splat(1.2)), + (2.0, Vec3::splat(0.8)), + (2.5, Vec3::splat(1.2)), + (3.0, Vec3::splat(0.8)), + (3.5, Vec3::splat(1.2)), + (4.0, Vec3::splat(0.8)), + ]) + .expect("Failed to build scale curve"), + )), ); // There can be more than one curve targeting the same entity path animation.add_curve_to_target( AnimationTargetId::from_names( [planet.clone(), orbit_controller.clone(), satellite.clone()].iter(), ), - VariableCurve { - keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0, 4.0], - keyframes: Keyframes::Rotation(vec![ - Quat::IDENTITY, - Quat::from_axis_angle(Vec3::Y, PI / 2.), - Quat::from_axis_angle(Vec3::Y, PI / 2. * 2.), - Quat::from_axis_angle(Vec3::Y, PI / 2. * 3.), - Quat::IDENTITY, - ]), - interpolation: Interpolation::Linear, - }, + VariableCurve::Rotation(RotationCurve::SphericalLinear( + UnevenSampleAutoCurve::new(vec![ + (0.0, Quat::IDENTITY), + (1.0, Quat::from_axis_angle(Vec3::Y, PI / 2.)), + (2.0, Quat::from_axis_angle(Vec3::Y, PI / 2. * 2.)), + (3.0, Quat::from_axis_angle(Vec3::Y, PI / 2. * 3.)), + (4.0, Quat::IDENTITY), + ]) + .expect("Failed to build rotation curve"), + )), ); // Create the animation graph