2024-08-31 18:38:34 +00:00
|
|
|
|
//! 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;
|
2024-05-10 13:15:56 +00:00
|
|
|
|
use bevy_color::{Laba, LinearRgba, Oklaba, Srgba, Xyza};
|
2024-02-02 21:19:37 +00:00
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-22 00:06:24 +00:00
|
|
|
|
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;
|
2024-05-10 13:15:56 +00:00
|
|
|
|
value
|
2024-03-22 00:06:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-05-10 13:15:56 +00:00
|
|
|
|
value
|
2024-03-22 00:06:24 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-02 21:19:37 +00:00
|
|
|
|
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);
|
|
|
|
|
|
2024-03-22 00:06:24 +00:00
|
|
|
|
impl_color_animatable!(LinearRgba);
|
|
|
|
|
impl_color_animatable!(Laba);
|
|
|
|
|
impl_color_animatable!(Oklaba);
|
2024-03-22 17:31:48 +00:00
|
|
|
|
impl_color_animatable!(Srgba);
|
2024-03-22 00:06:24 +00:00
|
|
|
|
impl_color_animatable!(Xyza);
|
|
|
|
|
|
2024-02-02 21:19:37 +00:00
|
|
|
|
// 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 = rotation.slerp(input.value.rotation, input.weight);
|
|
|
|
|
} 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 {
|
2024-02-20 23:26:40 +00:00
|
|
|
|
/// Performs a slerp to smoothly interpolate between quaternions.
|
2024-02-02 21:19:37 +00:00
|
|
|
|
#[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 input in inputs {
|
|
|
|
|
value = Self::interpolate(&value, &input.value, input.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)
|
|
|
|
|
}
|