From 429987ebf811da7fa30adc57dca5d83ed9c7e871 Mon Sep 17 00:00:00 2001 From: Matty Date: Mon, 30 Sep 2024 15:56:55 -0400 Subject: [PATCH] Curve-based animation (#15434) # Objective This PR extends and reworks the material from #15282 by allowing arbitrary curves to be used by the animation system to animate arbitrary properties. The goals of this work are to: - Allow far greater flexibility in how animations are allowed to be defined in order to be used with `bevy_animation`. - Delegate responsibility over keyframe interpolation to `bevy_math` and the `Curve` libraries and reduce reliance on keyframes in animation definitions generally. - Move away from allowing the glTF spec to completely define animations on a mechanical level. ## Solution ### Overview At a high level, curves have been incorporated into the animation system using the `AnimationCurve` trait (closely related to what was `Keyframes`). From the top down: 1. In `animate_targets`, animations are driven by `VariableCurve`, which is now a thin wrapper around a `Box`. 2. `AnimationCurve` is something built out of a `Curve`, and it tells the animation system how to use the curve's output to actually mutate component properties. The trait looks like this: ```rust /// A low-level trait that provides control over how curves are actually applied to entities /// by the animation system. /// /// Typically, this will not need to be implemented manually, since it is automatically /// implemented by [`AnimatableCurve`] and other curves used by the animation system /// (e.g. those that animate parts of transforms or morph weights). However, this can be /// implemented manually when `AnimatableCurve` is not sufficiently expressive. /// /// In many respects, this behaves like a type-erased form of [`Curve`], where the output /// type of the curve is remembered only in the components that are mutated in the /// implementation of [`apply`]. /// /// [`apply`]: AnimationCurve::apply pub trait AnimationCurve: Reflect + Debug + Send + Sync { /// Returns a boxed clone of this value. fn clone_value(&self) -> Box; /// The range of times for which this animation is defined. fn domain(&self) -> Interval; /// Write the value of sampling this curve at time `t` into `transform` or `entity`, /// as appropriate, interpolating between the existing value and the sampled value /// using the given `weight`. fn apply<'a>( &self, t: f32, transform: Option>, entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, weight: f32, ) -> Result<(), AnimationEvaluationError>; } ``` 3. The conversion process from a `Curve` to an `AnimationCurve` involves using wrappers which communicate the intent to animate a particular property. For example, here is `TranslationCurve`, which wraps a `Curve` and uses it to animate `Transform::translation`: ```rust /// This type allows a curve valued in `Vec3` to become an [`AnimationCurve`] that animates /// the translation component of a transform. pub struct TranslationCurve(pub C); ``` ### Animatable Properties The `AnimatableProperty` trait survives in the transition, and it can be used to allow curves to animate arbitrary component properties. The updated documentation for `AnimatableProperty` explains this process:
Expand AnimatableProperty example Option<&mut Self::Property> { Some(&mut component.sections.get_mut(0)?.style.font_size) } } ``` You can then create an `AnimationClip` to animate this property like so: ```rust let mut animation_clip = AnimationClip::default(); animation_clip.add_curve_to_target( animation_target_id, AnimatableKeyframeCurve::new( [ (0.0, 24.0), (1.0, 80.0), ] ) .map(AnimatableCurve::::from_curve) .expect("Failed to create font size curve") ); ``` Here, the use of `AnimatableKeyframeCurve` creates a curve out of the given keyframe time-value pairs, using the `Animatable` implementation of `f32` to interpolate between them. The invocation of `AnimatableCurve::from_curve` with `FontSizeProperty` indicates that the `f32` output from that curve is to be used to animate the font size of a `Text` component (as configured above).
### glTF Loading glTF animations are now loaded into `Curve` types of various kinds, depending on what is being animated and what interpolation mode is being used. Those types get wrapped into and converted into `Box` and shoved inside of a `VariableCurve` just like everybody else. ### Morph Weights There is an `IterableCurve` abstraction which allows sampling these from a contiguous buffer without allocating. Its only reason for existing is that Rust disallows you from naming function types, otherwise we would just use `Curve` with an iterator output type. (The iterator involves `Map`, and the name of the function type would have to be able to be named, but it is not.) A `WeightsCurve` adaptor turns an `IterableCurve` into an `AnimationCurve`, so it behaves like everything else in that regard. ## Testing Tested by running existing animation examples. Interpolation logic has had additional tests added within the `Curve` API to replace the tests in `bevy_animation`. Some kinds of out-of-bounds errors have become impossible. Performance testing on `many_foxes` (`animate_targets`) suggests that performance is very similar to the existing implementation. Here are a couple trace histograms across different runs (yellow is this branch, red is main). Screenshot 2024-09-27 at 9 41 50 AM Screenshot 2024-09-27 at 9 45 18 AM --- ## Migration Guide Most user code that does not directly deal with `AnimationClip` and `VariableCurve` will not need to be changed. On the other hand, `VariableCurve` has been completely overhauled. If you were previously defining animation curves in code using keyframes, you will need to migrate that code to use curve constructors instead. For example, a rotation animation defined using keyframes and added to an animation clip like this: ```rust animation_clip.add_curve_to_target( 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, }, ); ``` would now be added like this: ```rust animation_clip.add_curve_to_target( animation_target_id, AnimatableKeyframeCurve::new([0.0, 1.0, 2.0, 3.0, 4.0].into_iter().zip([ 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, ])) .map(RotationCurve) .expect("Failed to build rotation curve"), ); ``` Note that the interface of `AnimationClip::add_curve_to_target` has also changed (as this example shows, if subtly), and now takes its curve input as an `impl AnimationCurve`. If you need to add a `VariableCurve` directly, a new method `add_variable_curve_to_target` accommodates that (and serves as a one-to-one migration in this regard). ### For reviewers The diff is pretty big, and the structure of some of the changes might not be super-obvious: - `keyframes.rs` became `animation_curves.rs`, and `AnimationCurve` is based heavily on `Keyframes`, with the adaptors also largely following suite. - The Curve API adaptor structs were moved from `bevy_math::curve::mod` into their own module `adaptors`. There are no functional changes to how these adaptors work; this is just to make room for the specialized reflection implementations since `mod.rs` was getting kind of cramped. - The new module `gltf_curves` holds the additional curve constructions that are needed by the glTF loader. Note that the loader uses a mix of these and off-the-shelf `bevy_math` curve stuff. - `animatable.rs` no longer holds logic related to keyframe interpolation, which is now delegated to the existing abstractions in `bevy_math::curve::cores`. --------- Co-authored-by: Gino Valente <49806985+MrGVSV@users.noreply.github.com> Co-authored-by: aecsocket <43144841+aecsocket@users.noreply.github.com> --- crates/bevy_animation/src/animatable.rs | 86 +-- crates/bevy_animation/src/animation_curves.rs | 538 ++++++++++++++++ crates/bevy_animation/src/gltf_curves.rs | 422 +++++++++++++ crates/bevy_animation/src/keyframes.rs | 584 ------------------ crates/bevy_animation/src/lib.rs | 508 +++------------ crates/bevy_gltf/src/loader.rs | 178 +++++- crates/bevy_math/src/curve/adaptors.rs | 544 +++++++++++++++- crates/bevy_math/src/curve/cores.rs | 196 +++++- crates/bevy_math/src/curve/iterable.rs | 53 ++ crates/bevy_math/src/curve/mod.rs | 256 +------- examples/animation/animated_transform.rs | 100 +-- examples/animation/animated_ui.rs | 30 +- 12 files changed, 2023 insertions(+), 1472 deletions(-) create mode 100644 crates/bevy_animation/src/animation_curves.rs create mode 100644 crates/bevy_animation/src/gltf_curves.rs delete mode 100644 crates/bevy_animation/src/keyframes.rs create mode 100644 crates/bevy_math/src/curve/iterable.rs diff --git a/crates/bevy_animation/src/animatable.rs b/crates/bevy_animation/src/animatable.rs index 1a0cce53bfeec..298d0125a208c 100644 --- a/crates/bevy_animation/src/animatable.rs +++ b/crates/bevy_animation/src/animatable.rs @@ -1,6 +1,6 @@ //! Traits and type for interpolating between values. -use crate::{util, AnimationEvaluationError, Interpolation}; +use crate::util; use bevy_color::{Laba, LinearRgba, Oklaba, Srgba, Xyza}; use bevy_math::*; use bevy_reflect::Reflect; @@ -188,93 +188,11 @@ impl Animatable for Quat { } } -/// An abstraction over a list of keyframes. -/// -/// Using this abstraction instead of `Vec` enables more flexibility in how -/// keyframes are stored. In particular, morph weights use this trait in order -/// to flatten the keyframes for all morph weights into a single vector instead -/// of nesting vectors. -pub(crate) trait GetKeyframe { - /// The type of the property to be animated. - type Output; - /// Retrieves the value of the keyframe at the given index. - fn get_keyframe(&self, index: usize) -> Option<&Self::Output>; -} - -/// Interpolates between keyframes and stores the result in `dest`. -/// -/// This is factored out so that it can be shared between implementations of -/// [`crate::keyframes::Keyframes`]. -pub(crate) fn interpolate_keyframes( - dest: &mut T, - keyframes: &(impl GetKeyframe + ?Sized), - interpolation: Interpolation, - step_start: usize, - time: f32, - weight: f32, - duration: f32, -) -> Result<(), AnimationEvaluationError> -where - T: Animatable + Clone, -{ - let value = match interpolation { - Interpolation::Step => { - let Some(start_keyframe) = keyframes.get_keyframe(step_start) else { - return Err(AnimationEvaluationError::KeyframeNotPresent(step_start)); - }; - (*start_keyframe).clone() - } - - Interpolation::Linear => { - let (Some(start_keyframe), Some(end_keyframe)) = ( - keyframes.get_keyframe(step_start), - keyframes.get_keyframe(step_start + 1), - ) else { - return Err(AnimationEvaluationError::KeyframeNotPresent(step_start + 1)); - }; - - T::interpolate(start_keyframe, end_keyframe, time) - } - - Interpolation::CubicSpline => { - let ( - Some(start_keyframe), - Some(start_tangent_keyframe), - Some(end_tangent_keyframe), - Some(end_keyframe), - ) = ( - keyframes.get_keyframe(step_start * 3 + 1), - keyframes.get_keyframe(step_start * 3 + 2), - keyframes.get_keyframe(step_start * 3 + 3), - keyframes.get_keyframe(step_start * 3 + 4), - ) - else { - return Err(AnimationEvaluationError::KeyframeNotPresent( - step_start * 3 + 4, - )); - }; - - interpolate_with_cubic_bezier( - start_keyframe, - start_tangent_keyframe, - end_tangent_keyframe, - end_keyframe, - time, - duration, - ) - } - }; - - *dest = T::interpolate(dest, &value, weight); - - Ok(()) -} - /// Evaluates a cubic Bézier curve at a value `t`, given two endpoints and the /// derivatives at those endpoints. /// /// The derivatives are linearly scaled by `duration`. -fn interpolate_with_cubic_bezier(p0: &T, d0: &T, d3: &T, p3: &T, t: f32, duration: f32) -> T +pub fn interpolate_with_cubic_bezier(p0: &T, d0: &T, d3: &T, p3: &T, t: f32, duration: f32) -> T where T: Animatable + Clone, { diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs new file mode 100644 index 0000000000000..746b6f8b9dd1e --- /dev/null +++ b/crates/bevy_animation/src/animation_curves.rs @@ -0,0 +1,538 @@ +//! The [`AnimationCurve`] trait and adaptors that allow curves to implement it. +//! +//! # Overview +//! +//! The flow of curves into the animation system generally begins with something that +//! implements the [`Curve`] trait. Let's imagine, for example, that we have some +//! `Curve` that we want to use to animate something. That could be defined in +//! a number of different ways, but let's imagine that we've defined it [using a function]: +//! +//! # use bevy_math::curve::{Curve, Interval, function_curve}; +//! # use bevy_math::vec3; +//! let wobble_curve = function_curve( +//! Interval::UNIT, +//! |t| { vec3(t.cos(), 0.0, 0.0) }, +//! ); +//! +//! Okay, so we have a curve, but the animation system also needs to know, in some way, +//! how the values from this curve should actually be used. That is, it needs to know what +//! to animate! That's what [`AnimationCurve`] is for. In particular, what we need to do +//! is take our curve and turn it into an `AnimationCurve` which will be usable by the +//! animation system. +//! +//! For instance, let's imagine that we want to use the `Vec3` output +//! from our curve to animate the [translation component of a `Transform`]. For this, there is +//! the adaptor [`TranslationCurve`], which wraps any `Curve` and turns it into an +//! [`AnimationCurve`] that will use the given curve to animate the entity's translation: +//! +//! # use bevy_math::curve::{Curve, Interval, function_curve}; +//! # use bevy_math::vec3; +//! # use bevy_animation::animation_curves::*; +//! # let wobble_curve = function_curve( +//! # Interval::UNIT, +//! # |t| vec3(t.cos(), 0.0, 0.0) +//! # ); +//! let wobble_animation = TranslationCurve(wobble_curve); +//! +//! And finally, this `AnimationCurve` needs to be added to an [`AnimationClip`] in order to +//! actually animate something. This is what that looks like: +//! +//! # use bevy_math::curve::{Curve, Interval, function_curve}; +//! # use bevy_animation::{AnimationClip, AnimationTargetId, animation_curves::*}; +//! # use bevy_core::Name; +//! # use bevy_math::vec3; +//! # let wobble_curve = function_curve( +//! # Interval::UNIT, +//! # |t| { vec3(t.cos(), 0.0, 0.0) }, +//! # ); +//! # let wobble_animation = TranslationCurve(wobble_curve); +//! # let animation_target_id = AnimationTargetId::from(&Name::new("Test")); +//! let mut animation_clip = AnimationClip::default(); +//! animation_clip.add_curve_to_target( +//! animation_target_id, +//! wobble_animation, +//! ); +//! +//! # Making animation curves +//! +//! The overview showed one example, but in general there are a few different ways of going from +//! a [`Curve`], which produces time-related data of some kind, to an [`AnimationCurve`], which +//! knows how to apply that data to an entity. +//! +//! ## `Transform` +//! +//! [`Transform`] is special and has its own adaptors: +//! - [`TranslationCurve`], which uses `Vec3` output to animate [`Transform::translation`] +//! - [`RotationCurve`], which uses `Quat` output to animate [`Transform::rotation`] +//! - [`ScaleCurve`], which uses `Vec3` output to animate [`Transform::scale`] +//! - [`TransformCurve`], which uses `Transform` output to animate the entire `Transform` +//! +//! ## Animatable properties +//! +//! Animation of arbitrary components can be accomplished using [`AnimatableProperty`] in +//! conjunction with [`AnimatableCurve`]. See the documentation [there] for details. +//! +//! [using a function]: bevy_math::curve::function_curve +//! [translation component of a `Transform`]: bevy_transform::prelude::Transform::translation +//! [`AnimationClip`]: crate::AnimationClip +//! [there]: AnimatableProperty + +use core::{ + any::TypeId, + fmt::{self, Debug, Formatter}, + marker::PhantomData, +}; + +use bevy_ecs::{component::Component, world::Mut}; +use bevy_math::{ + curve::{ + cores::{UnevenCore, UnevenCoreError}, + iterable::IterableCurve, + Curve, Interval, + }, + FloatExt, Quat, Vec3, +}; +use bevy_reflect::{FromReflect, Reflect, Reflectable, TypePath}; +use bevy_render::mesh::morph::MorphWeights; +use bevy_transform::prelude::Transform; + +use crate::{prelude::Animatable, AnimationEntityMut, AnimationEvaluationError}; + +/// A value on a component that Bevy can animate. +/// +/// You can implement this trait on a unit struct in order to support animating +/// custom components other than transforms and morph weights. Use that type in +/// conjunction with [`AnimatableCurve`] (and perhaps [`AnimatableKeyframeCurve`] +/// to define the animation itself). For example, in order to animate font size of a +/// text section from 24 pt. to 80 pt., you might use: +/// +/// # use bevy_animation::prelude::AnimatableProperty; +/// # use bevy_reflect::Reflect; +/// # use bevy_text::Text; +/// #[derive(Reflect)] +/// struct FontSizeProperty; +/// +/// impl AnimatableProperty for FontSizeProperty { +/// type Component = Text; +/// type Property = f32; +/// fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property> { +/// Some(&mut component.sections.get_mut(0)?.style.font_size) +/// } +/// } +/// +/// You can then create an [`AnimationClip`] to animate this property like so: +/// +/// # use bevy_animation::{AnimationClip, AnimationTargetId, VariableCurve}; +/// # use bevy_animation::prelude::{AnimatableProperty, AnimatableKeyframeCurve, AnimatableCurve}; +/// # use bevy_core::Name; +/// # use bevy_reflect::Reflect; +/// # use bevy_text::Text; +/// # let animation_target_id = AnimationTargetId::from(&Name::new("Test")); +/// # #[derive(Reflect)] +/// # struct FontSizeProperty; +/// # impl AnimatableProperty for FontSizeProperty { +/// # type Component = Text; +/// # type Property = f32; +/// # fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property> { +/// # Some(&mut component.sections.get_mut(0)?.style.font_size) +/// # } +/// # } +/// let mut animation_clip = AnimationClip::default(); +/// animation_clip.add_curve_to_target( +/// animation_target_id, +/// AnimatableKeyframeCurve::new( +/// [ +/// (0.0, 24.0), +/// (1.0, 80.0), +/// ] +/// ) +/// .map(AnimatableCurve::::from_curve) +/// .expect("Failed to create font size curve") +/// ); +/// +/// Here, the use of [`AnimatableKeyframeCurve`] creates a curve out of the given keyframe time-value +/// pairs, using the [`Animatable`] implementation of `f32` to interpolate between them. The +/// invocation of [`AnimatableCurve::from_curve`] with `FontSizeProperty` indicates that the `f32` +/// output from that curve is to be used to animate the font size of a `Text` component (as +/// configured above). +/// +/// [`AnimationClip`]: crate::AnimationClip +pub trait AnimatableProperty: Reflect + TypePath { + /// The type of the component that the property lives on. + type Component: Component; + + /// The type of the property to be animated. + type Property: Animatable + FromReflect + Reflectable + Clone + Sync + Debug; + + /// Given a reference to the component, returns a reference to the property. + /// + /// If the property couldn't be found, returns `None`. + fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property>; +} + +/// This trait collects the additional requirements on top of [`Curve`] needed for a +/// curve to be used as an [`AnimationCurve`]. +pub trait AnimationCompatibleCurve: Curve + Debug + Clone + Reflectable {} + +impl AnimationCompatibleCurve for C where C: Curve + Debug + Clone + Reflectable {} + +/// This type allows the conversion of a [curve] valued in the [property type] of an +/// [`AnimatableProperty`] into an [`AnimationCurve`] which animates that property. +/// +/// [curve]: Curve +/// [property type]: AnimatableProperty::Property +#[derive(Reflect, FromReflect)] +#[reflect(from_reflect = false)] +pub struct AnimatableCurve { + curve: C, + #[reflect(ignore)] + _phantom: PhantomData

, +} + +impl AnimatableCurve +where + P: AnimatableProperty, + C: AnimationCompatibleCurve, +{ + /// Create an [`AnimatableCurve`] (and thus an [`AnimationCurve`]) from a curve + /// valued in an [animatable property]. + /// + /// [animatable property]: AnimatableProperty::Property + pub fn from_curve(curve: C) -> Self { + Self { + curve, + _phantom: PhantomData, + } + } +} + +impl Clone for AnimatableCurve +where + C: Clone, +{ + fn clone(&self) -> Self { + Self { + curve: self.curve.clone(), + _phantom: PhantomData, + } + } +} + +impl Debug for AnimatableCurve +where + C: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("AnimatableCurve") + .field("curve", &self.curve) + .finish() + } +} + +impl AnimationCurve for AnimatableCurve +where + P: AnimatableProperty, + C: AnimationCompatibleCurve, +{ + fn clone_value(&self) -> Box { + Box::new(self.clone()) + } + + fn domain(&self) -> Interval { + self.curve.domain() + } + + fn apply<'a>( + &self, + t: f32, + _transform: Option>, + mut entity: AnimationEntityMut<'a>, + weight: f32, + ) -> Result<(), AnimationEvaluationError> { + let mut component = entity.get_mut::().ok_or_else(|| { + AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) + })?; + let property = P::get_mut(&mut component) + .ok_or_else(|| AnimationEvaluationError::PropertyNotPresent(TypeId::of::

()))?; + let value = self.curve.sample_clamped(t); + *property = ::interpolate(property, &value, weight); + Ok(()) + } +} + +/// This type allows a [curve] valued in `Vec3` to become an [`AnimationCurve`] that animates +/// the translation component of a transform. +/// +/// [curve]: Curve +#[derive(Debug, Clone, Reflect, FromReflect)] +#[reflect(from_reflect = false)] +pub struct TranslationCurve(pub C); + +impl AnimationCurve for TranslationCurve +where + C: AnimationCompatibleCurve, +{ + fn clone_value(&self) -> Box { + Box::new(self.clone()) + } + + fn domain(&self) -> Interval { + self.0.domain() + } + + fn apply<'a>( + &self, + t: f32, + transform: Option>, + _entity: AnimationEntityMut<'a>, + weight: f32, + ) -> Result<(), AnimationEvaluationError> { + let mut component = transform.ok_or_else(|| { + AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) + })?; + let new_value = self.0.sample_clamped(t); + component.translation = + ::interpolate(&component.translation, &new_value, weight); + Ok(()) + } +} + +/// This type allows a [curve] valued in `Quat` to become an [`AnimationCurve`] that animates +/// the rotation component of a transform. +/// +/// [curve]: Curve +#[derive(Debug, Clone, Reflect, FromReflect)] +#[reflect(from_reflect = false)] +pub struct RotationCurve(pub C); + +impl AnimationCurve for RotationCurve +where + C: AnimationCompatibleCurve, +{ + fn clone_value(&self) -> Box { + Box::new(self.clone()) + } + + fn domain(&self) -> Interval { + self.0.domain() + } + + fn apply<'a>( + &self, + t: f32, + transform: Option>, + _entity: AnimationEntityMut<'a>, + weight: f32, + ) -> Result<(), AnimationEvaluationError> { + let mut component = transform.ok_or_else(|| { + AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) + })?; + let new_value = self.0.sample_clamped(t); + component.rotation = + ::interpolate(&component.rotation, &new_value, weight); + Ok(()) + } +} + +/// This type allows a [curve] valued in `Vec3` to become an [`AnimationCurve`] that animates +/// the scale component of a transform. +/// +/// [curve]: Curve +#[derive(Debug, Clone, Reflect, FromReflect)] +#[reflect(from_reflect = false)] +pub struct ScaleCurve(pub C); + +impl AnimationCurve for ScaleCurve +where + C: AnimationCompatibleCurve, +{ + fn clone_value(&self) -> Box { + Box::new(self.clone()) + } + + fn domain(&self) -> Interval { + self.0.domain() + } + + fn apply<'a>( + &self, + t: f32, + transform: Option>, + _entity: AnimationEntityMut<'a>, + weight: f32, + ) -> Result<(), AnimationEvaluationError> { + let mut component = transform.ok_or_else(|| { + AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) + })?; + let new_value = self.0.sample_clamped(t); + component.scale = ::interpolate(&component.scale, &new_value, weight); + Ok(()) + } +} + +/// This type allows a [curve] valued in `Transform` to become an [`AnimationCurve`] that animates +/// a transform. +/// +/// This exists primarily as a convenience to animate entities using the entire transform at once +/// instead of splitting it into pieces and animating each part (translation, rotation, scale). +/// +/// [curve]: Curve +#[derive(Debug, Clone, Reflect, FromReflect)] +#[reflect(from_reflect = false)] +pub struct TransformCurve(pub C); + +impl AnimationCurve for TransformCurve +where + C: AnimationCompatibleCurve, +{ + fn clone_value(&self) -> Box { + Box::new(self.clone()) + } + + fn domain(&self) -> Interval { + self.0.domain() + } + + fn apply<'a>( + &self, + t: f32, + transform: Option>, + _entity: AnimationEntityMut<'a>, + weight: f32, + ) -> Result<(), AnimationEvaluationError> { + let mut component = transform.ok_or_else(|| { + AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) + })?; + let new_value = self.0.sample_clamped(t); + *component = ::interpolate(&component, &new_value, weight); + Ok(()) + } +} + +/// This type allows an [`IterableCurve`] valued in `f32` to be used as an [`AnimationCurve`] +/// that animates [morph weights]. +/// +/// [morph weights]: MorphWeights +#[derive(Debug, Clone, Reflect, FromReflect)] +#[reflect(from_reflect = false)] +pub struct WeightsCurve(pub C); + +impl AnimationCurve for WeightsCurve +where + C: IterableCurve + Debug + Clone + Reflectable, +{ + fn clone_value(&self) -> Box { + Box::new(self.clone()) + } + + fn domain(&self) -> Interval { + self.0.domain() + } + + fn apply<'a>( + &self, + t: f32, + _transform: Option>, + mut entity: AnimationEntityMut<'a>, + weight: f32, + ) -> Result<(), AnimationEvaluationError> { + let mut dest = entity.get_mut::().ok_or_else(|| { + AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) + })?; + lerp_morph_weights(dest.weights_mut(), self.0.sample_iter_clamped(t), weight); + Ok(()) + } +} + +/// Update `morph_weights` based on weights in `incoming_weights` with a linear interpolation +/// on `lerp_weight`. +fn lerp_morph_weights( + morph_weights: &mut [f32], + incoming_weights: impl Iterator, + lerp_weight: f32, +) { + let zipped = morph_weights.iter_mut().zip(incoming_weights); + for (morph_weight, incoming_weights) in zipped { + *morph_weight = morph_weight.lerp(incoming_weights, lerp_weight); + } +} + +/// A low-level trait that provides control over how curves are actually applied to entities +/// by the animation system. +/// +/// Typically, this will not need to be implemented manually, since it is automatically +/// implemented by [`AnimatableCurve`] and other curves used by the animation system +/// (e.g. those that animate parts of transforms or morph weights). However, this can be +/// implemented manually when `AnimatableCurve` is not sufficiently expressive. +/// +/// In many respects, this behaves like a type-erased form of [`Curve`], where the output +/// type of the curve is remembered only in the components that are mutated in the +/// implementation of [`apply`]. +/// +/// [`apply`]: AnimationCurve::apply +pub trait AnimationCurve: Reflect + Debug + Send + Sync { + /// Returns a boxed clone of this value. + fn clone_value(&self) -> Box; + + /// The range of times for which this animation is defined. + fn domain(&self) -> Interval; + + /// Write the value of sampling this curve at time `t` into `transform` or `entity`, + /// as appropriate, interpolating between the existing value and the sampled value + /// using the given `weight`. + fn apply<'a>( + &self, + t: f32, + transform: Option>, + entity: AnimationEntityMut<'a>, + weight: f32, + ) -> Result<(), AnimationEvaluationError>; +} + +/// A [curve] defined by keyframes with values in an [animatable] type. +/// +/// The keyframes are interpolated using the type's [`Animatable::interpolate`] implementation. +/// +/// [curve]: Curve +/// [animatable]: Animatable +#[derive(Debug, Clone, Reflect)] +pub struct AnimatableKeyframeCurve { + core: UnevenCore, +} + +impl Curve for AnimatableKeyframeCurve +where + T: Animatable + Clone, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + self.core.sample_with(t, ::interpolate) + } + + #[inline] + fn sample_clamped(&self, t: f32) -> T { + // Sampling by keyframes is automatically clamped to the keyframe bounds. + self.sample_unchecked(t) + } +} + +impl AnimatableKeyframeCurve +where + T: Animatable, +{ + /// Create a new [`AnimatableKeyframeCurve`] from the given `keyframes`. The values of this + /// curve are interpolated from the keyframes using the output type's implementation of + /// [`Animatable::interpolate`]. + /// + /// There must be at least two samples in order for this method to succeed. + pub fn new(keyframes: impl IntoIterator) -> Result { + Ok(Self { + core: UnevenCore::new(keyframes)?, + }) + } +} diff --git a/crates/bevy_animation/src/gltf_curves.rs b/crates/bevy_animation/src/gltf_curves.rs new file mode 100644 index 0000000000000..f32ddc4ab0176 --- /dev/null +++ b/crates/bevy_animation/src/gltf_curves.rs @@ -0,0 +1,422 @@ +//! Concrete curve structures used to load glTF curves into the animation system. + +use bevy_math::{ + curve::{cores::*, iterable::IterableCurve, *}, + vec4, Quat, 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)?, + }) + } +} + +// NOTE: We can probably delete `CubicRotationCurve` once we improve the `Reflect` implementations +// for the `Curve` API adaptors; this is basically a `CubicKeyframeCurve` composed with `map`. + +/// A keyframe-defined curve that uses cubic spline interpolation, special-cased for quaternions +/// since it uses `Vec4` internally. +#[derive(Debug, Clone, Reflect)] +pub struct CubicRotationCurve { + // Note: The sample width here should be 3. + core: ChunkedUnevenCore, +} + +impl Curve for CubicRotationCurve { + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> Quat { + let vec = 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) + } + }; + Quat::from_vec4(vec.normalize()) + } +} + +impl CubicRotationCurve { + /// Create a new [`CubicRotationCurve`] 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]` + /// + /// To sample quaternions from this curve, the resulting interpolated `Vec4` output is normalized + /// and interpreted as a quaternion. + pub fn new( + times: impl IntoIterator, + values: impl IntoIterator, + ) -> Result { + Ok(Self { + core: ChunkedUnevenCore::new(times, values, 3)?, + }) + } +} + +/// 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(&self, t: f32) -> impl Iterator { + 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(&self, t: f32) -> impl Iterator { + 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() + } + + fn sample_iter_unchecked(&self, t: f32) -> impl Iterator { + 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("number of values ({values_given}) is not 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), +} + +//---------// +// 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(), + } + } +} + +/// 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, +{ + let coeffs = (vec4(2.0, 1.0, -2.0, 1.0) * lerp + vec4(-3.0, -2.0, 3.0, -1.0)) * lerp; + value_start * (coeffs.x * lerp + 1.0) + + tangent_out_start * step_duration * lerp * (coeffs.y + 1.0) + + value_end * lerp * coeffs.z + + tangent_in_end * step_duration * lerp * coeffs.w +} + +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/keyframes.rs b/crates/bevy_animation/src/keyframes.rs deleted file mode 100644 index b85feb5af1e61..0000000000000 --- a/crates/bevy_animation/src/keyframes.rs +++ /dev/null @@ -1,584 +0,0 @@ -//! Keyframes of animation clips. - -use core::{ - any::TypeId, - fmt::{self, Debug, Formatter}, -}; - -use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::{component::Component, world::Mut}; -use bevy_math::{Quat, Vec3}; -use bevy_reflect::{FromReflect, Reflect, Reflectable, TypePath}; -use bevy_render::mesh::morph::MorphWeights; -use bevy_transform::prelude::Transform; - -use crate::{ - animatable, - prelude::{Animatable, GetKeyframe}, - AnimationEntityMut, AnimationEvaluationError, Interpolation, -}; - -/// A value on a component that Bevy can animate. -/// -/// You can implement this trait on a unit struct in order to support animating -/// custom components other than transforms and morph weights. Use that type in -/// conjunction with [`AnimatablePropertyKeyframes`]. For example, in order to -/// animate font size of a text section from 24 pt. to 80 pt., you might use: -/// -/// # use bevy_animation::prelude::AnimatableProperty; -/// # use bevy_reflect::Reflect; -/// # use bevy_text::Text; -/// #[derive(Reflect)] -/// struct FontSizeProperty; -/// -/// impl AnimatableProperty for FontSizeProperty { -/// type Component = Text; -/// type Property = f32; -/// fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property> { -/// Some(&mut component.sections.get_mut(0)?.style.font_size) -/// } -/// } -/// -/// You can then create a [`crate::AnimationClip`] to animate this property like so: -/// -/// # use bevy_animation::{AnimationClip, AnimationTargetId, Interpolation, VariableCurve}; -/// # use bevy_animation::prelude::{AnimatableProperty, AnimatablePropertyKeyframes}; -/// # use bevy_core::Name; -/// # use bevy_reflect::Reflect; -/// # use bevy_text::Text; -/// # let animation_target_id = AnimationTargetId::from(&Name::new("Test")); -/// # #[derive(Reflect)] -/// # struct FontSizeProperty; -/// # impl AnimatableProperty for FontSizeProperty { -/// # type Component = Text; -/// # type Property = f32; -/// # fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property> { -/// # Some(&mut component.sections.get_mut(0)?.style.font_size) -/// # } -/// # } -/// let mut animation_clip = AnimationClip::default(); -/// animation_clip.add_curve_to_target( -/// animation_target_id, -/// VariableCurve::linear::>( -/// [0.0, 1.0], -/// [24.0, 80.0], -/// ), -/// ); -pub trait AnimatableProperty: Reflect + TypePath + 'static { - /// The type of the component that the property lives on. - type Component: Component; - - /// The type of the property to be animated. - type Property: Animatable + FromReflect + Reflectable + Clone + Sync + Debug + 'static; - - /// Given a reference to the component, returns a reference to the property. - /// - /// If the property couldn't be found, returns `None`. - fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property>; -} - -/// Keyframes in a [`crate::VariableCurve`] that animate an -/// [`AnimatableProperty`]. -/// -/// This is the a generic type of [`Keyframes`] that can animate any -/// [`AnimatableProperty`]. See the documentation for [`AnimatableProperty`] for -/// more information as to how to use this type. -/// -/// If you're animating scale, rotation, or translation of a [`Transform`], -/// [`ScaleKeyframes`], [`RotationKeyframes`], and [`TranslationKeyframes`] are -/// faster, and simpler, alternatives to this type. -#[derive(Reflect, Deref, DerefMut)] -pub struct AnimatablePropertyKeyframes

