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.
This commit is contained in:
Patrick Walton 2024-09-23 10:14:12 -07:00 committed by GitHub
parent 6e95f297ea
commit 8154164f1b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1440 additions and 449 deletions

View file

@ -3500,6 +3500,17 @@ description = "Demonstrates percentage-closer soft shadows (PCSS)"
category = "3D Rendering"
wasm = false
[[example]]
name = "animated_ui"
path = "examples/animation/animated_ui.rs"
doc-scrape-examples = true
[package.metadata.example.animated_ui]
name = "Animated UI"
description = "Shows how to use animation clips to animate UI properties"
category = "Animation"
wasm = true
[profile.wasm-release]
inherits = "release"
opt-level = "z"

View file

@ -27,6 +27,10 @@ bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" }
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" }
bevy_ui = { path = "../bevy_ui", version = "0.15.0-dev", features = [
"bevy_text",
] }
bevy_text = { path = "../bevy_text", version = "0.15.0-dev" }
# other
fixedbitset = "0.5"

View file

@ -1,8 +1,7 @@
//! Traits and type for interpolating between values.
use crate::util;
use crate::{util, AnimationEvaluationError, Interpolation};
use bevy_color::{Laba, LinearRgba, Oklaba, Srgba, Xyza};
use bevy_ecs::world::World;
use bevy_math::*;
use bevy_reflect::Reflect;
use bevy_transform::prelude::Transform;
@ -28,10 +27,6 @@ pub trait Animatable: Reflect + Sized + Send + Sync + 'static {
///
/// Implementors should return a default value when no inputs are provided here.
fn blend(inputs: impl Iterator<Item = BlendInput<Self>>) -> Self;
/// Post-processes the value using resources in the [`World`].
/// Most animatable types do not need to implement this.
fn post_process(&mut self, _world: &World) {}
}
macro_rules! impl_float_animatable {
@ -192,3 +187,159 @@ impl Animatable for Quat {
value
}
}
/// An abstraction over a list of keyframes.
///
/// Using this abstraction instead of `Vec<T>` 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<T>(
dest: &mut T,
keyframes: &(impl GetKeyframe<Output = T> + ?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<T>(p0: &T, d0: &T, d3: &T, p3: &T, t: f32, duration: f32) -> T
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)
}

View file

