diff --git a/Cargo.toml b/Cargo.toml index e913e3ef587b1..fc26f5cf69a36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3488,6 +3488,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" diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml index 767a4e86758b8..ae1e8ee23cdc9 100644 --- a/crates/bevy_animation/Cargo.toml +++ b/crates/bevy_animation/Cargo.toml @@ -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" diff --git a/crates/bevy_animation/src/animatable.rs b/crates/bevy_animation/src/animatable.rs index c51b995b3c20a..1a0cce53bfeec 100644 --- a/crates/bevy_animation/src/animatable.rs +++ b/crates/bevy_animation/src/animatable.rs @@ -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>) -> 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` enables more flexibility in how +/// keyframes are stored. In particular, morph weights use this trait in order +/// to flatten the keyframes for all morph weights into a single vector instead +/// of nesting vectors. +pub(crate) trait GetKeyframe { + /// The type of the property to be animated. + type Output; + /// Retrieves the value of the keyframe at the given index. + fn get_keyframe(&self, index: usize) -> Option<&Self::Output>; +} + +/// Interpolates between keyframes and stores the result in `dest`. +/// +/// This is factored out so that it can be shared between implementations of +/// [`crate::keyframes::Keyframes`]. +pub(crate) fn interpolate_keyframes( + dest: &mut T, + keyframes: &(impl GetKeyframe + ?Sized), + interpolation: Interpolation, + step_start: usize, + time: f32, + weight: f32, + duration: f32, +) -> Result<(), AnimationEvaluationError> +where + T: Animatable + Clone, +{ + let value = match interpolation { + Interpolation::Step => { + let Some(start_keyframe) = keyframes.get_keyframe(step_start) else { + return Err(AnimationEvaluationError::KeyframeNotPresent(step_start)); + }; + (*start_keyframe).clone() + } + + Interpolation::Linear => { + let (Some(start_keyframe), Some(end_keyframe)) = ( + keyframes.get_keyframe(step_start), + keyframes.get_keyframe(step_start + 1), + ) else { + return Err(AnimationEvaluationError::KeyframeNotPresent(step_start + 1)); + }; + + T::interpolate(start_keyframe, end_keyframe, time) + } + + Interpolation::CubicSpline => { + let ( + Some(start_keyframe), + Some(start_tangent_keyframe), + Some(end_tangent_keyframe), + Some(end_keyframe), + ) = ( + keyframes.get_keyframe(step_start * 3 + 1), + keyframes.get_keyframe(step_start * 3 + 2), + keyframes.get_keyframe(step_start * 3 + 3), + keyframes.get_keyframe(step_start * 3 + 4), + ) + else { + return Err(AnimationEvaluationError::KeyframeNotPresent( + step_start * 3 + 4, + )); + }; + + interpolate_with_cubic_bezier( + start_keyframe, + start_tangent_keyframe, + end_tangent_keyframe, + end_keyframe, + time, + duration, + ) + } + }; + + *dest = T::interpolate(dest, &value, weight); + + Ok(()) +} + +/// Evaluates a cubic Bézier curve at a value `t`, given two endpoints and the +/// derivatives at those endpoints. +/// +/// The derivatives are linearly scaled by `duration`. +fn interpolate_with_cubic_bezier(p0: &T, d0: &T, d3: &T, p3: &T, t: f32, duration: f32) -> T +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) +} diff --git a/crates/bevy_animation/src/keyframes.rs b/crates/bevy_animation/src/keyframes.rs new file mode 100644 index 0000000000000..f2694930c0be7 --- /dev/null +++ b/crates/bevy_animation/src/keyframes.rs @@ -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::>( +/// [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

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

Clone for AnimatablePropertyKeyframes

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

Debug for AnimatablePropertyKeyframes

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

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

Keyframes for AnimatablePropertyKeyframes

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

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

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

GetKeyframe for AnimatablePropertyKeyframes

+where + P: AnimatableProperty, +{ + type Output = P::Property; + + fn get_keyframe(&self, index: usize) -> Option<&Self::Output> { + self.get(index) + } +} + +/// Information needed to look up morph weight values in the flattened morph +/// weight keyframes vector. +struct GetMorphWeightKeyframe<'k> { + /// The morph weights keyframe structure that we're animating. + keyframes: &'k MorphWeightsKeyframes, + /// The index of the morph target in that structure. + morph_target_index: usize, +} + +impl Keyframes for MorphWeightsKeyframes { + fn clone_value(&self) -> Box { + Box::new(self.clone()) + } + + fn apply_single_keyframe<'a>( + &self, + _: Option>, + mut entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, + weight: f32, + ) -> Result<(), AnimationEvaluationError> { + let mut dest = entity.get_mut::().ok_or_else(|| { + AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) + })?; + + // TODO: Go 4 weights at a time to make better use of SIMD. + for (morph_target_index, morph_weight) in dest.weights_mut().iter_mut().enumerate() { + *morph_weight = + f32::interpolate(morph_weight, &self.weights[morph_target_index], weight); + } + + Ok(()) + } + + fn apply_tweened_keyframes<'a>( + &self, + _: Option>, + mut entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, + interpolation: Interpolation, + step_start: usize, + time: f32, + weight: f32, + duration: f32, + ) -> Result<(), AnimationEvaluationError> { + let mut dest = entity.get_mut::().ok_or_else(|| { + AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) + })?; + + // TODO: Go 4 weights at a time to make better use of SIMD. + for (morph_target_index, morph_weight) in dest.weights_mut().iter_mut().enumerate() { + animatable::interpolate_keyframes( + morph_weight, + &GetMorphWeightKeyframe { + keyframes: self, + morph_target_index, + }, + interpolation, + step_start, + time, + weight, + duration, + )?; + } + + Ok(()) + } +} + +impl GetKeyframe for GetMorphWeightKeyframe<'_> { + type Output = f32; + + fn get_keyframe(&self, keyframe_index: usize) -> Option<&Self::Output> { + self.keyframes + .weights + .as_slice() + .get(keyframe_index * self.keyframes.morph_target_count + self.morph_target_index) + } +} diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index c8f521fdbc5b4..837accc6fa02c 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -9,28 +9,39 @@ pub mod animatable; pub mod graph; +pub mod keyframes; pub mod transition; mod util; +use std::any::{Any, TypeId}; use std::cell::RefCell; use std::collections::BTreeMap; +use std::fmt::Debug; use std::hash::{Hash, Hasher}; use std::iter; -use std::ops::{Add, Mul}; use bevy_app::{App, Plugin, PostUpdate}; use bevy_asset::{Asset, AssetApp, Assets, Handle}; use bevy_core::Name; -use bevy_ecs::{entity::MapEntities, prelude::*, reflect::ReflectMapEntities}; -use bevy_math::{FloatExt, FloatPow, Quat, Vec3}; -use bevy_reflect::std_traits::ReflectDefault; -use bevy_reflect::Reflect; -use bevy_render::mesh::morph::MorphWeights; +use bevy_ecs::entity::MapEntities; +use bevy_ecs::prelude::*; +use bevy_ecs::reflect::ReflectMapEntities; +use bevy_ecs::world::EntityMutExcept; +use bevy_math::FloatExt; +use bevy_reflect::utility::NonGenericTypeInfoCell; +use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_reflect::{ + ApplyError, DynamicStruct, FieldIter, FromReflect, FromType, GetTypeRegistration, NamedField, + PartialReflect, ReflectFromPtr, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, Struct, + StructInfo, TypeInfo, TypePath, TypeRegistration, Typed, +}; use bevy_time::Time; -use bevy_transform::{prelude::Transform, TransformSystem}; +use bevy_transform::prelude::Transform; +use bevy_transform::TransformSystem; +use bevy_ui::UiSystem; +use bevy_utils::hashbrown::HashMap; use bevy_utils::{ - hashbrown::HashMap, - tracing::{error, trace}, + tracing::{trace, warn}, NoOpHash, }; use fixedbitset::FixedBitSet; @@ -46,13 +57,14 @@ use uuid::Uuid; pub mod prelude { #[doc(hidden)] pub use crate::{ - animatable::*, graph::*, transition::*, AnimationClip, AnimationPlayer, AnimationPlugin, - Interpolation, Keyframes, VariableCurve, + animatable::*, graph::*, keyframes::*, transition::*, AnimationClip, AnimationPlayer, + AnimationPlugin, Interpolation, VariableCurve, }; } use crate::{ graph::{AnimationGraph, AnimationGraphAssetLoader, AnimationNodeIndex}, + keyframes::Keyframes, transition::{advance_transitions, expire_completed_transitions, AnimationTransitions}, }; @@ -61,46 +73,11 @@ use crate::{ /// [UUID namespace]: https://en.wikipedia.org/wiki/Universally_unique_identifier#Versions_3_and_5_(namespace_name-based) pub static ANIMATION_TARGET_NAMESPACE: Uuid = Uuid::from_u128(0x3179f519d9274ff2b5966fd077023911); -/// List of keyframes for one of the attribute of a [`Transform`]. -#[derive(Reflect, Clone, Debug)] -pub enum Keyframes { - /// Keyframes for rotation. - Rotation(Vec), - /// Keyframes for translation. - Translation(Vec), - /// Keyframes for scale. - Scale(Vec), - /// Keyframes for morph target weights. - /// - /// Note that in `.0`, each contiguous `target_count` values is a single - /// keyframe representing the weight values at given keyframe. - /// - /// This follows the [glTF design]. - /// - /// [glTF design]: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#animations - Weights(Vec), -} - -impl Keyframes { - /// Returns the number of keyframes. - pub fn len(&self) -> usize { - match self { - Keyframes::Weights(vec) => vec.len(), - Keyframes::Translation(vec) | Keyframes::Scale(vec) => vec.len(), - Keyframes::Rotation(vec) => vec.len(), - } - } - - /// Returns true if the number of keyframes is zero. - pub fn is_empty(&self) -> bool { - self.len() == 0 - } -} - -/// Describes how an attribute of a [`Transform`] or [`MorphWeights`] should be animated. +/// Describes how an attribute of a [`Transform`] or +/// [`bevy_render::mesh::morph::MorphWeights`] should be animated. /// /// `keyframe_timestamps` and `keyframes` should have the same length. -#[derive(Reflect, Clone, Debug)] +#[derive(Debug, TypePath)] pub struct VariableCurve { /// Timestamp for each of the keyframes. pub keyframe_timestamps: Vec, @@ -111,12 +88,84 @@ pub struct VariableCurve { /// - for `Interpolation::Step` and `Interpolation::Linear`, each keyframe is a single value /// - for `Interpolation::CubicSpline`, each keyframe is made of three values for `tangent_in`, /// `keyframe_value` and `tangent_out` - pub keyframes: Keyframes, + pub keyframes: Box, /// Interpolation method to use between keyframes. pub interpolation: Interpolation, } +impl Clone for VariableCurve { + fn clone(&self) -> Self { + VariableCurve { + keyframe_timestamps: self.keyframe_timestamps.clone(), + keyframes: Keyframes::clone_value(&*self.keyframes), + interpolation: self.interpolation, + } + } +} + impl VariableCurve { + /// Creates a new curve from timestamps, keyframes, and interpolation type. + /// + /// The two arrays must have the same length. + pub fn new( + keyframe_timestamps: Vec, + keyframes: impl Into, + interpolation: Interpolation, + ) -> VariableCurve + where + K: Keyframes, + { + VariableCurve { + keyframe_timestamps, + keyframes: Box::new(keyframes.into()), + interpolation, + } + } + + /// Creates a new curve from timestamps and keyframes with no interpolation. + /// + /// The two arrays must have the same length. + pub fn step( + keyframe_timestamps: impl Into>, + keyframes: impl Into, + ) -> VariableCurve + where + K: Keyframes, + { + VariableCurve::new(keyframe_timestamps.into(), keyframes, Interpolation::Step) + } + + /// Creates a new curve from timestamps and keyframes with linear + /// interpolation. + /// + /// The two arrays must have the same length. + pub fn linear( + keyframe_timestamps: impl Into>, + keyframes: impl Into, + ) -> VariableCurve + where + K: Keyframes, + { + VariableCurve::new(keyframe_timestamps.into(), keyframes, Interpolation::Linear) + } + + /// Creates a new curve from timestamps and keyframes with no interpolation. + /// + /// The two arrays must have the same length. + pub fn cubic_spline( + keyframe_timestamps: impl Into>, + keyframes: impl Into, + ) -> VariableCurve + where + K: Keyframes, + { + VariableCurve::new( + keyframe_timestamps.into(), + keyframes, + Interpolation::CubicSpline, + ) + } + /// Find the index of the keyframe at or before the current time. /// /// Returns [`None`] if the curve is finished or not yet started. @@ -183,8 +232,223 @@ impl VariableCurve { } } +// We have to implement `PartialReflect` manually because of the embedded +// `Box`, which can't be automatically derived yet. +impl PartialReflect for VariableCurve { + #[inline] + fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { + Some(::type_info()) + } + + #[inline] + fn into_partial_reflect(self: Box) -> Box { + self + } + + #[inline] + fn as_partial_reflect(&self) -> &dyn PartialReflect { + self + } + + #[inline] + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { + self + } + + fn try_into_reflect(self: Box) -> Result, Box> { + Ok(self) + } + + #[inline] + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) + } + + #[inline] + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) + } + + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + if let ReflectRef::Struct(struct_value) = value.reflect_ref() { + for (i, value) in struct_value.iter_fields().enumerate() { + let name = struct_value.name_at(i).unwrap(); + if let Some(v) = self.field_mut(name) { + v.try_apply(value)?; + } + } + } else { + return Err(ApplyError::MismatchedKinds { + from_kind: value.reflect_kind(), + to_kind: ReflectKind::Struct, + }); + } + Ok(()) + } + + fn reflect_ref(&self) -> ReflectRef { + ReflectRef::Struct(self) + } + + fn reflect_mut(&mut self) -> ReflectMut { + ReflectMut::Struct(self) + } + + fn reflect_owned(self: Box) -> ReflectOwned { + ReflectOwned::Struct(self) + } + + fn clone_value(&self) -> Box { + Box::new((*self).clone()) + } +} + +// We have to implement `Reflect` manually because of the embedded `Box`, which can't be automatically derived yet. +impl Reflect for VariableCurve { + #[inline] + fn into_any(self: Box) -> Box { + self + } + + #[inline] + fn as_any(&self) -> &dyn Any { + self + } + + #[inline] + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + #[inline] + fn into_reflect(self: Box) -> Box { + self + } + + #[inline] + fn as_reflect(&self) -> &dyn Reflect { + self + } + + #[inline] + fn as_reflect_mut(&mut self) -> &mut dyn Reflect { + self + } + + #[inline] + fn set(&mut self, value: Box) -> Result<(), Box> { + *self = value.take()?; + Ok(()) + } +} + +// We have to implement `Struct` manually because of the embedded `Box`, which can't be automatically derived yet. +impl Struct for VariableCurve { + fn field(&self, name: &str) -> Option<&dyn PartialReflect> { + match name { + "keyframe_timestamps" => Some(&self.keyframe_timestamps), + "keyframes" => Some(self.keyframes.as_partial_reflect()), + "interpolation" => Some(&self.interpolation), + _ => None, + } + } + + fn field_mut(&mut self, name: &str) -> Option<&mut dyn PartialReflect> { + match name { + "keyframe_timestamps" => Some(&mut self.keyframe_timestamps), + "keyframes" => Some(self.keyframes.as_partial_reflect_mut()), + "interpolation" => Some(&mut self.interpolation), + _ => None, + } + } + + fn field_at(&self, index: usize) -> Option<&dyn PartialReflect> { + match index { + 0 => Some(&self.keyframe_timestamps), + 1 => Some(self.keyframes.as_partial_reflect()), + 2 => Some(&self.interpolation), + _ => None, + } + } + + fn field_at_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { + match index { + 0 => Some(&mut self.keyframe_timestamps), + 1 => Some(self.keyframes.as_partial_reflect_mut()), + 2 => Some(&mut self.interpolation), + _ => None, + } + } + + fn name_at(&self, index: usize) -> Option<&str> { + match index { + 0 => Some("keyframe_timestamps"), + 1 => Some("keyframes"), + 2 => Some("interpolation"), + _ => None, + } + } + + fn field_len(&self) -> usize { + 3 + } + + fn iter_fields(&self) -> FieldIter { + FieldIter::new(self) + } + + fn clone_dynamic(&self) -> DynamicStruct { + DynamicStruct::from_iter([ + ( + "keyframe_timestamps", + Box::new(self.keyframe_timestamps.clone()) as Box, + ), + ("keyframes", PartialReflect::clone_value(&*self.keyframes)), + ( + "interpolation", + Box::new(self.interpolation) as Box, + ), + ]) + } +} + +// We have to implement `FromReflect` manually because of the embedded `Box`, which can't be automatically derived yet. +impl FromReflect for VariableCurve { + fn from_reflect(reflect: &dyn PartialReflect) -> Option { + Some(reflect.try_downcast_ref::()?.clone()) + } +} + +// We have to implement `GetTypeRegistration` manually because of the embedded +// `Box`, which can't be automatically derived yet. +impl GetTypeRegistration for VariableCurve { + fn get_type_registration() -> TypeRegistration { + let mut registration = TypeRegistration::of::(); + registration.insert::(FromType::::from_type()); + registration + } +} + +// We have to implement `Typed` manually because of the embedded `Box`, which can't be automatically derived yet. +impl Typed for VariableCurve { + fn type_info() -> &'static TypeInfo { + static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); + CELL.get_or_set(|| { + TypeInfo::Struct(StructInfo::new::(&[ + NamedField::new::>("keyframe_timestamps"), + NamedField::new::<()>("keyframes"), + NamedField::new::("interpolation"), + ])) + }) + } +} + /// Interpolation method to use between keyframes. -#[derive(Reflect, Clone, Debug)] +#[derive(Reflect, Clone, Copy, Debug)] pub enum Interpolation { /// Linear interpolation between the two closest keyframes. Linear, @@ -348,6 +612,29 @@ pub enum RepeatAnimation { Forever, } +/// Why Bevy failed to evaluate an animation. +#[derive(Clone, Debug)] +pub enum AnimationEvaluationError { + /// The `keyframes` array is too small. + /// + /// For curves with `Interpolation::Step` or `Interpolation::Linear`, the + /// `keyframes` array must have at least as many elements as keyframe + /// timestamps. For curves with `Interpolation::CubicBezier`, the + /// `keyframes` array must have at least 3× the number of elements as + /// keyframe timestamps, in order to account for the tangents. + KeyframeNotPresent(usize), + + /// The component to be animated isn't present on the animation target. + /// + /// To fix this error, make sure the entity to be animated contains all + /// components that have animation curves. + ComponentNotPresent(TypeId), + + /// The component to be animated was present, but the property on the + /// component wasn't present. + PropertyNotPresent(TypeId), +} + /// An animation that an [`AnimationPlayer`] is currently either playing or was /// playing, but is presently paused. /// @@ -564,16 +851,6 @@ impl Clone for AnimationPlayer { } } -/// The components that we might need to read or write during animation of each -/// animation target. -struct AnimationTargetContext<'a> { - entity: Entity, - target: &'a AnimationTarget, - name: Option<&'a Name>, - transform: Option>, - morph_weights: Option>, -} - /// Information needed during the traversal of the animation graph in /// [`advance_animations`]. #[derive(Default)] @@ -597,15 +874,6 @@ struct EvaluatedAnimationGraphNode { mask: AnimationMask, } -thread_local! { - /// A cached per-thread copy of the graph evaluator. - /// - /// Caching the evaluator lets us save allocation traffic from frame to - /// frame. - static ANIMATION_GRAPH_EVALUATOR: RefCell = - RefCell::new(AnimationGraphEvaluator::default()); -} - impl AnimationPlayer { /// Start playing an animation, restarting it if necessary. pub fn start(&mut self, animation: AnimationNodeIndex) -> &mut ActiveAnimation { @@ -826,55 +1094,51 @@ pub fn advance_animations( } /// A system that modifies animation targets (e.g. bones in a skinned mesh) -/// according to the currently-playing animation. +/// according to the currently-playing animations. pub fn animate_targets( clips: Res>, graphs: Res>, players: Query<(&AnimationPlayer, &Handle)>, mut targets: Query<( - Entity, - &AnimationTarget, - Option<&Name>, - AnyOf<(&mut Transform, &mut MorphWeights)>, + Option<&mut Transform>, + EntityMutExcept<(Transform, AnimationPlayer, Handle)>, )>, ) { - // We use two queries here: one read-only query for animation players and - // one read-write query for animation targets (e.g. bones). The - // `AnimationPlayer` query is read-only shared memory accessible from all - // animation targets, which are evaluated in parallel. - - // Iterate over all animation targets in parallel. + // Evaluate all animation targets in parallel. targets .par_iter_mut() - .for_each(|(id, target, name, (transform, morph_weights))| { - let Ok((animation_player, animation_graph_handle)) = players.get(target.player) else { - trace!( - "Either an animation player {:?} or a graph was missing for the target \ - entity {:?} ({:?}); no animations will play this frame", - target.player, - id, - name, - ); + .for_each(|(mut transform, mut entity_mut)| { + let Some(&AnimationTarget { + id: target_id, + player: player_id, + }) = entity_mut.get::() + else { return; }; + let (animation_player, animation_graph_id) = + if let Ok((player, graph_handle)) = players.get(player_id) { + (player, graph_handle.id()) + } else { + trace!( + "Either an animation player {:?} or a graph was missing for the target \ + entity {:?} ({:?}); no animations will play this frame", + player_id, + entity_mut.id(), + entity_mut.get::(), + ); + return; + }; + // The graph might not have loaded yet. Safely bail. - let Some(animation_graph) = graphs.get(animation_graph_handle) else { + let Some(animation_graph) = graphs.get(animation_graph_id) else { return; }; - let mut target_context = AnimationTargetContext { - entity: id, - target, - name, - transform, - morph_weights, - }; - // Determine which mask groups this animation target belongs to. let target_mask = animation_graph .mask_groups - .get(&target.id) + .get(&target_id) .cloned() .unwrap_or_default(); @@ -914,307 +1178,53 @@ pub fn animate_targets( continue; }; - let Some(curves) = clip.curves_for_target(target_context.target.id) else { + let Some(curves) = clip.curves_for_target(target_id) else { continue; }; let weight = active_animation.computed_weight; total_weight += weight; - target_context.apply(curves, weight / total_weight, active_animation.seek_time); - } - }); -} - -impl AnimationTargetContext<'_> { - /// Applies a clip to a single animation target according to the - /// [`AnimationTargetContext`]. - fn apply(&mut self, curves: &[VariableCurve], weight: f32, seek_time: f32) { - for curve in curves { - // Some curves have only one keyframe used to set a transform - if curve.keyframe_timestamps.len() == 1 { - self.apply_single_keyframe(curve, weight); - continue; - } - - // Find the best keyframe to interpolate from - let step_start = curve.find_interpolation_start_keyframe(seek_time); - - let timestamp_start = curve.keyframe_timestamps[step_start]; - let timestamp_end = curve.keyframe_timestamps[step_start + 1]; - // Compute how far we are through the keyframe, normalized to [0, 1] - let lerp = f32::inverse_lerp(timestamp_start, timestamp_end, seek_time).clamp(0.0, 1.0); - - self.apply_tweened_keyframe( - curve, - step_start, - lerp, - weight, - timestamp_end - timestamp_start, - ); - } - } - - fn apply_single_keyframe(&mut self, curve: &VariableCurve, weight: f32) { - match &curve.keyframes { - Keyframes::Rotation(keyframes) => { - if let Some(ref mut transform) = self.transform { - transform.rotation = transform.rotation.slerp(keyframes[0], weight); - } - } - - Keyframes::Translation(keyframes) => { - if let Some(ref mut transform) = self.transform { - transform.translation = transform.translation.lerp(keyframes[0], weight); - } - } - - Keyframes::Scale(keyframes) => { - if let Some(ref mut transform) = self.transform { - transform.scale = transform.scale.lerp(keyframes[0], weight); - } - } - - Keyframes::Weights(keyframes) => { - let Some(ref mut morphs) = self.morph_weights else { - error!( - "Tried to animate morphs on {:?} ({:?}), but no `MorphWeights` was found", - self.entity, self.name, - ); - return; - }; - - let target_count = morphs.weights().len(); - lerp_morph_weights( - morphs.weights_mut(), - get_keyframe(target_count, keyframes, 0).iter().copied(), - weight, - ); - } - } - } - - fn apply_tweened_keyframe( - &mut self, - curve: &VariableCurve, - step_start: usize, - lerp: f32, - weight: f32, - duration: f32, - ) { - match (&curve.interpolation, &curve.keyframes) { - (Interpolation::Step, Keyframes::Rotation(keyframes)) => { - if let Some(ref mut transform) = self.transform { - transform.rotation = transform.rotation.slerp(keyframes[step_start], weight); - } - } - - (Interpolation::Linear, Keyframes::Rotation(keyframes)) => { - let Some(ref mut transform) = self.transform else { - return; - }; - - let rot_start = keyframes[step_start]; - let rot_end = keyframes[step_start + 1]; - - // Rotations are using a spherical linear interpolation - let rot = rot_start.slerp(rot_end, lerp); - transform.rotation = transform.rotation.slerp(rot, weight); - } - - (Interpolation::CubicSpline, Keyframes::Rotation(keyframes)) => { - let Some(ref mut transform) = self.transform else { - return; - }; - - let value_start = keyframes[step_start * 3 + 1]; - let tangent_out_start = keyframes[step_start * 3 + 2]; - let tangent_in_end = keyframes[(step_start + 1) * 3]; - let value_end = keyframes[(step_start + 1) * 3 + 1]; - let result = cubic_spline_interpolation( - value_start, - tangent_out_start, - tangent_in_end, - value_end, - lerp, - duration, - ); - transform.rotation = transform.rotation.slerp(result.normalize(), weight); - } - - (Interpolation::Step, Keyframes::Translation(keyframes)) => { - if let Some(ref mut transform) = self.transform { - transform.translation = - transform.translation.lerp(keyframes[step_start], weight); - } - } - - (Interpolation::Linear, Keyframes::Translation(keyframes)) => { - let Some(ref mut transform) = self.transform else { - return; - }; - - let translation_start = keyframes[step_start]; - let translation_end = keyframes[step_start + 1]; - let result = translation_start.lerp(translation_end, lerp); - transform.translation = transform.translation.lerp(result, weight); - } - - (Interpolation::CubicSpline, Keyframes::Translation(keyframes)) => { - let Some(ref mut transform) = self.transform else { - return; - }; + let weight = weight / total_weight; + let seek_time = active_animation.seek_time; + + for curve in curves { + // Some curves have only one keyframe used to set a transform + if curve.keyframe_timestamps.len() == 1 { + if let Err(err) = curve.keyframes.apply_single_keyframe( + transform.as_mut().map(|transform| transform.reborrow()), + entity_mut.reborrow(), + weight, + ) { + warn!("Animation application failed: {:?}", err); + } - let value_start = keyframes[step_start * 3 + 1]; - let tangent_out_start = keyframes[step_start * 3 + 2]; - let tangent_in_end = keyframes[(step_start + 1) * 3]; - let value_end = keyframes[(step_start + 1) * 3 + 1]; - let result = cubic_spline_interpolation( - value_start, - tangent_out_start, - tangent_in_end, - value_end, - lerp, - duration, - ); - transform.translation = transform.translation.lerp(result, weight); - } + continue; + } - (Interpolation::Step, Keyframes::Scale(keyframes)) => { - if let Some(ref mut transform) = self.transform { - transform.scale = transform.scale.lerp(keyframes[step_start], weight); + // Find the best keyframe to interpolate from + let step_start = curve.find_interpolation_start_keyframe(seek_time); + + let timestamp_start = curve.keyframe_timestamps[step_start]; + let timestamp_end = curve.keyframe_timestamps[step_start + 1]; + // Compute how far we are through the keyframe, normalized to [0, 1] + let lerp = f32::inverse_lerp(timestamp_start, timestamp_end, seek_time) + .clamp(0.0, 1.0); + + if let Err(err) = curve.keyframes.apply_tweened_keyframes( + transform.as_mut().map(|transform| transform.reborrow()), + entity_mut.reborrow(), + curve.interpolation, + step_start, + lerp, + weight, + timestamp_end - timestamp_start, + ) { + warn!("Animation application failed: {:?}", err); + } } } - - (Interpolation::Linear, Keyframes::Scale(keyframes)) => { - let Some(ref mut transform) = self.transform else { - return; - }; - - let scale_start = keyframes[step_start]; - let scale_end = keyframes[step_start + 1]; - let result = scale_start.lerp(scale_end, lerp); - transform.scale = transform.scale.lerp(result, weight); - } - - (Interpolation::CubicSpline, Keyframes::Scale(keyframes)) => { - let Some(ref mut transform) = self.transform else { - return; - }; - - let value_start = keyframes[step_start * 3 + 1]; - let tangent_out_start = keyframes[step_start * 3 + 2]; - let tangent_in_end = keyframes[(step_start + 1) * 3]; - let value_end = keyframes[(step_start + 1) * 3 + 1]; - let result = cubic_spline_interpolation( - value_start, - tangent_out_start, - tangent_in_end, - value_end, - lerp, - duration, - ); - transform.scale = transform.scale.lerp(result, weight); - } - - (Interpolation::Step, Keyframes::Weights(keyframes)) => { - let Some(ref mut morphs) = self.morph_weights else { - return; - }; - - let target_count = morphs.weights().len(); - let morph_start = get_keyframe(target_count, keyframes, step_start); - lerp_morph_weights(morphs.weights_mut(), morph_start.iter().copied(), weight); - } - - (Interpolation::Linear, Keyframes::Weights(keyframes)) => { - let Some(ref mut morphs) = self.morph_weights else { - return; - }; - - let target_count = morphs.weights().len(); - let morph_start = get_keyframe(target_count, keyframes, step_start); - let morph_end = get_keyframe(target_count, keyframes, step_start + 1); - let result = morph_start - .iter() - .zip(morph_end) - .map(|(a, b)| a.lerp(*b, lerp)); - lerp_morph_weights(morphs.weights_mut(), result, weight); - } - - (Interpolation::CubicSpline, Keyframes::Weights(keyframes)) => { - let Some(ref mut morphs) = self.morph_weights else { - return; - }; - - let target_count = morphs.weights().len(); - let morph_start = get_keyframe(target_count, keyframes, step_start * 3 + 1); - let tangents_out_start = get_keyframe(target_count, keyframes, step_start * 3 + 2); - let tangents_in_end = get_keyframe(target_count, keyframes, (step_start + 1) * 3); - let morph_end = get_keyframe(target_count, keyframes, (step_start + 1) * 3 + 1); - let result = morph_start - .iter() - .zip(tangents_out_start) - .zip(tangents_in_end) - .zip(morph_end) - .map( - |(((&value_start, &tangent_out_start), &tangent_in_end), &value_end)| { - cubic_spline_interpolation( - value_start, - tangent_out_start, - tangent_in_end, - value_end, - lerp, - duration, - ) - }, - ); - lerp_morph_weights(morphs.weights_mut(), result, weight); - } - } - } -} - -/// Update `weights` based on weights in `keyframe` with a linear interpolation -/// on `key_lerp`. -fn lerp_morph_weights(weights: &mut [f32], keyframe: impl Iterator, key_lerp: f32) { - let zipped = weights.iter_mut().zip(keyframe); - for (morph_weight, keyframe) in zipped { - *morph_weight = morph_weight.lerp(keyframe, key_lerp); - } -} - -/// Extract a keyframe from a list of keyframes by index. -/// -/// # Panics -/// -/// When `key_index * target_count` is larger than `keyframes` -/// -/// This happens when `keyframes` is not formatted as described in -/// [`Keyframes::Weights`]. A possible cause is [`AnimationClip`] not being -/// meant to be used for the [`MorphWeights`] of the entity it's being applied to. -fn get_keyframe(target_count: usize, keyframes: &[f32], key_index: usize) -> &[f32] { - let start = target_count * key_index; - let end = target_count * (key_index + 1); - &keyframes[start..end] -} - -/// Helper function for cubic spline interpolation. -fn cubic_spline_interpolation( - value_start: T, - tangent_out_start: T, - tangent_in_end: T, - value_end: T, - lerp: f32, - step_duration: f32, -) -> T -where - T: Mul + Add, -{ - value_start * (2.0 * lerp.cubed() - 3.0 * lerp.squared() + 1.0) - + tangent_out_start * (step_duration) * (lerp.cubed() - 2.0 * lerp.squared() + lerp) - + value_end * (-2.0 * lerp.cubed() + 3.0 * lerp.squared()) - + tangent_in_end * step_duration * (lerp.cubed() - lerp.squared()) + }); } /// Adds animation support to an app @@ -1237,11 +1247,20 @@ impl Plugin for AnimationPlugin { ( advance_transitions, advance_animations, - animate_targets.after(bevy_render::mesh::morph::inherit_weights), + // TODO: `animate_targets` can animate anything, so + // ambiguity testing currently considers it ambiguous with + // every other system in `PostUpdate`. We may want to move + // it to its own system set after `Update` but before + // `PostUpdate`. For now, we just disable ambiguity testing + // for this system. + animate_targets + .after(bevy_render::mesh::morph::inherit_weights) + .ambiguous_with_all(), expire_completed_transitions, ) .chain() - .before(TransformSystem::TransformPropagate), + .before(TransformSystem::TransformPropagate) + .before(UiSystem::Prepare), ); } } @@ -1296,10 +1315,11 @@ impl AnimationGraphEvaluator { #[cfg(test)] mod tests { - use crate::VariableCurve; + use crate::{prelude::TranslationKeyframes, VariableCurve}; use bevy_math::Vec3; - fn test_variable_curve() -> VariableCurve { + // Returns the curve and the keyframe count. + fn test_variable_curve() -> (VariableCurve, usize) { let keyframe_timestamps = vec![1.0, 2.0, 3.0, 4.0]; let keyframes = vec![ Vec3::ONE * 0.0, @@ -1309,13 +1329,14 @@ mod tests { ]; let interpolation = crate::Interpolation::Linear; - let variable_curve = VariableCurve { + assert_eq!(keyframe_timestamps.len(), keyframes.len()); + let keyframe_count = keyframes.len(); + + let variable_curve = VariableCurve::new::( keyframe_timestamps, - keyframes: crate::Keyframes::Translation(keyframes), + keyframes, interpolation, - }; - - assert!(variable_curve.keyframe_timestamps.len() == variable_curve.keyframes.len()); + ); // f32 doesn't impl Ord so we can't easily sort it let mut maybe_last_timestamp = None; @@ -1328,12 +1349,12 @@ mod tests { maybe_last_timestamp = Some(current_timestamp); } - variable_curve + (variable_curve, keyframe_count) } #[test] fn find_current_keyframe_is_in_bounds() { - let curve = test_variable_curve(); + let curve = test_variable_curve().0; let min_time = *curve.keyframe_timestamps.first().unwrap(); // We will always get none at times at or past the second last keyframe let second_last_keyframe = curve.keyframe_timestamps.len() - 2; @@ -1364,7 +1385,7 @@ mod tests { #[test] fn find_current_keyframe_returns_none_on_unstarted_animations() { - let curve = test_variable_curve(); + let curve = test_variable_curve().0; let min_time = *curve.keyframe_timestamps.first().unwrap(); let seek_time = 0.0; assert!(seek_time < min_time); @@ -1378,7 +1399,7 @@ mod tests { #[test] fn find_current_keyframe_returns_none_on_finished_animation() { - let curve = test_variable_curve(); + let curve = test_variable_curve().0; let max_time = *curve.keyframe_timestamps.last().unwrap(); assert!(max_time < f32::INFINITY); @@ -1391,7 +1412,7 @@ mod tests { #[test] fn second_last_keyframe_is_found_correctly() { - let curve = test_variable_curve(); + let curve = test_variable_curve().0; // Exact time match let second_last_keyframe = curve.keyframe_timestamps.len() - 2; @@ -1410,8 +1431,8 @@ mod tests { #[test] fn exact_keyframe_matches_are_found_correctly() { - let curve = test_variable_curve(); - let second_last_keyframe = curve.keyframes.len() - 2; + let (curve, keyframe_count) = test_variable_curve(); + let second_last_keyframe = keyframe_count - 2; for i in 0..=second_last_keyframe { let seek_time = curve.keyframe_timestamps[i]; @@ -1423,9 +1444,8 @@ mod tests { #[test] fn exact_and_inexact_keyframes_correspond() { - let curve = test_variable_curve(); - - let second_last_keyframe = curve.keyframes.len() - 2; + let (curve, keyframe_count) = test_variable_curve(); + let second_last_keyframe = keyframe_count - 2; for i in 0..=second_last_keyframe { let seek_time = curve.keyframe_timestamps[i]; diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 91d021b1fac9f..c91885469c766 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -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) -> Self { Self { entity, access } } @@ -2043,6 +2044,7 @@ impl<'w> FilteredEntityRef<'w> { } impl<'w> From> 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> 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) -> 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 diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index 88a1964370568..d36fc4a39a97c 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -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 } - ReadOutputs::Rotations(rots) => Keyframes::Rotation( + ReadOutputs::Rotations(rots) => Box::new(RotationKeyframes( rots.into_f32().map(bevy_math::Quat::from_array).collect(), - ), + )) + as Box, ReadOutputs::Scales(scale) => { - Keyframes::Scale(scale.map(Vec3::from).collect()) + Box::new(ScaleKeyframes(scale.map(Vec3::from).collect())) + as Box } 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 } } } else { diff --git a/crates/bevy_reflect/derive/src/impls/typed.rs b/crates/bevy_reflect/derive/src/impls/typed.rs index 17ea342107e96..ee8046f68247a 100644 --- a/crates/bevy_reflect/derive/src/impls/typed.rs +++ b/crates/bevy_reflect/derive/src/impls/typed.rs @@ -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 } diff --git a/crates/bevy_reflect/src/impls/std.rs b/crates/bevy_reflect/src/impls/std.rs index 85f63be654340..26a32a9ae1f9c 100644 --- a/crates/bevy_reflect/src/impls/std.rs +++ b/crates/bevy_reflect/src/impls/std.rs @@ -452,6 +452,7 @@ macro_rules! impl_reflect_for_veclike { } impl PartialReflect for $ty { + #[inline] fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { Some(::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 } diff --git a/crates/bevy_reflect/src/type_info.rs b/crates/bevy_reflect/src/type_info.rs index fdd59396daea8..d1ff268123ada 100644 --- a/crates/bevy_reflect/src/type_info.rs +++ b/crates/bevy_reflect/src/type_info.rs @@ -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 } diff --git a/crates/bevy_reflect/src/type_registry.rs b/crates/bevy_reflect/src/type_registry.rs index 4f39d17c5a097..defd5748a144a 100644 --- a/crates/bevy_reflect/src/type_registry.rs +++ b/crates/bevy_reflect/src/type_registry.rs @@ -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) } diff --git a/examples/README.md b/examples/README.md index fb03438de3c9e..147d5ee3c659b 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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 diff --git a/examples/animation/animated_transform.rs b/examples/animation/animated_transform.rs index d85adf1028eb7..7fae355b9becb 100644 --- a/examples/animation/animated_transform.rs +++ b/examples/animation/animated_transform.rs @@ -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::( + [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::( + [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::( + [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::( + [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 diff --git a/examples/animation/animated_ui.rs b/examples/animation/animated_ui.rs new file mode 100644 index 0000000000000..ad55bac900358 --- /dev/null +++ b/examples/animation/animated_ui.rs @@ -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, + // 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, + animation_clips: &mut Assets, + ) -> 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::>( + [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::>( + [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, + mut animation_graphs: ResMut>, + mut animation_clips: ResMut>, +) { + // 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); + }); +}