(pub Vec) -where - P: AnimatableProperty; - -impl

Clone for AnimatablePropertyKeyframes

-where - P: AnimatableProperty, -{ - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} - -impl

Debug for AnimatablePropertyKeyframes

-where - P: AnimatableProperty, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.debug_tuple("AnimatablePropertyKeyframes") - .field(&self.0) - .finish() - } -} - -/// A low-level trait for use in [`crate::VariableCurve`] that provides fine -/// control over how animations are evaluated. -/// -/// You can implement this trait when the generic -/// [`AnimatablePropertyKeyframes`] isn't sufficiently-expressive for your -/// needs. For example, [`MorphWeights`] implements this trait instead of using -/// [`AnimatablePropertyKeyframes`] because it needs to animate arbitrarily many -/// weights at once, which can't be done with [`Animatable`] as that works on -/// fixed-size values only. -pub trait Keyframes: Reflect + Debug + Send + Sync { - /// Returns a boxed clone of this value. - fn clone_value(&self) -> Box; - - /// Interpolates between the existing value and the value of the first - /// keyframe, and writes the value into `transform` and/or `entity` as - /// appropriate. - /// - /// Arguments: - /// - /// * `transform`: The transform of the entity, if present. - /// - /// * `entity`: Allows access to the rest of the components of the entity. - /// - /// * `weight`: The blend weight between the existing component value (0.0) - /// and the one computed from the keyframes (1.0). - fn apply_single_keyframe<'a>( - &self, - transform: Option>, - entity: AnimationEntityMut<'a>, - weight: f32, - ) -> Result<(), AnimationEvaluationError>; - - /// Interpolates between the existing value and the value of the two nearest - /// keyframes, and writes the value into `transform` and/or `entity` as - /// appropriate. - /// - /// Arguments: - /// - /// * `transform`: The transform of the entity, if present. - /// - /// * `entity`: Allows access to the rest of the components of the entity. - /// - /// * `interpolation`: The type of interpolation to use. - /// - /// * `step_start`: The index of the first keyframe. - /// - /// * `time`: The blend weight between the first keyframe (0.0) and the next - /// keyframe (1.0). - /// - /// * `weight`: The blend weight between the existing component value (0.0) - /// and the one computed from the keyframes (1.0). - /// - /// If `interpolation` is `Interpolation::Linear`, then pseudocode for this - /// function could be `property = lerp(property, lerp(keyframes[step_start], - /// keyframes[step_start + 1], time), weight)`. - #[allow(clippy::too_many_arguments)] - fn apply_tweened_keyframes<'a>( - &self, - transform: Option>, - entity: AnimationEntityMut<'a>, - interpolation: Interpolation, - step_start: usize, - time: f32, - weight: f32, - duration: f32, - ) -> Result<(), AnimationEvaluationError>; -} - -/// Keyframes for animating [`Transform::translation`]. -/// -/// An example of a [`crate::AnimationClip`] that animates translation: -/// -/// # use bevy_animation::{AnimationClip, AnimationTargetId, Interpolation}; -/// # use bevy_animation::{VariableCurve, prelude::TranslationKeyframes}; -/// # use bevy_core::Name; -/// # use bevy_math::Vec3; -/// # let animation_target_id = AnimationTargetId::from(&Name::new("Test")); -/// let mut animation_clip = AnimationClip::default(); -/// animation_clip.add_curve_to_target( -/// animation_target_id, -/// VariableCurve::linear::( -/// [0.0, 1.0], -/// [Vec3::ZERO, Vec3::ONE], -/// ), -/// ); -#[derive(Clone, Reflect, Debug, Deref, DerefMut)] -pub struct TranslationKeyframes(pub Vec); - -/// Keyframes for animating [`Transform::scale`]. -/// -/// An example of a [`crate::AnimationClip`] that animates translation: -/// -/// # use bevy_animation::{AnimationClip, AnimationTargetId, Interpolation}; -/// # use bevy_animation::{VariableCurve, prelude::ScaleKeyframes}; -/// # use bevy_core::Name; -/// # use bevy_math::Vec3; -/// # let animation_target_id = AnimationTargetId::from(&Name::new("Test")); -/// let mut animation_clip = AnimationClip::default(); -/// animation_clip.add_curve_to_target( -/// animation_target_id, -/// VariableCurve::linear::( -/// [0.0, 1.0], -/// [Vec3::ONE, Vec3::splat(2.0)], -/// ), -/// ); -#[derive(Clone, Reflect, Debug, Deref, DerefMut)] -pub struct ScaleKeyframes(pub Vec); - -/// Keyframes for animating [`Transform::rotation`]. -/// -/// An example of a [`crate::AnimationClip`] that animates translation: -/// -/// # use bevy_animation::{AnimationClip, AnimationTargetId, Interpolation}; -/// # use bevy_animation::{VariableCurve, prelude::RotationKeyframes}; -/// # use bevy_core::Name; -/// # use bevy_math::Quat; -/// # use std::f32::consts::FRAC_PI_2; -/// # let animation_target_id = AnimationTargetId::from(&Name::new("Test")); -/// let mut animation_clip = AnimationClip::default(); -/// animation_clip.add_curve_to_target( -/// animation_target_id, -/// VariableCurve::linear::( -/// [0.0, 1.0], -/// [Quat::from_rotation_x(FRAC_PI_2), Quat::from_rotation_y(FRAC_PI_2)], -/// ), -/// ); -#[derive(Clone, Reflect, Debug, Deref, DerefMut)] -pub struct RotationKeyframes(pub Vec); - -/// Keyframes for animating [`MorphWeights`]. -#[derive(Clone, Debug, Reflect)] -pub struct MorphWeightsKeyframes { - /// The total number of morph weights. - pub morph_target_count: usize, - - /// The morph weights. - /// - /// The length of this vector should be the total number of morph weights - /// times the number of keyframes. - pub weights: Vec, -} - -impl From for TranslationKeyframes -where - T: Into>, -{ - fn from(value: T) -> Self { - Self(value.into()) - } -} - -impl Keyframes for TranslationKeyframes { - fn clone_value(&self) -> Box { - Box::new(self.clone()) - } - - fn apply_single_keyframe<'a>( - &self, - transform: Option>, - _: AnimationEntityMut<'a>, - weight: f32, - ) -> Result<(), AnimationEvaluationError> { - let mut component = transform.ok_or_else(|| { - AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) - })?; - let value = self - .first() - .ok_or(AnimationEvaluationError::KeyframeNotPresent(0))?; - component.translation = Animatable::interpolate(&component.translation, value, weight); - Ok(()) - } - - fn apply_tweened_keyframes<'a>( - &self, - transform: Option>, - _: AnimationEntityMut<'a>, - interpolation: Interpolation, - step_start: usize, - time: f32, - weight: f32, - duration: f32, - ) -> Result<(), AnimationEvaluationError> { - let mut component = transform.ok_or_else(|| { - AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) - })?; - animatable::interpolate_keyframes( - &mut component.translation, - &(*self)[..], - interpolation, - step_start, - time, - weight, - duration, - ) - } -} - -impl From for ScaleKeyframes -where - T: Into>, -{ - fn from(value: T) -> Self { - Self(value.into()) - } -} - -impl Keyframes for ScaleKeyframes { - fn clone_value(&self) -> Box { - Box::new(self.clone()) - } - - fn apply_single_keyframe<'a>( - &self, - transform: Option>, - _: AnimationEntityMut<'a>, - weight: f32, - ) -> Result<(), AnimationEvaluationError> { - let mut component = transform.ok_or_else(|| { - AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) - })?; - let value = self - .first() - .ok_or(AnimationEvaluationError::KeyframeNotPresent(0))?; - component.scale = Animatable::interpolate(&component.scale, value, weight); - Ok(()) - } - - fn apply_tweened_keyframes<'a>( - &self, - transform: Option>, - _: AnimationEntityMut<'a>, - interpolation: Interpolation, - step_start: usize, - time: f32, - weight: f32, - duration: f32, - ) -> Result<(), AnimationEvaluationError> { - let mut component = transform.ok_or_else(|| { - AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) - })?; - animatable::interpolate_keyframes( - &mut component.scale, - &(*self)[..], - interpolation, - step_start, - time, - weight, - duration, - ) - } -} - -impl From for RotationKeyframes -where - T: Into>, -{ - fn from(value: T) -> Self { - Self(value.into()) - } -} - -impl Keyframes for RotationKeyframes { - fn clone_value(&self) -> Box { - Box::new(self.clone()) - } - - fn apply_single_keyframe<'a>( - &self, - transform: Option>, - _: AnimationEntityMut<'a>, - weight: f32, - ) -> Result<(), AnimationEvaluationError> { - let mut component = transform.ok_or_else(|| { - AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) - })?; - let value = self - .first() - .ok_or(AnimationEvaluationError::KeyframeNotPresent(0))?; - component.rotation = Animatable::interpolate(&component.rotation, value, weight); - Ok(()) - } - - fn apply_tweened_keyframes<'a>( - &self, - transform: Option>, - _: AnimationEntityMut<'a>, - interpolation: Interpolation, - step_start: usize, - time: f32, - weight: f32, - duration: f32, - ) -> Result<(), AnimationEvaluationError> { - let mut component = transform.ok_or_else(|| { - AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) - })?; - animatable::interpolate_keyframes( - &mut component.rotation, - &(*self)[..], - interpolation, - step_start, - time, - weight, - duration, - ) - } -} - -impl From for AnimatablePropertyKeyframes