@ -0,0 +1,591 @@
//! Keyframes of animation clips.
use std::any::TypeId;
use std::fmt::{self, Debug, Formatter};
use bevy_asset::Handle;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::component::Component;
use bevy_ecs::world::{EntityMutExcept, Mut};
use bevy_math::{Quat, Vec3};
use bevy_reflect::{FromReflect, GetTypeRegistration, Reflect, TypePath, Typed};
use bevy_render::mesh::morph::MorphWeights;
use bevy_transform::prelude::Transform;
use crate::graph::AnimationGraph;
use crate::prelude::{Animatable, GetKeyframe};
use crate::{animatable, AnimationEvaluationError, AnimationPlayer, 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::<AnimatablePropertyKeyframes<FontSizeProperty>>(
/// [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
+ GetTypeRegistration
+ Reflect
+ TypePath
+ Typed
+ 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<P>(pub Vec<P::Property>)
where
P: AnimatableProperty;
impl<P> Clone for AnimatablePropertyKeyframes<P>
where
P: AnimatableProperty,
{
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<P> Debug for AnimatablePropertyKeyframes<P>
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<dyn Keyframes>;
/// 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<Mut<'a, Transform>>,
entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle<AnimationGraph>)>,
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<Mut<'a, Transform>>,
entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle<AnimationGraph>)>,
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::<TranslationKeyframes>(
/// [0.0, 1.0],
/// [Vec3::ZERO, Vec3::ONE],
/// ),
/// );
#[derive(Clone, Reflect, Debug, Deref, DerefMut)]
pub struct TranslationKeyframes(pub Vec<Vec3>);
/// 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::<ScaleKeyframes>(
/// [0.0, 1.0],
/// [Vec3::ONE, Vec3::splat(2.0)],
/// ),
/// );
#[derive(Clone, Reflect, Debug, Deref, DerefMut)]
pub struct ScaleKeyframes(pub Vec<Vec3>);
/// 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::<RotationKeyframes>(
/// [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<Quat>);
/// 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<f32>,
}
impl<T> From<T> for TranslationKeyframes
where
T: Into<Vec<Vec3>>,
{
fn from(value: T) -> Self {
Self(value.into())
}
}
impl Keyframes for TranslationKeyframes {
fn clone_value(&self) -> Box<dyn Keyframes> {
Box::new(self.clone())
}
fn apply_single_keyframe<'a>(
&self,
transform: Option<Mut<'a, Transform>>,
_: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle<AnimationGraph>)>,
weight: f32,
) -> Result<(), AnimationEvaluationError> {
let mut component = transform.ok_or_else(|| {
AnimationEvaluationError::ComponentNotPresent(TypeId::of::<Transform>())
})?;
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<Mut<'a, Transform>>,
_: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle<AnimationGraph>)>,
interpolation: Interpolation,
step_start: usize,
time: f32,
weight: f32,
duration: f32,
) -> Result<(), AnimationEvaluationError> {
let mut component = transform.ok_or_else(|| {
AnimationEvaluationError::ComponentNotPresent(TypeId::of::<Transform>())
})?;
animatable::interpolate_keyframes(
&mut component.translation,
&(*self)[..],
interpolation,
step_start,
time,
weight,
duration,
)
}
}
impl<T> From<T> for ScaleKeyframes
where
T: Into<Vec<Vec3>>,
{
fn from(value: T) -> Self {
Self(value.into())
}
}
impl Keyframes for ScaleKeyframes {
fn clone_value(&self) -> Box<dyn Keyframes> {
Box::new(self.clone())
}
fn apply_single_keyframe<'a>(
&self,
transform: Option<Mut<'a, Transform>>,
_: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle<AnimationGraph>)>,
weight: f32,
) -> Result<(), AnimationEvaluationError> {
let mut component = transform.ok_or_else(|| {
AnimationEvaluationError::ComponentNotPresent(TypeId::of::<Transform>())
})?;
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<Mut<'a, Transform>>,
_: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle<AnimationGraph>)>,
interpolation: Interpolation,
step_start: usize,
time: f32,
weight: f32,
duration: f32,
) -> Result<(), AnimationEvaluationError> {
let mut component = transform.ok_or_else(|| {
AnimationEvaluationError::ComponentNotPresent(TypeId::of::<Transform>())
})?;
animatable::interpolate_keyframes(
&mut component.scale,
&(*self)[..],
interpolation,
step_start,
time,
weight,
duration,
)
}
}
impl<T> From<T> for RotationKeyframes
where
T: Into<Vec<Quat>>,
{
fn from(value: T) -> Self {
Self(value.into())
}
}
impl Keyframes for RotationKeyframes {
fn clone_value(&self) -> Box<dyn Keyframes> {
Box::new(self.clone())
}
fn apply_single_keyframe<'a>(
&self,
transform: Option<Mut<'a, Transform>>,
_: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle<AnimationGraph>)>,
weight: f32,
) -> Result<(), AnimationEvaluationError> {
let mut component = transform.ok_or_else(|| {
AnimationEvaluationError::ComponentNotPresent(TypeId::of::<Transform>())
})?;
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<Mut<'a, Transform>>,
_: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle<AnimationGraph>)>,
interpolation: Interpolation,
step_start: usize,
time: f32,
weight: f32,
duration: f32,
) -> Result<(), AnimationEvaluationError> {
let mut component = transform.ok_or_else(|| {
AnimationEvaluationError::ComponentNotPresent(TypeId::of::<Transform>())
})?;
animatable::interpolate_keyframes(
&mut component.rotation,
&(*self)[..],
interpolation,
step_start,
time,
weight,
duration,
)
}
}
impl<P, T> From<T> for AnimatablePropertyKeyframes<P>
where
P: AnimatableProperty,
T: Into<Vec<P::Property>>,
{
fn from(value: T) -> Self {
Self(value.into())
}
}
impl<P> Keyframes for AnimatablePropertyKeyframes<P>
where
P: AnimatableProperty,
{
fn clone_value(&self) -> Box<dyn Keyframes> {
Box::new((*self).clone())
}
fn apply_single_keyframe<'a>(
&self,
_: Option<Mut<'a, Transform>>,
mut entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle<AnimationGraph>)>,
weight: f32,
) -> Result<(), AnimationEvaluationError> {
let mut component = entity.get_mut::<P::Component>().ok_or_else(|| {
AnimationEvaluationError::ComponentNotPresent(TypeId::of::<P::Component>())
})?;
let property = P::get_mut(&mut component)
.ok_or_else(|| AnimationEvaluationError::PropertyNotPresent(TypeId::of::<P>()))?;
let value = self
.first()
.ok_or(AnimationEvaluationError::KeyframeNotPresent(0))?;
<P::Property>::interpolate(property, value, weight);
Ok(())
}
fn apply_tweened_keyframes<'a>(
&self,
_: Option<Mut<'a, Transform>>,
mut entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle<AnimationGraph>)>,
interpolation: Interpolation,
step_start: usize,
time: f32,
weight: f32,
duration: f32,
) -> Result<(), AnimationEvaluationError> {
let mut component = entity.get_mut::<P::Component>().ok_or_else(|| {
AnimationEvaluationError::ComponentNotPresent(TypeId::of::<P::Component>())
})?;
let property = P::get_mut(&mut component)
.ok_or_else(|| AnimationEvaluationError::PropertyNotPresent(TypeId::of::<P>()))?;
animatable::interpolate_keyframes(
property,
self,
interpolation,
step_start,
time,
weight,
duration,
)?;
Ok(())
}
}
impl<A> GetKeyframe for [A]
where
A: Animatable,
{
type Output = A;
fn get_keyframe(&self, index: usize) -> Option<&Self::Output> {
self.get(index)
}
}
impl<P> GetKeyframe for AnimatablePropertyKeyframes<P>
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<dyn Keyframes> {
Box::new(self.clone())
}
fn apply_single_keyframe<'a>(
&self,
_: Option<Mut<'a, Transform>>,
mut entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle<AnimationGraph>)>,
weight: f32,
) -> Result<(), AnimationEvaluationError> {
let mut dest = entity.get_mut::<MorphWeights>().ok_or_else(|| {
AnimationEvaluationError::ComponentNotPresent(TypeId::of::<MorphWeights>())
})?;
// 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<'a, Transform>>,
mut entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle<AnimationGraph>)>,
interpolation: Interpolation,
step_start: usize,
time: f32,
weight: f32,
duration: f32,
) -> Result<(), AnimationEvaluationError> {
let mut dest = entity.get_mut::<MorphWeights>().ok_or_else(|| {
AnimationEvaluationError::ComponentNotPresent(TypeId::of::<MorphWeights>())
})?;
// 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)
}
}

