bevy/crates/bevy_animation/src/animatable.rs

274 lines
8.9 KiB
Rust
Raw Normal View History

//! Traits and type for interpolating between values.
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<dyn AnimationCurve>`. 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<dyn AnimationCurve>; /// 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<Mut<'a, Transform>>, entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle<AnimationGraph>)>, 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<Vec3>` 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<C>(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: <details> <summary>Expand AnimatableProperty example</summary An `AnimatableProperty` is 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: ```rust #[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: ```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::<FontSizeProperty, _>::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). </details> ### 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<dyn AnimationCurve>` 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). <img width="669" alt="Screenshot 2024-09-27 at 9 41 50 AM" src="https://github.com/user-attachments/assets/5ba4e9ac-3aea-452e-aaf8-1492acc2d7fc"> <img width="673" alt="Screenshot 2024-09-27 at 9 45 18 AM" src="https://github.com/user-attachments/assets/8982538b-04cf-46b5-97b2-164c6bc8162e"> --- ## 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>
2024-09-30 19:56:55 +00:00
use crate::util;
use bevy_color::{Laba, LinearRgba, Oklaba, Srgba, Xyza};
use bevy_math::*;
use bevy_reflect::Reflect;
use bevy_transform::prelude::Transform;
/// An individual input for [`Animatable::blend`].
pub struct BlendInput<T> {
/// The individual item's weight. This may not be bound to the range `[0.0, 1.0]`.
pub weight: f32,
/// The input value to be blended.
pub value: T,
/// Whether or not to additively blend this input into the final result.
pub additive: bool,
}
/// An animatable value type.
pub trait Animatable: Reflect + Sized + Send + Sync + 'static {
/// Interpolates between `a` and `b` with a interpolation factor of `time`.
///
/// The `time` parameter here may not be clamped to the range `[0.0, 1.0]`.
fn interpolate(a: &Self, b: &Self, time: f32) -> Self;
/// Blends one or more values together.
///
/// Implementors should return a default value when no inputs are provided here.
fn blend(inputs: impl Iterator<Item = BlendInput<Self>>) -> Self;
}
macro_rules! impl_float_animatable {
($ty: ty, $base: ty) => {
impl Animatable for $ty {
#[inline]
fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
let t = <$base>::from(t);
(*a) * (1.0 - t) + (*b) * t
}
#[inline]
fn blend(inputs: impl Iterator<Item = BlendInput<Self>>) -> Self {
let mut value = Default::default();
for input in inputs {
if input.additive {
value += <$base>::from(input.weight) * input.value;
} else {
value = Self::interpolate(&value, &input.value, input.weight);
}
}
value
}
}
};
}
macro_rules! impl_color_animatable {
($ty: ident) => {
impl Animatable for $ty {
#[inline]
fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
let value = *a * (1. - t) + *b * t;
value
}
#[inline]
fn blend(inputs: impl Iterator<Item = BlendInput<Self>>) -> Self {
let mut value = Default::default();
for input in inputs {
if input.additive {
value += input.weight * input.value;
} else {
value = Self::interpolate(&value, &input.value, input.weight);
}
}
value
}
}
};
}
impl_float_animatable!(f32, f32);
impl_float_animatable!(Vec2, f32);
impl_float_animatable!(Vec3A, f32);
impl_float_animatable!(Vec4, f32);
impl_float_animatable!(f64, f64);
impl_float_animatable!(DVec2, f64);
impl_float_animatable!(DVec3, f64);
impl_float_animatable!(DVec4, f64);
impl_color_animatable!(LinearRgba);
impl_color_animatable!(Laba);
impl_color_animatable!(Oklaba);
impl_color_animatable!(Srgba);
impl_color_animatable!(Xyza);
// Vec3 is special cased to use Vec3A internally for blending
impl Animatable for Vec3 {
#[inline]
fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
(*a) * (1.0 - t) + (*b) * t
}
#[inline]
fn blend(inputs: impl Iterator<Item = BlendInput<Self>>) -> Self {
let mut value = Vec3A::ZERO;
for input in inputs {
if input.additive {
value += input.weight * Vec3A::from(input.value);
} else {
value = Vec3A::interpolate(&value, &Vec3A::from(input.value), input.weight);
}
}
Self::from(value)
}
}
impl Animatable for bool {
#[inline]
fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
util::step_unclamped(*a, *b, t)
}
#[inline]
fn blend(inputs: impl Iterator<Item = BlendInput<Self>>) -> Self {
inputs
.max_by(|a, b| FloatOrd(a.weight).cmp(&FloatOrd(b.weight)))
.map(|input| input.value)
.unwrap_or(false)
}
}
impl Animatable for Transform {
fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
Self {
translation: Vec3::interpolate(&a.translation, &b.translation, t),
rotation: Quat::interpolate(&a.rotation, &b.rotation, t),
scale: Vec3::interpolate(&a.scale, &b.scale, t),
}
}
fn blend(inputs: impl Iterator<Item = BlendInput<Self>>) -> Self {
let mut translation = Vec3A::ZERO;
let mut scale = Vec3A::ZERO;
let mut rotation = Quat::IDENTITY;
for input in inputs {
if input.additive {
translation += input.weight * Vec3A::from(input.value.translation);
scale += input.weight * Vec3A::from(input.value.scale);
rotation =
Quat::slerp(Quat::IDENTITY, input.value.rotation, input.weight) * rotation;
} else {
translation = Vec3A::interpolate(
&translation,
&Vec3A::from(input.value.translation),
input.weight,
);
scale = Vec3A::interpolate(&scale, &Vec3A::from(input.value.scale), input.weight);
rotation = Quat::interpolate(&rotation, &input.value.rotation, input.weight);
}
}
Self {
translation: Vec3::from(translation),
rotation,
scale: Vec3::from(scale),
}
}
}
impl Animatable for Quat {
/// Performs a slerp to smoothly interpolate between quaternions.
#[inline]
fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
// We want to smoothly interpolate between the two quaternions by default,
// rather than using a quicker but less correct linear interpolation.
a.slerp(*b, t)
}
#[inline]
fn blend(inputs: impl Iterator<Item = BlendInput<Self>>) -> Self {
let mut value = Self::IDENTITY;
for BlendInput {
weight,
value: incoming_value,
additive,
} in inputs
{
if additive {
value = Self::slerp(Self::IDENTITY, incoming_value, weight) * value;
} else {
value = Self::interpolate(&value, &incoming_value, weight);
}
}
value
}
}
Allow animation clips to animate arbitrary properties. (#15282) Currently, Bevy restricts animation clips to animating `Transform::translation`, `Transform::rotation`, `Transform::scale`, or `MorphWeights`, which correspond to the properties that glTF can animate. This is insufficient for many use cases such as animating UI, as the UI layout systems expect to have exclusive control over UI elements' `Transform`s and therefore the `Style` properties must be animated instead. This commit fixes this, allowing for `AnimationClip`s to animate arbitrary properties. The `Keyframes` structure has been turned into a low-level trait that can be implemented to achieve arbitrary animation behavior. Along with `Keyframes`, this patch adds a higher-level trait, `AnimatableProperty`, that simplifies the task of animating single interpolable properties. Built-in `Keyframes` implementations exist for translation, rotation, scale, and morph weights. For the most part, you can migrate by simply changing your code from `Keyframes::Translation(...)` to `TranslationKeyframes(...)`, and likewise for rotation, scale, and morph weights. An example `AnimatableProperty` implementation for the font size of a text section follows: #[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) } } In order to keep this patch relatively small, this patch doesn't include an implementation of `AnimatableProperty` on top of the reflection system. That can be a follow-up. This patch builds on top of the new `EntityMutExcept<>` type in order to widen the `AnimationTarget` query to include write access to all components. Because `EntityMutExcept<>` has some performance overhead over an explicit query, we continue to explicitly query `Transform` in order to avoid regressing the performance of skeletal animation, such as the `many_foxes` benchmark. I've measured the performance of that benchmark and have found no significant regressions. A new example, `animated_ui`, has been added. This example shows how to use Bevy's built-in animation infrastructure to animate font size and color, which wasn't possible before this patch. ## Showcase https://github.com/user-attachments/assets/1fa73492-a9ce-405a-a8f2-4aacd7f6dc97 ## Migration Guide * Animation keyframes are now an extensible trait, not an enum. Replace `Keyframes::Translation(...)`, `Keyframes::Scale(...)`, `Keyframes::Rotation(...)`, and `Keyframes::Weights(...)` with `Box::new(TranslationKeyframes(...))`, `Box::new(ScaleKeyframes(...))`, `Box::new(RotationKeyframes(...))`, and `Box::new(MorphWeightsKeyframes(...))` respectively.
2024-09-23 17:14:12 +00:00
/// 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`.
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<dyn AnimationCurve>`. 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<dyn AnimationCurve>; /// 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<Mut<'a, Transform>>, entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle<AnimationGraph>)>, 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<Vec3>` 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<C>(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: <details> <summary>Expand AnimatableProperty example</summary An `AnimatableProperty` is 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: ```rust #[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: ```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::<FontSizeProperty, _>::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). </details> ### 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<dyn AnimationCurve>` 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). <img width="669" alt="Screenshot 2024-09-27 at 9 41 50 AM" src="https://github.com/user-attachments/assets/5ba4e9ac-3aea-452e-aaf8-1492acc2d7fc"> <img width="673" alt="Screenshot 2024-09-27 at 9 45 18 AM" src="https://github.com/user-attachments/assets/8982538b-04cf-46b5-97b2-164c6bc8162e"> --- ## 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>
2024-09-30 19:56:55 +00:00
pub fn interpolate_with_cubic_bezier<T>(p0: &T, d0: &T, d3: &T, p3: &T, t: f32, duration: f32) -> T
Allow animation clips to animate arbitrary properties. (#15282) Currently, Bevy restricts animation clips to animating `Transform::translation`, `Transform::rotation`, `Transform::scale`, or `MorphWeights`, which correspond to the properties that glTF can animate. This is insufficient for many use cases such as animating UI, as the UI layout systems expect to have exclusive control over UI elements' `Transform`s and therefore the `Style` properties must be animated instead. This commit fixes this, allowing for `AnimationClip`s to animate arbitrary properties. The `Keyframes` structure has been turned into a low-level trait that can be implemented to achieve arbitrary animation behavior. Along with `Keyframes`, this patch adds a higher-level trait, `AnimatableProperty`, that simplifies the task of animating single interpolable properties. Built-in `Keyframes` implementations exist for translation, rotation, scale, and morph weights. For the most part, you can migrate by simply changing your code from `Keyframes::Translation(...)` to `TranslationKeyframes(...)`, and likewise for rotation, scale, and morph weights. An example `AnimatableProperty` implementation for the font size of a text section follows: #[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) } } In order to keep this patch relatively small, this patch doesn't include an implementation of `AnimatableProperty` on top of the reflection system. That can be a follow-up. This patch builds on top of the new `EntityMutExcept<>` type in order to widen the `AnimationTarget` query to include write access to all components. Because `EntityMutExcept<>` has some performance overhead over an explicit query, we continue to explicitly query `Transform` in order to avoid regressing the performance of skeletal animation, such as the `many_foxes` benchmark. I've measured the performance of that benchmark and have found no significant regressions. A new example, `animated_ui`, has been added. This example shows how to use Bevy's built-in animation infrastructure to animate font size and color, which wasn't possible before this patch. ## Showcase https://github.com/user-attachments/assets/1fa73492-a9ce-405a-a8f2-4aacd7f6dc97 ## Migration Guide * Animation keyframes are now an extensible trait, not an enum. Replace `Keyframes::Translation(...)`, `Keyframes::Scale(...)`, `Keyframes::Rotation(...)`, and `Keyframes::Weights(...)` with `Box::new(TranslationKeyframes(...))`, `Box::new(ScaleKeyframes(...))`, `Box::new(RotationKeyframes(...))`, and `Box::new(MorphWeightsKeyframes(...))` respectively.
2024-09-23 17:14:12 +00:00
where
T: Animatable + Clone,
{
// We're given two endpoints, along with the derivatives at those endpoints,
// and have to evaluate the cubic Bézier curve at time t using only
// (additive) blending and linear interpolation.
//
// Evaluating a Bézier curve via repeated linear interpolation when the
// control points are known is straightforward via [de Casteljau
// subdivision]. So the only remaining problem is to get the two off-curve
// control points. The [derivative of the cubic Bézier curve] is:
//
// B(t) = 3(1 - t)²(P₁ - P₀) + 6(1 - t)t(P₂ - P₁) + 3t²(P₃ - P₂)
//
// Setting t = 0 and t = 1 and solving gives us:
//
// P₁ = P₀ + B(0) / 3
// P₂ = P₃ - B(1) / 3
//
// These P₁ and P₂ formulas can be expressed as additive blends.
//
// So, to sum up, first we calculate the off-curve control points via
// additive blending, and then we use repeated linear interpolation to
// evaluate the curve.
//
// [de Casteljau subdivision]: https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm
// [derivative of the cubic Bézier curve]: https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B%C3%A9zier_curves
// Compute control points from derivatives.
let p1 = T::blend(
[
BlendInput {
weight: duration / 3.0,
value: (*d0).clone(),
additive: true,
},
BlendInput {
weight: 1.0,
value: (*p0).clone(),
additive: true,
},
]
.into_iter(),
);
let p2 = T::blend(
[
BlendInput {
weight: duration / -3.0,
value: (*d3).clone(),
additive: true,
},
BlendInput {
weight: 1.0,
value: (*p3).clone(),
additive: true,
},
]
.into_iter(),
);
// Use de Casteljau subdivision to evaluate.
let p0p1 = T::interpolate(p0, &p1, t);
let p1p2 = T::interpolate(&p1, &p2, t);
let p2p3 = T::interpolate(&p2, p3, t);
let p0p1p2 = T::interpolate(&p0p1, &p1p2, t);
let p1p2p3 = T::interpolate(&p1p2, &p2p3, t);
T::interpolate(&p0p1p2, &p1p2p3, t)
}