-where - P: AnimatableProperty, - T: Into>, -{ - fn from(value: T) -> Self { - Self(value.into()) - } -} - -impl

Keyframes for AnimatablePropertyKeyframes

-where - P: AnimatableProperty, -{ - fn clone_value(&self) -> Box { - Box::new((*self).clone()) - } - - fn apply_single_keyframe<'a>( - &self, - _: Option>, - mut entity: AnimationEntityMut<'a>, - weight: f32, - ) -> Result<(), AnimationEvaluationError> { - let mut component = entity.get_mut::().ok_or_else(|| { - AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) - })?; - let property = P::get_mut(&mut component) - .ok_or_else(|| AnimationEvaluationError::PropertyNotPresent(TypeId::of::

()))?; - let value = self - .first() - .ok_or(AnimationEvaluationError::KeyframeNotPresent(0))?; - ::interpolate(property, value, weight); - Ok(()) - } - - fn apply_tweened_keyframes<'a>( - &self, - _: Option>, - mut entity: AnimationEntityMut<'a>, - interpolation: Interpolation, - step_start: usize, - time: f32, - weight: f32, - duration: f32, - ) -> Result<(), AnimationEvaluationError> { - let mut component = entity.get_mut::().ok_or_else(|| { - AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) - })?; - let property = P::get_mut(&mut component) - .ok_or_else(|| AnimationEvaluationError::PropertyNotPresent(TypeId::of::

()))?; - animatable::interpolate_keyframes( - property, - self, - interpolation, - step_start, - time, - weight, - duration, - )?; - Ok(()) - } -} - -impl GetKeyframe for [A] -where - A: Animatable, -{ - type Output = A; - - fn get_keyframe(&self, index: usize) -> Option<&Self::Output> { - self.get(index) - } -} - -impl