File diff suppressed because it is too large Load diff

View file

@ -1905,6 +1905,7 @@ impl<'w> FilteredEntityRef<'w> {
/// - If `access` takes read access to a component no mutable reference to that
/// component can exist at the same time as the returned [`FilteredEntityMut`]
/// - If `access` takes any access for a component `entity` must have that component.
#[inline]
pub(crate) unsafe fn new(entity: UnsafeEntityCell<'w>, access: Access<ComponentId>) -> Self {
Self { entity, access }
}
@ -2043,6 +2044,7 @@ impl<'w> FilteredEntityRef<'w> {
}
impl<'w> From<FilteredEntityMut<'w>> for FilteredEntityRef<'w> {
#[inline]
fn from(entity_mut: FilteredEntityMut<'w>) -> Self {
// SAFETY:
// - `FilteredEntityMut` guarantees exclusive access to all components in the new `FilteredEntityRef`.
@ -2051,6 +2053,7 @@ impl<'w> From<FilteredEntityMut<'w>> for FilteredEntityRef<'w> {
}
impl<'a> From<&'a FilteredEntityMut<'_>> for FilteredEntityRef<'a> {
#[inline]
fn from(entity_mut: &'a FilteredEntityMut<'_>) -> Self {
// SAFETY:
// - `FilteredEntityMut` guarantees exclusive access to all components in the new `FilteredEntityRef`.
@ -2144,6 +2147,7 @@ impl<'w> FilteredEntityMut<'w> {
/// - If `access` takes write access to a component, no reference to that component
/// may exist at the same time as the returned [`FilteredEntityMut`]
/// - If `access` takes any access for a component `entity` must have that component.
#[inline]
pub(crate) unsafe fn new(entity: UnsafeEntityCell<'w>, access: Access<ComponentId>) -> Self {
Self { entity, access }
}
@ -2156,6 +2160,7 @@ impl<'w> FilteredEntityMut<'w> {
}
/// Gets read-only access to all of the entity's components.
#[inline]
pub fn as_readonly(&self) -> FilteredEntityRef<'_> {
FilteredEntityRef::from(self)
}
@ -2397,6 +2402,13 @@ where
}
}
/// Returns the [ID](Entity) of the current entity.
#[inline]
#[must_use = "Omit the .id() call if you do not need to store the `Entity` identifier."]
pub fn id(&self) -> Entity {
self.entity.id()
}
/// Gets access to the component of type `C` for the current entity. Returns
/// `None` if the component doesn't have a component of that type or if the
/// type is one of the excluded components.
@ -2478,6 +2490,13 @@ where
}
}
/// Returns the [ID](Entity) of the current entity.
#[inline]
#[must_use = "Omit the .id() call if you do not need to store the `Entity` identifier."]
pub fn id(&self) -> Entity {
self.entity.id()
}
/// Returns a new instance with a shorter lifetime.
///
/// This is useful if you have `&mut EntityMutExcept`, but you need