GetKeyframe for AnimatablePropertyKeyframes

-where - P: AnimatableProperty, -{ - type Output = P::Property; - - fn get_keyframe(&self, index: usize) -> Option<&Self::Output> { - self.get(index) - } -} - -/// Information needed to look up morph weight values in the flattened morph -/// weight keyframes vector. -struct GetMorphWeightKeyframe<'k> { - /// The morph weights keyframe structure that we're animating. - keyframes: &'k MorphWeightsKeyframes, - /// The index of the morph target in that structure. - morph_target_index: usize, -} - -impl Keyframes for MorphWeightsKeyframes { - fn clone_value(&self) -> Box { - Box::new(self.clone()) - } - - fn apply_single_keyframe<'a>( - &self, - _: Option>, - mut entity: AnimationEntityMut<'a>, - weight: f32, - ) -> Result<(), AnimationEvaluationError> { - let mut dest = entity.get_mut::().ok_or_else(|| { - AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) - })?; - - // TODO: Go 4 weights at a time to make better use of SIMD. - for (morph_target_index, morph_weight) in dest.weights_mut().iter_mut().enumerate() { - *morph_weight = - f32::interpolate(morph_weight, &self.weights[morph_target_index], weight); - } - - Ok(()) - } - - fn apply_tweened_keyframes<'a>( - &self, - _: Option>, - mut entity: AnimationEntityMut<'a>, - interpolation: Interpolation, - step_start: usize, - time: f32, - weight: f32, - duration: f32, - ) -> Result<(), AnimationEvaluationError> { - let mut dest = entity.get_mut::().ok_or_else(|| { - AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) - })?; - - // TODO: Go 4 weights at a time to make better use of SIMD. - for (morph_target_index, morph_weight) in dest.weights_mut().iter_mut().enumerate() { - animatable::interpolate_keyframes( - morph_weight, - &GetMorphWeightKeyframe { - keyframes: self, - morph_target_index, - }, - interpolation, - step_start, - time, - weight, - duration, - )?; - } - - Ok(()) - } -} - -impl GetKeyframe for GetMorphWeightKeyframe<'_> { - type Output = f32; - - fn get_keyframe(&self, keyframe_index: usize) -> Option<&Self::Output> { - self.keyframes - .weights - .as_slice() - .get(keyframe_index * self.keyframes.morph_target_count + self.morph_target_index) - } -} diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index ec3c69461e51e..c9d821b3d57bb 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -10,8 +10,9 @@ extern crate alloc; pub mod animatable; +pub mod animation_curves; +pub mod gltf_curves; pub mod graph; -pub mod keyframes; pub mod transition; mod util; @@ -33,12 +34,11 @@ use bevy_ecs::{ reflect::{ReflectMapEntities, ReflectVisitEntities, ReflectVisitEntitiesMut}, world::EntityMutExcept, }; -use bevy_math::FloatExt; use bevy_reflect::{ - prelude::ReflectDefault, utility::NonGenericTypeInfoCell, ApplyError, DynamicStruct, FieldIter, - FromReflect, FromType, GetTypeRegistration, NamedField, PartialReflect, Reflect, - ReflectFromPtr, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, Struct, StructInfo, - TypeInfo, TypePath, TypeRegistration, Typed, + prelude::ReflectDefault, utility::NonGenericTypeInfoCell, ApplyError, DynamicTupleStruct, + FromReflect, FromType, GetTypeRegistration, PartialReflect, Reflect, ReflectFromPtr, + ReflectKind, ReflectMut, ReflectOwned, ReflectRef, TupleStruct, TupleStructFieldIter, + TupleStructInfo, TypeInfo, TypePath, TypeRegistration, Typed, UnnamedField, }; use bevy_time::Time; use bevy_transform::{prelude::Transform, TransformSystem}; @@ -61,14 +61,14 @@ use uuid::Uuid; pub mod prelude { #[doc(hidden)] pub use crate::{ - animatable::*, graph::*, keyframes::*, transition::*, AnimationClip, AnimationPlayer, - AnimationPlugin, Interpolation, VariableCurve, + animatable::*, animation_curves::*, graph::*, transition::*, AnimationClip, + AnimationPlayer, AnimationPlugin, VariableCurve, }; } use crate::{ + animation_curves::AnimationCurve, graph::{AnimationGraph, AnimationGraphAssetLoader, AnimationNodeIndex}, - keyframes::Keyframes, transition::{advance_transitions, expire_completed_transitions, AnimationTransitions}, }; @@ -77,167 +77,29 @@ use crate::{ /// [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); -/// Describes how an attribute of a [`Transform`] or -/// [`bevy_render::mesh::morph::MorphWeights`] should be animated. +/// Contains an [animation curve] which is used to animate entities. /// -/// `keyframe_timestamps` and `keyframes` should have the same length. +/// [animation curve]: AnimationCurve #[derive(Debug, TypePath)] -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: Box, - /// Interpolation method to use between keyframes. - pub interpolation: Interpolation, -} +pub struct VariableCurve(pub Box); impl Clone for VariableCurve { fn clone(&self) -> Self { - VariableCurve { - keyframe_timestamps: self.keyframe_timestamps.clone(), - keyframes: Keyframes::clone_value(&*self.keyframes), - interpolation: self.interpolation, - } + Self(AnimationCurve::clone_value(&*self.0)) } } impl VariableCurve { - /// Creates a new curve from timestamps, keyframes, and interpolation type. - /// - /// The two arrays must have the same length. - pub fn new( - keyframe_timestamps: Vec, - keyframes: impl Into, - interpolation: Interpolation, - ) -> VariableCurve - where - K: Keyframes, - { - VariableCurve { - keyframe_timestamps, - keyframes: Box::new(keyframes.into()), - interpolation, - } - } - - /// Creates a new curve from timestamps and keyframes with no interpolation. - /// - /// The two arrays must have the same length. - pub fn step( - keyframe_timestamps: impl Into>, - keyframes: impl Into, - ) -> VariableCurve - where - K: Keyframes, - { - VariableCurve::new(keyframe_timestamps.into(), keyframes, Interpolation::Step) - } - - /// Creates a new curve from timestamps and keyframes with linear - /// interpolation. - /// - /// The two arrays must have the same length. - pub fn linear( - keyframe_timestamps: impl Into>, - keyframes: impl Into, - ) -> VariableCurve - where - K: Keyframes, - { - VariableCurve::new(keyframe_timestamps.into(), keyframes, Interpolation::Linear) - } - - /// Creates a new curve from timestamps and keyframes with no interpolation. - /// - /// The two arrays must have the same length. - pub fn cubic_spline( - keyframe_timestamps: impl Into>, - keyframes: impl Into, - ) -> VariableCurve - where - K: Keyframes, - { - VariableCurve::new( - keyframe_timestamps.into(), - keyframes, - Interpolation::CubicSpline, - ) - } - - /// Find the index of the keyframe at or before the current time. + /// Create a new [`VariableCurve`] from an [animation curve]. /// - /// 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), - } + /// [animation curve]: AnimationCurve + pub fn new(animation_curve: impl AnimationCurve) -> Self { + Self(Box::new(animation_curve)) } } // We have to implement `PartialReflect` manually because of the embedded -// `Box`, which can't be automatically derived yet. +// `Box`, which can't be automatically derived yet. impl PartialReflect for VariableCurve { #[inline] fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { @@ -274,32 +136,31 @@ impl PartialReflect for VariableCurve { } fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { - if let ReflectRef::Struct(struct_value) = value.reflect_ref() { - for (i, value) in struct_value.iter_fields().enumerate() { - let name = struct_value.name_at(i).unwrap(); - if let Some(v) = self.field_mut(name) { + if let ReflectRef::TupleStruct(tuple_value) = value.reflect_ref() { + for (i, value) in tuple_value.iter_fields().enumerate() { + if let Some(v) = self.field_mut(i) { v.try_apply(value)?; } } } else { return Err(ApplyError::MismatchedKinds { from_kind: value.reflect_kind(), - to_kind: ReflectKind::Struct, + to_kind: ReflectKind::TupleStruct, }); } Ok(()) } fn reflect_ref(&self) -> ReflectRef { - ReflectRef::Struct(self) + ReflectRef::TupleStruct(self) } fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::Struct(self) + ReflectMut::TupleStruct(self) } fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::Struct(self) + ReflectOwned::TupleStruct(self) } fn clone_value(&self) -> Box { @@ -308,7 +169,7 @@ impl PartialReflect for VariableCurve { } // We have to implement `Reflect` manually because of the embedded `Box`, which can't be automatically derived yet. +// AnimationCurve>`, which can't be automatically derived yet. impl Reflect for VariableCurve { #[inline] fn into_any(self: Box) -> Box { @@ -347,79 +208,38 @@ impl Reflect for VariableCurve { } } -// We have to implement `Struct` manually because of the embedded `Box`, which can't be automatically derived yet. -impl Struct for VariableCurve { - fn field(&self, name: &str) -> Option<&dyn PartialReflect> { - match name { - "keyframe_timestamps" => Some(&self.keyframe_timestamps), - "keyframes" => Some(self.keyframes.as_partial_reflect()), - "interpolation" => Some(&self.interpolation), - _ => None, - } - } - - fn field_mut(&mut self, name: &str) -> Option<&mut dyn PartialReflect> { - match name { - "keyframe_timestamps" => Some(&mut self.keyframe_timestamps), - "keyframes" => Some(self.keyframes.as_partial_reflect_mut()), - "interpolation" => Some(&mut self.interpolation), - _ => None, - } - } - - fn field_at(&self, index: usize) -> Option<&dyn PartialReflect> { +// We have to implement `TupleStruct` manually because of the embedded `Box`, which can't be automatically derived yet. +impl TupleStruct for VariableCurve { + fn field(&self, index: usize) -> Option<&dyn PartialReflect> { match index { - 0 => Some(&self.keyframe_timestamps), - 1 => Some(self.keyframes.as_partial_reflect()), - 2 => Some(&self.interpolation), + 0 => Some(self.0.as_partial_reflect()), _ => None, } } - fn field_at_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { + fn field_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { match index { - 0 => Some(&mut self.keyframe_timestamps), - 1 => Some(self.keyframes.as_partial_reflect_mut()), - 2 => Some(&mut self.interpolation), - _ => None, - } - } - - fn name_at(&self, index: usize) -> Option<&str> { - match index { - 0 => Some("keyframe_timestamps"), - 1 => Some("keyframes"), - 2 => Some("interpolation"), + 0 => Some(self.0.as_partial_reflect_mut()), _ => None, } } fn field_len(&self) -> usize { - 3 + 1 } - fn iter_fields(&self) -> FieldIter { - FieldIter::new(self) + fn iter_fields(&self) -> TupleStructFieldIter { + TupleStructFieldIter::new(self) } - fn clone_dynamic(&self) -> DynamicStruct { - DynamicStruct::from_iter([ - ( - "keyframe_timestamps", - Box::new(self.keyframe_timestamps.clone()) as Box, - ), - ("keyframes", PartialReflect::clone_value(&*self.keyframes)), - ( - "interpolation", - Box::new(self.interpolation) as Box, - ), - ]) + fn clone_dynamic(&self) -> DynamicTupleStruct { + DynamicTupleStruct::from_iter([PartialReflect::clone_value(&*self.0)]) } } // We have to implement `FromReflect` manually because of the embedded `Box`, which can't be automatically derived yet. +// AnimationCurve>`, which can't be automatically derived yet. impl FromReflect for VariableCurve { fn from_reflect(reflect: &dyn PartialReflect) -> Option { Some(reflect.try_downcast_ref::()?.clone()) @@ -427,7 +247,7 @@ impl FromReflect for VariableCurve { } // We have to implement `GetTypeRegistration` manually because of the embedded -// `Box`, which can't be automatically derived yet. +// `Box`, which can't be automatically derived yet. impl GetTypeRegistration for VariableCurve { fn get_type_registration() -> TypeRegistration { let mut registration = TypeRegistration::of::(); @@ -437,32 +257,16 @@ impl GetTypeRegistration for VariableCurve { } // We have to implement `Typed` manually because of the embedded `Box`, which can't be automatically derived yet. +// AnimationCurve>`, which can't be automatically derived yet. impl Typed for VariableCurve { fn type_info() -> &'static TypeInfo { static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); CELL.get_or_set(|| { - TypeInfo::Struct(StructInfo::new::(&[ - NamedField::new::>("keyframe_timestamps"), - NamedField::new::<()>("keyframes"), - NamedField::new::("interpolation"), - ])) + TypeInfo::TupleStruct(TupleStructInfo::new::(&[UnnamedField::new::<()>(0)])) }) } } -/// Interpolation method to use between keyframes. -#[derive(Reflect, Clone, Copy, 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. /// @@ -590,18 +394,46 @@ impl AnimationClip { self.duration = duration_sec; } - /// Adds a [`VariableCurve`] to an [`AnimationTarget`] named by an + /// Adds an [`AnimationCurve`] to an [`AnimationTarget`] named by an /// [`AnimationTargetId`]. /// /// If the curve extends beyond the current duration of this clip, this /// method lengthens this clip to include the entire time span that the /// curve covers. - pub fn add_curve_to_target(&mut self, target_id: AnimationTargetId, curve: VariableCurve) { + pub fn add_curve_to_target( + &mut self, + target_id: AnimationTargetId, + curve: impl AnimationCurve, + ) { // 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.curves.entry(target_id).or_default().push(curve); + let end = curve.domain().end(); + if end.is_finite() { + self.duration = self.duration.max(end); + } + self.curves + .entry(target_id) + .or_default() + .push(VariableCurve::new(curve)); + } + + /// Like [`add_curve_to_target`], but adding a [`VariableCurve`] directly. + /// + /// Under normal circumstances, that method is generally more convenient. + /// + /// [`add_curve_to_target`]: AnimationClip::add_curve_to_target + pub fn add_variable_curve_to_target( + &mut self, + target_id: AnimationTargetId, + variable_curve: VariableCurve, + ) { + let end = variable_curve.0.domain().end(); + if end.is_finite() { + self.duration = self.duration.max(end); + } + self.curves + .entry(target_id) + .or_default() + .push(variable_curve); } } @@ -620,15 +452,6 @@ pub enum RepeatAnimation { /// Why Bevy failed to evaluate an animation. #[derive(Clone, Debug)] pub enum AnimationEvaluationError { - /// The `keyframes` array is too small. - /// - /// For curves with `Interpolation::Step` or `Interpolation::Linear`, the - /// `keyframes` array must have at least as many elements as keyframe - /// timestamps. For curves with `Interpolation::CubicBezier`, the - /// `keyframes` array must have at least 3× the number of elements as - /// keyframe timestamps, in order to account for the tangents. - KeyframeNotPresent(usize), - /// The component to be animated isn't present on the animation target. /// /// To fix this error, make sure the entity to be animated contains all @@ -1199,36 +1022,11 @@ pub fn animate_targets( let seek_time = active_animation.seek_time; for curve in curves { - // Some curves have only one keyframe used to set a transform - if curve.keyframe_timestamps.len() == 1 { - if let Err(err) = curve.keyframes.apply_single_keyframe( - transform.as_mut().map(|transform| transform.reborrow()), - entity_mut.reborrow(), - weight, - ) { - warn!("Animation application failed: {:?}", err); - } - - 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); - - if let Err(err) = curve.keyframes.apply_tweened_keyframes( + if let Err(err) = curve.0.apply( + seek_time, transform.as_mut().map(|transform| transform.reborrow()), entity_mut.reborrow(), - curve.interpolation, - step_start, - lerp, weight, - timestamp_end - timestamp_start, ) { warn!("Animation application failed: {:?}", err); } @@ -1316,153 +1114,3 @@ impl AnimationGraphEvaluator { .extend(iter::repeat(EvaluatedAnimationGraphNode::default()).take(node_count)); } } - -#[cfg(test)] -mod tests { - use crate::{prelude::TranslationKeyframes, VariableCurve}; - use bevy_math::Vec3; - - // Returns the curve and the keyframe count. - fn test_variable_curve() -> (VariableCurve, usize) { - 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; - - assert_eq!(keyframe_timestamps.len(), keyframes.len()); - let keyframe_count = keyframes.len(); - - let variable_curve = VariableCurve::new::( - keyframe_timestamps, - keyframes, - interpolation, - ); - - // 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, keyframe_count) - } - - #[test] - fn find_current_keyframe_is_in_bounds() { - let curve = test_variable_curve().0; - 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().0; - 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().0; - 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().0; - - // 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, keyframe_count) = test_variable_curve(); - let second_last_keyframe = keyframe_count - 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, keyframe_count) = test_variable_curve(); - let second_last_keyframe = keyframe_count - 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 758c4d44ca473..1be494679a3f0 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -268,7 +268,9 @@ async fn load_gltf<'a, 'b, 'c>( #[cfg(feature = "bevy_animation")] let (animations, named_animations, animation_roots) = { - use bevy_animation::Interpolation; + use bevy_animation::{animation_curves::*, gltf_curves::*, VariableCurve}; + 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(); @@ -276,12 +278,8 @@ async fn load_gltf<'a, 'b, 'c>( for animation in gltf.animations() { let mut animation_clip = 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 { @@ -296,26 +294,150 @@ 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) => { - Box::new(TranslationKeyframes(tr.map(Vec3::from).collect())) - as Box + let translations: Vec = tr.map(Vec3::from).collect(); + if keyframe_timestamps.len() == 1 { + #[allow(clippy::unnecessary_map_on_constructor)] + Some(constant_curve(Interval::EVERYWHERE, translations[0])) + .map(TranslationCurve) + .map(VariableCurve::new) + } else { + match interpolation { + gltf::animation::Interpolation::Linear => { + UnevenSampleAutoCurve::new( + keyframe_timestamps.into_iter().zip(translations), + ) + .ok() + .map(TranslationCurve) + .map(VariableCurve::new) + } + gltf::animation::Interpolation::Step => { + SteppedKeyframeCurve::new( + keyframe_timestamps.into_iter().zip(translations), + ) + .ok() + .map(TranslationCurve) + .map(VariableCurve::new) + } + gltf::animation::Interpolation::CubicSpline => { + CubicKeyframeCurve::new(keyframe_timestamps, translations) + .ok() + .map(TranslationCurve) + .map(VariableCurve::new) + } + } + } + } + ReadOutputs::Rotations(rots) => { + let rotations: Vec = + rots.into_f32().map(Quat::from_array).collect(); + if keyframe_timestamps.len() == 1 { + #[allow(clippy::unnecessary_map_on_constructor)] + Some(constant_curve(Interval::EVERYWHERE, rotations[0])) + .map(RotationCurve) + .map(VariableCurve::new) + } else { + match interpolation { + gltf::animation::Interpolation::Linear => { + UnevenSampleAutoCurve::new( + keyframe_timestamps.into_iter().zip(rotations), + ) + .ok() + .map(RotationCurve) + .map(VariableCurve::new) + } + gltf::animation::Interpolation::Step => { + SteppedKeyframeCurve::new( + keyframe_timestamps.into_iter().zip(rotations), + ) + .ok() + .map(RotationCurve) + .map(VariableCurve::new) + } + gltf::animation::Interpolation::CubicSpline => { + CubicRotationCurve::new( + keyframe_timestamps, + rotations.into_iter().map(Vec4::from), + ) + .ok() + .map(RotationCurve) + .map(VariableCurve::new) + } + } + } } - ReadOutputs::Rotations(rots) => Box::new(RotationKeyframes( - rots.into_f32().map(bevy_math::Quat::from_array).collect(), - )) - as Box, ReadOutputs::Scales(scale) => { - Box::new(ScaleKeyframes(scale.map(Vec3::from).collect())) - as Box + let scales: Vec = scale.map(Vec3::from).collect(); + if keyframe_timestamps.len() == 1 { + #[allow(clippy::unnecessary_map_on_constructor)] + Some(constant_curve(Interval::EVERYWHERE, scales[0])) + .map(ScaleCurve) + .map(VariableCurve::new) + } else { + match interpolation { + gltf::animation::Interpolation::Linear => { + UnevenSampleAutoCurve::new( + keyframe_timestamps.into_iter().zip(scales), + ) + .ok() + .map(ScaleCurve) + .map(VariableCurve::new) + } + gltf::animation::Interpolation::Step => { + SteppedKeyframeCurve::new( + keyframe_timestamps.into_iter().zip(scales), + ) + .ok() + .map(ScaleCurve) + .map(VariableCurve::new) + } + gltf::animation::Interpolation::CubicSpline => { + CubicKeyframeCurve::new(keyframe_timestamps, scales) + .ok() + .map(ScaleCurve) + .map(VariableCurve::new) + } + } + } } ReadOutputs::MorphTargetWeights(weights) => { - let weights: Vec<_> = weights.into_f32().collect(); - Box::new(MorphWeightsKeyframes { - morph_target_count: weights.len() / keyframe_timestamps.len(), - weights, - }) as Box + let weights: Vec = weights.into_f32().collect(); + if keyframe_timestamps.len() == 1 { + #[allow(clippy::unnecessary_map_on_constructor)] + Some(constant_curve(Interval::EVERYWHERE, weights)) + .map(WeightsCurve) + .map(VariableCurve::new) + } else { + match interpolation { + gltf::animation::Interpolation::Linear => { + WideLinearKeyframeCurve::new(keyframe_timestamps, weights) + .ok() + .map(WeightsCurve) + .map(VariableCurve::new) + } + gltf::animation::Interpolation::Step => { + WideSteppedKeyframeCurve::new(keyframe_timestamps, weights) + .ok() + .map(WeightsCurve) + .map(VariableCurve::new) + } + gltf::animation::Interpolation::CubicSpline => { + WideCubicKeyframeCurve::new(keyframe_timestamps, weights) + .ok() + .map(WeightsCurve) + .map(VariableCurve::new) + } + } + } } } } else { @@ -323,15 +445,19 @@ 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( + animation_clip.add_variable_curve_to_target( AnimationTargetId::from_names(path.iter()), - VariableCurve { - keyframe_timestamps, - keyframes, - interpolation, - }, + curve, ); } else { warn!( diff --git a/crates/bevy_math/src/curve/adaptors.rs b/crates/bevy_math/src/curve/adaptors.rs index 05285572b6e6a..227a8ec3e0452 100644 --- a/crates/bevy_math/src/curve/adaptors.rs +++ b/crates/bevy_math/src/curve/adaptors.rs @@ -1,27 +1,504 @@ -//! A module containing utility helper structs to transform a [`Curve`] into another. This is useful -//! for building up complex curves from simple segments. -use core::marker::PhantomData; +//! Adaptors used by the Curve API for transforming and combining curves together. + +use super::interval::*; +use super::Curve; use crate::VectorSpace; +use core::any::type_name; +use core::fmt::{self, Debug}; +use core::marker::PhantomData; + +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::{utility::GenericTypePathCell, Reflect, TypePath}; + +#[cfg(feature = "bevy_reflect")] +mod paths { + pub(super) const THIS_MODULE: &str = "bevy_math::curve::adaptors"; + pub(super) const THIS_CRATE: &str = "bevy_math"; +} + +// NOTE ON REFLECTION: +// +// Function members of structs pose an obstacle for reflection, because they don't implement +// reflection traits themselves. Some of these are more problematic than others; for example, +// `FromReflect` is basically hopeless for function members regardless, so function-containing +// adaptors will just never be `FromReflect` (at least until function item types implement +// Default, if that ever happens). Similarly, they do not implement `TypePath`, and as a result, +// those adaptors also need custom `TypePath` adaptors which use `type_name` instead. +// +// The sum total weirdness of the `Reflect` implementations amounts to this; those adaptors: +// - are currently never `FromReflect`; +// - have custom `TypePath` implementations which are not fully stable; +// - have custom `Debug` implementations which display the function only by type name. + +/// A curve with a constant value over its domain. +/// +/// This is a curve that holds an inner value and always produces a clone of that value when sampled. +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct ConstantCurve { + pub(crate) domain: Interval, + pub(crate) value: T, +} + +impl ConstantCurve +where + T: Clone, +{ + /// Create a constant curve, which has the given `domain` and always produces the given `value` + /// when sampled. + pub fn new(domain: Interval, value: T) -> Self { + Self { domain, value } + } +} + +impl Curve for ConstantCurve +where + T: Clone, +{ + #[inline] + fn domain(&self) -> Interval { + self.domain + } + + #[inline] + fn sample_unchecked(&self, _t: f32) -> T { + self.value.clone() + } +} + +/// A curve defined by a function together with a fixed domain. +/// +/// This is a curve that holds an inner function `f` which takes numbers (`f32`) as input and produces +/// output of type `T`. The value of this curve when sampled at time `t` is just `f(t)`. +#[derive(Clone)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(where T: TypePath), + reflect(from_reflect = false, type_path = false), +)] +pub struct FunctionCurve { + pub(crate) domain: Interval, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) f: F, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) _phantom: PhantomData, +} + +impl Debug for FunctionCurve { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("FunctionCurve") + .field("domain", &self.domain) + .field("f", &type_name::()) + .finish() + } +} + +/// Note: This is not a fully stable implementation of `TypePath` due to usage of `type_name` +/// for function members. +#[cfg(feature = "bevy_reflect")] +impl TypePath for FunctionCurve +where + T: TypePath, + F: 'static, +{ + fn type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| { + format!( + "{}::FunctionCurve<{},{}>", + paths::THIS_MODULE, + T::type_path(), + type_name::() + ) + }) + } -use super::{Curve, Interval}; + fn short_type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| { + format!( + "FunctionCurve<{},{}>", + T::short_type_path(), + type_name::() + ) + }) + } + + fn type_ident() -> Option<&'static str> { + Some("FunctionCurve") + } + + fn crate_name() -> Option<&'static str> { + Some(paths::THIS_CRATE) + } + + fn module_path() -> Option<&'static str> { + Some(paths::THIS_MODULE) + } +} + +impl FunctionCurve +where + F: Fn(f32) -> T, +{ + /// Create a new curve with the given `domain` from the given `function`. When sampled, the + /// `function` is evaluated at the sample time to compute the output. + pub fn new(domain: Interval, function: F) -> Self { + FunctionCurve { + domain, + f: function, + _phantom: PhantomData, + } + } +} + +impl Curve for FunctionCurve +where + F: Fn(f32) -> T, +{ + #[inline] + fn domain(&self) -> Interval { + self.domain + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + (self.f)(t) + } +} + +/// A curve whose samples are defined by mapping samples from another curve through a +/// given function. Curves of this type are produced by [`Curve::map`]. +#[derive(Clone)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(where S: TypePath, T: TypePath, C: TypePath), + reflect(from_reflect = false, type_path = false), +)] +pub struct MapCurve { + pub(crate) preimage: C, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) f: F, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) _phantom: PhantomData<(S, T)>, +} + +impl Debug for MapCurve +where + C: Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MapCurve") + .field("preimage", &self.preimage) + .field("f", &type_name::()) + .finish() + } +} + +/// Note: This is not a fully stable implementation of `TypePath` due to usage of `type_name` +/// for function members. +#[cfg(feature = "bevy_reflect")] +impl TypePath for MapCurve +where + S: TypePath, + T: TypePath, + C: TypePath, + F: 'static, +{ + fn type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| { + format!( + "{}::MapCurve<{},{},{},{}>", + paths::THIS_MODULE, + S::type_path(), + T::type_path(), + C::type_path(), + type_name::() + ) + }) + } + + fn short_type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| { + format!( + "MapCurve<{},{},{},{}>", + S::type_path(), + T::type_path(), + C::type_path(), + type_name::() + ) + }) + } + + fn type_ident() -> Option<&'static str> { + Some("MapCurve") + } + + fn crate_name() -> Option<&'static str> { + Some(paths::THIS_CRATE) + } + + fn module_path() -> Option<&'static str> { + Some(paths::THIS_MODULE) + } +} + +impl Curve for MapCurve +where + C: Curve, + F: Fn(S) -> T, +{ + #[inline] + fn domain(&self) -> Interval { + self.preimage.domain() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + (self.f)(self.preimage.sample_unchecked(t)) + } +} + +/// A curve whose sample space is mapped onto that of some base curve's before sampling. +/// Curves of this type are produced by [`Curve::reparametrize`]. +#[derive(Clone)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(where T: TypePath, C: TypePath), + reflect(from_reflect = false, type_path = false), +)] +pub struct ReparamCurve { + pub(crate) domain: Interval, + pub(crate) base: C, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) f: F, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) _phantom: PhantomData, +} + +impl Debug for ReparamCurve +where + C: Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ReparamCurve") + .field("domain", &self.domain) + .field("base", &self.base) + .field("f", &type_name::()) + .finish() + } +} + +/// Note: This is not a fully stable implementation of `TypePath` due to usage of `type_name` +/// for function members. +#[cfg(feature = "bevy_reflect")] +impl TypePath for ReparamCurve +where + T: TypePath, + C: TypePath, + F: 'static, +{ + fn type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| { + format!( + "{}::ReparamCurve<{},{},{}>", + paths::THIS_MODULE, + T::type_path(), + C::type_path(), + type_name::() + ) + }) + } + + fn short_type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| { + format!( + "ReparamCurve<{},{},{}>", + T::type_path(), + C::type_path(), + type_name::() + ) + }) + } + + fn type_ident() -> Option<&'static str> { + Some("ReparamCurve") + } + + fn crate_name() -> Option<&'static str> { + Some(paths::THIS_CRATE) + } + + fn module_path() -> Option<&'static str> { + Some(paths::THIS_MODULE) + } +} + +impl Curve for ReparamCurve +where + C: Curve, + F: Fn(f32) -> f32, +{ + #[inline] + fn domain(&self) -> Interval { + self.domain + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + self.base.sample_unchecked((self.f)(t)) + } +} + +/// A curve that has had its domain changed by a linear reparametrization (stretching and scaling). +/// Curves of this type are produced by [`Curve::reparametrize_linear`]. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(from_reflect = false) +)] +pub struct LinearReparamCurve { + /// Invariants: The domain of this curve must always be bounded. + pub(crate) base: C, + /// Invariants: This interval must always be bounded. + pub(crate) new_domain: Interval, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) _phantom: PhantomData, +} + +impl Curve for LinearReparamCurve +where + C: Curve, +{ + #[inline] + fn domain(&self) -> Interval { + self.new_domain + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + // The invariants imply this unwrap always succeeds. + let f = self.new_domain.linear_map_to(self.base.domain()).unwrap(); + self.base.sample_unchecked(f(t)) + } +} + +/// A curve that has been reparametrized by another curve, using that curve to transform the +/// sample times before sampling. Curves of this type are produced by [`Curve::reparametrize_by_curve`]. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct CurveReparamCurve { + pub(crate) base: C, + pub(crate) reparam_curve: D, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) _phantom: PhantomData, +} + +impl Curve for CurveReparamCurve +where + C: Curve, + D: Curve, +{ + #[inline] + fn domain(&self) -> Interval { + self.reparam_curve.domain() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + let sample_time = self.reparam_curve.sample_unchecked(t); + self.base.sample_unchecked(sample_time) + } +} + +/// A curve that is the graph of another curve over its parameter space. Curves of this type are +/// produced by [`Curve::graph`]. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct GraphCurve { + pub(crate) base: C, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) _phantom: PhantomData, +} + +impl Curve<(f32, T)> for GraphCurve +where + C: Curve, +{ + #[inline] + fn domain(&self) -> Interval { + self.base.domain() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> (f32, T) { + (t, self.base.sample_unchecked(t)) + } +} + +/// A curve that combines the output data from two constituent curves into a tuple output. Curves +/// of this type are produced by [`Curve::zip`]. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct ZipCurve { + pub(crate) domain: Interval, + pub(crate) first: C, + pub(crate) second: D, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) _phantom: PhantomData<(S, T)>, +} + +impl Curve<(S, T)> for ZipCurve +where + C: Curve, + D: Curve, +{ + #[inline] + fn domain(&self) -> Interval { + self.domain + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> (S, T) { + ( + self.first.sample_unchecked(t), + self.second.sample_unchecked(t), + ) + } +} /// The curve that results from chaining one curve with another. The second curve is /// effectively reparametrized so that its start is at the end of the first. /// -/// Curves of this type are produced by [`Curve::chain`]. -/// -/// # Domain +/// For this to be well-formed, the first curve's domain must be right-finite and the second's +/// must be left-finite. /// -/// The first curve's domain must be right-finite and the second's must be left-finite to get a -/// valid [`ChainCurve`]. +/// Curves of this type are produced by [`Curve::chain`]. #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub struct ChainCurve { - pub(super) first: C, - pub(super) second: D, - pub(super) _phantom: PhantomData, + pub(crate) first: C, + pub(crate) second: D, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) _phantom: PhantomData, } impl Curve for ChainCurve @@ -62,10 +539,11 @@ where /// The original curve's domain must be bounded to get a valid [`ReverseCurve`]. #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub struct ReverseCurve { - pub(super) curve: C, - pub(super) _phantom: PhantomData, + pub(crate) curve: C, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) _phantom: PhantomData, } impl Curve for ReverseCurve @@ -98,11 +576,12 @@ where /// The original curve's domain must be bounded to get a valid [`RepeatCurve`]. #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub struct RepeatCurve { - pub(super) domain: Interval, - pub(super) curve: C, - pub(super) _phantom: PhantomData, + pub(crate) domain: Interval, + pub(crate) curve: C, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) _phantom: PhantomData, } impl Curve for RepeatCurve @@ -142,10 +621,11 @@ where /// The original curve's domain must be bounded to get a valid [`ForeverCurve`]. #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub struct ForeverCurve { - pub(super) curve: C, - pub(super) _phantom: PhantomData, + pub(crate) curve: C, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) _phantom: PhantomData, } impl Curve for ForeverCurve @@ -181,10 +661,11 @@ where /// The original curve's domain must be right-finite to get a valid [`PingPongCurve`]. #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub struct PingPongCurve { - pub(super) curve: C, - pub(super) _phantom: PhantomData, + pub(crate) curve: C, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) _phantom: PhantomData, } impl Curve for PingPongCurve @@ -230,13 +711,14 @@ where /// valid [`ContinuationCurve`]. #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub struct ContinuationCurve { - pub(super) first: C, - pub(super) second: D, + pub(crate) first: C, + pub(crate) second: D, // cache the offset in the curve directly to prevent triple sampling for every sample we make - pub(super) offset: T, - pub(super) _phantom: PhantomData, + pub(crate) offset: T, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) _phantom: PhantomData, } impl Curve for ContinuationCurve 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..b8adcd30d5c45 --- /dev/null +++ b/crates/bevy_math/src/curve/iterable.rs @@ -0,0 +1,53 @@ +//! 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(&self, t: f32) -> impl Iterator; + + /// 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(&self, _t: f32) -> impl Iterator { + self.value.iter().cloned() + } +} diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 5f630bc39970b..1a24266a3e335 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -5,13 +5,14 @@ pub mod adaptors; pub mod cores; pub mod interval; +pub mod iterable; pub mod sample_curves; // bevy_math::curve re-exports all commonly-needed curve-related items. +pub use adaptors::*; pub use interval::{interval, Interval}; pub use sample_curves::*; -use adaptors::*; use cores::{EvenCore, UnevenCore}; use crate::{StableInterpolate, VectorSpace}; @@ -232,13 +233,13 @@ pub trait Curve { /// time `t` and `y` is the sample of `other` at time `t`. The domain of the new curve is the /// intersection of the domains of its constituents. If the domain intersection would be empty, /// an error is returned. - fn zip(self, other: C) -> Result, InvalidIntervalError> + fn zip(self, other: C) -> Result, InvalidIntervalError> where Self: Sized, C: Curve + Sized, { let domain = self.domain().intersect(other.domain())?; - Ok(ProductCurve { + Ok(ZipCurve { domain, first: self, second: other, @@ -691,255 +692,6 @@ pub enum ResamplingError { UnboundedDomain, } -/// A curve with a constant value over its domain. -/// -/// This is a curve that holds an inner value and always produces a clone of that value when sampled. -#[derive(Clone, Copy, Debug)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] -pub struct ConstantCurve { - domain: Interval, - value: T, -} - -impl ConstantCurve -where - T: Clone, -{ - /// Create a constant curve, which has the given `domain` and always produces the given `value` - /// when sampled. - pub fn new(domain: Interval, value: T) -> Self { - Self { domain, value } - } -} - -impl Curve for ConstantCurve -where - T: Clone, -{ - #[inline] - fn domain(&self) -> Interval { - self.domain - } - - #[inline] - fn sample_unchecked(&self, _t: f32) -> T { - self.value.clone() - } -} - -/// A curve defined by a function together with a fixed domain. -/// -/// This is a curve that holds an inner function `f` which takes numbers (`f32`) as input and produces -/// output of type `T`. The value of this curve when sampled at time `t` is just `f(t)`. -#[derive(Clone, Debug)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] -pub struct FunctionCurve { - domain: Interval, - f: F, - _phantom: PhantomData, -} - -impl FunctionCurve -where - F: Fn(f32) -> T, -{ - /// Create a new curve with the given `domain` from the given `function`. When sampled, the - /// `function` is evaluated at the sample time to compute the output. - pub fn new(domain: Interval, function: F) -> Self { - FunctionCurve { - domain, - f: function, - _phantom: PhantomData, - } - } -} - -impl Curve for FunctionCurve -where - F: Fn(f32) -> T, -{ - #[inline] - fn domain(&self) -> Interval { - self.domain - } - - #[inline] - fn sample_unchecked(&self, t: f32) -> T { - (self.f)(t) - } -} - -/// A curve whose samples are defined by mapping samples from another curve through a -/// given function. Curves of this type are produced by [`Curve::map`]. -#[derive(Clone, Debug)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] -pub struct MapCurve { - preimage: C, - f: F, - _phantom: PhantomData<(S, T)>, -} - -impl Curve for MapCurve -where - C: Curve, - F: Fn(S) -> T, -{ - #[inline] - fn domain(&self) -> Interval { - self.preimage.domain() - } - - #[inline] - fn sample_unchecked(&self, t: f32) -> T { - (self.f)(self.preimage.sample_unchecked(t)) - } -} - -/// A curve whose sample space is mapped onto that of some base curve's before sampling. -/// Curves of this type are produced by [`Curve::reparametrize`]. -#[derive(Clone, Debug)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] -pub struct ReparamCurve { - domain: Interval, - base: C, - f: F, - _phantom: PhantomData, -} - -impl Curve for ReparamCurve -where - C: Curve, - F: Fn(f32) -> f32, -{ - #[inline] - fn domain(&self) -> Interval { - self.domain - } - - #[inline] - fn sample_unchecked(&self, t: f32) -> T { - self.base.sample_unchecked((self.f)(t)) - } -} - -/// A curve that has had its domain changed by a linear reparametrization (stretching and scaling). -/// Curves of this type are produced by [`Curve::reparametrize_linear`]. -#[derive(Clone, Debug)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] -pub struct LinearReparamCurve { - /// Invariants: The domain of the inner curve must always be bounded. - base: C, - /// Invariants: This interval must always be bounded. - new_domain: Interval, - _phantom: PhantomData, -} - -impl Curve for LinearReparamCurve -where - C: Curve, -{ - #[inline] - fn domain(&self) -> Interval { - self.new_domain - } - - #[inline] - fn sample_unchecked(&self, t: f32) -> T { - // The invariants imply this unwrap always succeeds. - let f = self.new_domain.linear_map_to(self.base.domain()).unwrap(); - self.base.sample_unchecked(f(t)) - } -} - -/// A curve that has been reparametrized by another curve, using that curve to transform the -/// sample times before sampling. Curves of this type are produced by [`Curve::reparametrize_by_curve`]. -#[derive(Clone, Debug)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] -pub struct CurveReparamCurve { - base: C, - reparam_curve: D, - _phantom: PhantomData, -} - -impl Curve for CurveReparamCurve -where - C: Curve, - D: Curve, -{ - #[inline] - fn domain(&self) -> Interval { - self.reparam_curve.domain() - } - - #[inline] - fn sample_unchecked(&self, t: f32) -> T { - let sample_time = self.reparam_curve.sample_unchecked(t); - self.base.sample_unchecked(sample_time) - } -} - -/// A curve that is the graph of another curve over its parameter space. Curves of this type are -/// produced by [`Curve::graph`]. -#[derive(Clone, Debug)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] -pub struct GraphCurve { - base: C, - _phantom: PhantomData, -} - -impl Curve<(f32, T)> for GraphCurve -where - C: Curve, -{ - #[inline] - fn domain(&self) -> Interval { - self.base.domain() - } - - #[inline] - fn sample_unchecked(&self, t: f32) -> (f32, T) { - (t, self.base.sample_unchecked(t)) - } -} - -/// A curve that combines the output data from two constituent curves into a tuple output. Curves -/// of this type are produced by [`Curve::zip`]. -#[derive(Clone, Debug)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] -pub struct ProductCurve { - domain: Interval, - first: C, - second: D, - _phantom: PhantomData<(S, T)>, -} - -impl Curve<(S, T)> for ProductCurve -where - C: Curve, - D: Curve, -{ - #[inline] - fn domain(&self) -> Interval { - self.domain - } - - #[inline] - fn sample_unchecked(&self, t: f32) -> (S, T) { - ( - self.first.sample_unchecked(t), - self.second.sample_unchecked(t), - ) - } -} - /// Create a [`Curve`] that constantly takes the given `value` over the given `domain`. pub fn constant_curve(domain: Interval, value: T) -> ConstantCurve { ConstantCurve { domain, value } diff --git a/examples/animation/animated_transform.rs b/examples/animation/animated_transform.rs index d5eb9780c9e4d..14c0c9fce5ded 100644 --- a/examples/animation/animated_transform.rs +++ b/examples/animation/animated_transform.rs @@ -49,79 +49,79 @@ fn setup( // Creating the animation let mut animation = AnimationClip::default(); - // A curve can modify a single part of a transform, here the translation + // A curve can modify a single part of a transform: here, the translation. let planet_animation_target_id = AnimationTargetId::from_name(&planet); animation.add_curve_to_target( planet_animation_target_id, - VariableCurve::linear::( - [0.0, 1.0, 2.0, 3.0, 4.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), - 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), - ], - ), + UnevenSampleAutoCurve::new([0.0, 1.0, 2.0, 3.0, 4.0].into_iter().zip([ + 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), + // 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), + ])) + .map(TranslationCurve) + .expect("should be able to build translation curve because we pass in valid samples"), ); // Or it can modify the rotation of the transform. // To find the entity to modify, the hierarchy will be traversed looking for - // an entity with the right name at each level + // an entity with the right name at each level. let orbit_controller_animation_target_id = AnimationTargetId::from_names([planet.clone(), orbit_controller.clone()].iter()); animation.add_curve_to_target( orbit_controller_animation_target_id, - VariableCurve::linear::( - [0.0, 1.0, 2.0, 3.0, 4.0], - [ - 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, - ], - ), + UnevenSampleAutoCurve::new([0.0, 1.0, 2.0, 3.0, 4.0].into_iter().zip([ + 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, + ])) + .map(RotationCurve) + .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 - // be created for each part that would have a different duration / period + // be created for each part that would have a different duration / period. let satellite_animation_target_id = AnimationTargetId::from_names( [planet.clone(), orbit_controller.clone(), satellite.clone()].iter(), ); animation.add_curve_to_target( satellite_animation_target_id, - VariableCurve::linear::( - [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0], - [ - 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), - ], - ), + UnevenSampleAutoCurve::new( + [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0] + .into_iter() + .zip([ + 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), + ]), + ) + .map(ScaleCurve) + .expect("Failed to build scale curve"), ); - // There can be more than one curve targeting the same entity path + // 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::linear::( - [0.0, 1.0, 2.0, 3.0, 4.0], - [ - 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, - ], - ), + UnevenSampleAutoCurve::new([0.0, 1.0, 2.0, 3.0, 4.0].into_iter().zip([ + 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, + ])) + .map(RotationCurve) + .expect("should be able to build translation curve because we pass in valid samples"), ); // Create the animation graph diff --git a/examples/animation/animated_ui.rs b/examples/animation/animated_ui.rs index ad55bac900358..448f692e0962c 100644 --- a/examples/animation/animated_ui.rs +++ b/examples/animation/animated_ui.rs @@ -77,24 +77,34 @@ impl AnimationInfo { // Create a curve that animates font size. // - // `VariableCurve::linear` is just a convenience constructor; it's also - // possible to initialize the structure manually. + // The curve itself is a `Curve`, and `f32` is `FontSizeProperty::Property`, + // which is required by `AnimatableCurve::from_curve`. animation_clip.add_curve_to_target( animation_target_id, - VariableCurve::linear::>( - [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0], - [24.0, 80.0, 24.0, 80.0, 24.0, 80.0, 24.0], - ), + AnimatableKeyframeCurve::new( + [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0] + .into_iter() + .zip([24.0, 80.0, 24.0, 80.0, 24.0, 80.0, 24.0]), + ) + .map(AnimatableCurve::::from_curve) + .expect("should be able to build translation curve because we pass in valid samples"), ); // Create a curve that animates font color. Note that this should have // the same time duration as the previous curve. + // + // Similar to the above, the curve itself is a `Curve`, and `Srgba` is + // `TextColorProperty::Property`, which is required by the `from_curve` method. animation_clip.add_curve_to_target( animation_target_id, - VariableCurve::linear::>( - [0.0, 1.0, 2.0, 3.0], - [Srgba::RED, Srgba::GREEN, Srgba::BLUE, Srgba::RED], - ), + AnimatableKeyframeCurve::new([0.0, 1.0, 2.0, 3.0].into_iter().zip([ + Srgba::RED, + Srgba::GREEN, + Srgba::BLUE, + Srgba::RED, + ])) + .map(AnimatableCurve::::from_curve) + .expect("should be able to build translation curve because we pass in valid samples"), ); // Save our animation clip as an asset.