View file

@ -3,6 +3,9 @@ use crate::{
GltfMeshExtras, GltfNode, GltfSceneExtras, GltfSkin,
};
use bevy_animation::prelude::{
Keyframes, MorphWeightsKeyframes, RotationKeyframes, ScaleKeyframes, TranslationKeyframes,
};
#[cfg(feature = "bevy_animation")]
use bevy_animation::{AnimationTarget, AnimationTargetId};
use bevy_asset::{
@ -263,7 +266,7 @@ async fn load_gltf<'a, 'b, 'c>(
#[cfg(feature = "bevy_animation")]
let (animations, named_animations, animation_roots) = {
use bevy_animation::{Interpolation, Keyframes};
use bevy_animation::Interpolation;
use gltf::animation::util::ReadOutputs;
let mut animations = vec![];
let mut named_animations = HashMap::default();
@ -294,16 +297,23 @@ async fn load_gltf<'a, 'b, 'c>(
let keyframes = if let Some(outputs) = reader.read_outputs() {
match outputs {
ReadOutputs::Translations(tr) => {
Keyframes::Translation(tr.map(Vec3::from).collect())
Box::new(TranslationKeyframes(tr.map(Vec3::from).collect()))
as Box<dyn Keyframes>
}
ReadOutputs::Rotations(rots) => Keyframes::Rotation(
ReadOutputs::Rotations(rots) => Box::new(RotationKeyframes(
rots.into_f32().map(bevy_math::Quat::from_array).collect(),
),
))
as Box<dyn Keyframes>,
ReadOutputs::Scales(scale) => {
Keyframes::Scale(scale.map(Vec3::from).collect())
Box::new(ScaleKeyframes(scale.map(Vec3::from).collect()))
as Box<dyn Keyframes>
}
ReadOutputs::MorphTargetWeights(weights) => {
Keyframes::Weights(weights.into_f32().collect())
let weights: Vec<_> = weights.into_f32().collect();
Box::new(MorphWeightsKeyframes {
morph_target_count: weights.len() / keyframe_timestamps.len(),
weights,
}) as Box<dyn Keyframes>
}
}
} else {

View file

@ -144,6 +144,7 @@ pub(crate) fn impl_typed(
quote! {
impl #impl_generics #bevy_reflect_path::Typed for #type_path #ty_generics #where_reflect_clause {
#[inline]
fn type_info() -> &'static #bevy_reflect_path::TypeInfo {
#type_info_cell
}

View file

@ -452,6 +452,7 @@ macro_rules! impl_reflect_for_veclike {
}
impl<T: FromReflect + MaybeTyped + TypePath + GetTypeRegistration> PartialReflect for $ty {
#[inline]
fn get_represented_type_info(&self) -> Option<&'static TypeInfo> {
Some(<Self as Typed>::type_info())
}
@ -460,10 +461,12 @@ macro_rules! impl_reflect_for_veclike {
self
}
#[inline]
fn as_partial_reflect(&self) -> &dyn PartialReflect {
self
}
#[inline]
fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect {
self
}

View file

@ -229,6 +229,7 @@ impl TypeInfo {
}
/// The [`TypeId`] of the underlying type.
#[inline]
pub fn type_id(&self) -> TypeId {
self.ty().id()
}
@ -381,6 +382,7 @@ impl Type {
}
/// Returns the [`TypeId`] of the type.
#[inline]
pub fn id(&self) -> TypeId {
self.type_id
}

View file

@ -279,6 +279,7 @@ impl TypeRegistry {
///
/// If the specified type has not been registered, returns `None`.
///
#[inline]
pub fn get(&self, type_id: TypeId) -> Option<&TypeRegistration> {
self.registrations.get(&type_id)
}

View file

@ -187,6 +187,7 @@ Example | Description
--- | ---
[Animated Fox](../examples/animation/animated_fox.rs) | Plays an animation from a skinned glTF
[Animated Transform](../examples/animation/animated_transform.rs) | Create and play an animation defined by code that operates on the `Transform` component
[Animated UI](../examples/animation/animated_ui.rs) | Shows how to use animation clips to animate UI properties
[Animation Graph](../examples/animation/animation_graph.rs) | Blends multiple animations together with a graph
[Animation Masks](../examples/animation/animation_masks.rs) | Demonstrates animation masks
[Color animation](../examples/animation/color_animation.rs) | Demonstrates how to animate colors using mixing and splines in different color spaces

View file

@ -51,9 +51,9 @@ fn setup(
let planet_animation_target_id = AnimationTargetId::from_name(&planet);
animation.add_curve_to_target(
planet_animation_target_id,
VariableCurve {
keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0, 4.0],
keyframes: Keyframes::Translation(vec![
VariableCurve::linear::<TranslationKeyframes>(
[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),
@ -61,9 +61,8 @@ fn setup(
// in case seamless looping is wanted, the last keyframe should
// be the same as the first one
Vec3::new(1.0, 0.0, 1.0),
]),
interpolation: Interpolation::Linear,
},
],
),
);
// Or it can modify the rotation of the transform.
// To find the entity to modify, the hierarchy will be traversed looking for
@ -72,17 +71,16 @@ fn setup(
AnimationTargetId::from_names([planet.clone(), orbit_controller.clone()].iter());
animation.add_curve_to_target(
orbit_controller_animation_target_id,
VariableCurve {
keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0, 4.0],
keyframes: Keyframes::Rotation(vec![
VariableCurve::linear::<RotationKeyframes>(
[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,
]),
interpolation: Interpolation::Linear,
},
],
),
);
// If a curve in an animation is shorter than the other, it will not repeat
// until all other curves are finished. In that case, another animation should
@ -92,9 +90,9 @@ fn setup(
);
animation.add_curve_to_target(
satellite_animation_target_id,
VariableCurve {
keyframe_timestamps: vec![0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0],
keyframes: Keyframes::Scale(vec![
VariableCurve::linear::<ScaleKeyframes>(
[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),
@ -104,26 +102,24 @@ fn setup(
Vec3::splat(0.8),
Vec3::splat(1.2),
Vec3::splat(0.8),
]),
interpolation: Interpolation::Linear,
},
],
),
);
// There can be more than one curve targeting the same entity path
animation.add_curve_to_target(
AnimationTargetId::from_names(
[planet.clone(), orbit_controller.clone(), satellite.clone()].iter(),
),
VariableCurve {
keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0, 4.0],
keyframes: Keyframes::Rotation(vec![
VariableCurve::linear::<RotationKeyframes>(
[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,
]),
interpolation: Interpolation::Linear,
},
],
),
);
// Create the animation graph

View file

@ -0,0 +1,181 @@
//! Shows how to use animation clips to animate UI properties.
use bevy::{
animation::{AnimationTarget, AnimationTargetId},
prelude::*,
};
// A type that represents the font size of the first text section.
//
// We implement `AnimatableProperty` on this.
#[derive(Reflect)]
struct FontSizeProperty;
// A type that represents the color of the first text section.
//
// We implement `AnimatableProperty` on this.
#[derive(Reflect)]
struct TextColorProperty;
// Holds information about the animation we programmatically create.
struct AnimationInfo {
// The name of the animation target (in this case, the text).
target_name: Name,
// The ID of the animation target, derived from the name.
target_id: AnimationTargetId,
// The animation graph asset.
graph: Handle<AnimationGraph>,
// The index of the node within that graph.
node_index: AnimationNodeIndex,
}
// The entry point.
fn main() {
App::new()
.add_plugins(DefaultPlugins)
// Note that we don't need any systems other than the setup system,
// because Bevy automatically updates animations every frame.
.add_systems(Startup, setup)
.run();
}
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)
}
}
impl AnimatableProperty for TextColorProperty {
type Component = Text;
type Property = Srgba;
fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property> {
match component.sections.get_mut(0)?.style.color {
Color::Srgba(ref mut color) => Some(color),
_ => None,
}
}
}
impl AnimationInfo {
// Programmatically creates the UI animation.
fn create(
animation_graphs: &mut Assets<AnimationGraph>,
animation_clips: &mut Assets<AnimationClip>,
) -> AnimationInfo {
// Create an ID that identifies the text node we're going to animate.
let animation_target_name = Name::new("Text");
let animation_target_id = AnimationTargetId::from_name(&animation_target_name);
// Allocate an animation clip.
let mut animation_clip = AnimationClip::default();
// Create a curve that animates font size.
//
// `VariableCurve::linear` is just a convenience constructor; it's also
// possible to initialize the structure manually.
animation_clip.add_curve_to_target(
animation_target_id,
VariableCurve::linear::<AnimatablePropertyKeyframes<FontSizeProperty>>(
[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],
),
);
// Create a curve that animates font color. Note that this should have
// the same time duration as the previous curve.
animation_clip.add_curve_to_target(
animation_target_id,
VariableCurve::linear::<AnimatablePropertyKeyframes<TextColorProperty>>(
[0.0, 1.0, 2.0, 3.0],
[Srgba::RED, Srgba::GREEN, Srgba::BLUE, Srgba::RED],
),
);
// Save our animation clip as an asset.
let animation_clip_handle = animation_clips.add(animation_clip);
// Create an animation graph with that clip.
let (animation_graph, animation_node_index) =
AnimationGraph::from_clip(animation_clip_handle);
let animation_graph_handle = animation_graphs.add(animation_graph);
AnimationInfo {
target_name: animation_target_name,
target_id: animation_target_id,
graph: animation_graph_handle,
node_index: animation_node_index,
}
}
}
// Creates all the entities in the scene.
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut animation_graphs: ResMut<Assets<AnimationGraph>>,
mut animation_clips: ResMut<Assets<AnimationClip>>,
) {
// Create the animation.
let AnimationInfo {
target_name: animation_target_name,
target_id: animation_target_id,
graph: animation_graph,
node_index: animation_node_index,
} = AnimationInfo::create(&mut animation_graphs, &mut animation_clips);
// Build an animation player that automatically plays the UI animation.
let mut animation_player = AnimationPlayer::default();
animation_player.play(animation_node_index).repeat();
// Add a camera.
commands.spawn(Camera2dBundle::default());
// Build the UI. We have a parent node that covers the whole screen and
// contains the `AnimationPlayer`, as well as a child node that contains the
// text to be animated.
commands
.spawn(NodeBundle {
// Cover the whole screen, and center contents.
style: Style {
position_type: PositionType::Absolute,
top: Val::Px(0.0),
left: Val::Px(0.0),
right: Val::Px(0.0),
bottom: Val::Px(0.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
..default()
})
.insert(animation_player)
.insert(animation_graph)
.with_children(|builder| {
// Build the text node.
let player = builder.parent_entity();
builder
.spawn(
TextBundle::from_section(
"Bevy",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 24.0,
color: Color::Srgba(Srgba::RED),
},
)
.with_text_justify(JustifyText::Center),
)
// Mark as an animation target.
.insert(AnimationTarget {
id: animation_target_id,
player,
})
.insert(animation_target_name);
});
}