From d2e6e5c7034569c7d95bd8ca27c5bf0bd762d3e4 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Tue, 10 Sep 2024 19:29:22 -0700 Subject: [PATCH 1/7] Allow animation clips to animate arbitrary properties. 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. --- Cargo.toml | 11 + crates/bevy_animation/Cargo.toml | 4 + crates/bevy_animation/src/animatable.rs | 161 +++- crates/bevy_animation/src/keyframes.rs | 549 +++++++++++++ crates/bevy_animation/src/lib.rs | 766 ++++++++---------- crates/bevy_ecs/src/world/entity_ref.rs | 5 + crates/bevy_gltf/src/loader.rs | 22 +- crates/bevy_reflect/derive/src/impls/typed.rs | 1 + crates/bevy_reflect/src/impls/std.rs | 3 + crates/bevy_reflect/src/type_info.rs | 2 + crates/bevy_reflect/src/type_registry.rs | 1 + examples/README.md | 1 + examples/animation/animated_transform.rs | 16 +- examples/animation/animated_ui.rs | 187 +++++ 14 files changed, 1303 insertions(+), 426 deletions(-) create mode 100644 crates/bevy_animation/src/keyframes.rs create mode 100644 examples/animation/animated_ui.rs diff --git a/Cargo.toml b/Cargo.toml index ddd4b9444812d..d140ef6748ff6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3452,6 +3452,17 @@ description = "Demonstrates animation masks" category = "Animation" wasm = true +[[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..a1770de6a8199 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,157 @@ 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 +/// [`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); + }; + (*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); + }; + + 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); + }; + + 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..8c7eab31a14ca --- /dev/null +++ b/crates/bevy_animation/src/keyframes.rs @@ -0,0 +1,549 @@ +//! Keyframes of animation clips. + +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 an [`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 { +/// keyframe_timestamps: vec![0.0, 1.0], +/// keyframes: Box::new(AnimatablePropertyKeyframes::(vec![ +/// 24.0, 80.0, +/// ])), +/// interpolation: Interpolation::Linear, +/// } +/// ); +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 [`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 [`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 [`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 { +/// keyframe_timestamps: vec![0.0, 1.0], +/// keyframes: Box::new(TranslationKeyframes(vec![Vec3::ZERO, Vec3::ONE])), +/// interpolation: Interpolation::Linear, +/// }, +/// ); +#[derive(Clone, Reflect, Debug, Deref, DerefMut)] +pub struct TranslationKeyframes(pub Vec); + +/// Keyframes for animating [`Transform::scale`]. +/// +/// An example of a [`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 { +/// keyframe_timestamps: vec![0.0, 1.0], +/// keyframes: Box::new(ScaleKeyframes(vec![Vec3::ONE, Vec3::splat(2.0)])), +/// interpolation: Interpolation::Linear, +/// }, +/// ); +#[derive(Clone, Reflect, Debug, Deref, DerefMut)] +pub struct ScaleKeyframes(pub Vec); + +/// Keyframes for animating [`Transform::rotation`]. +/// +/// An example of a [`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 { +/// keyframe_timestamps: vec![0.0, 1.0], +/// keyframes: Box::new(RotationKeyframes(vec![ +/// Quat::from_rotation_x(FRAC_PI_2), +/// Quat::from_rotation_y(FRAC_PI_2), +/// ])), +/// interpolation: Interpolation::Linear, +/// }, +/// ); +#[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 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(AnimationEvaluationError::ComponentNotPresent)?; + let value = self + .first() + .ok_or(AnimationEvaluationError::KeyframeNotPresent)?; + 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(AnimationEvaluationError::ComponentNotPresent)?; + animatable::interpolate_keyframes( + &mut component.translation, + &(*self)[..], + interpolation, + step_start, + time, + weight, + duration, + ) + } +} + +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(AnimationEvaluationError::ComponentNotPresent)?; + let value = self + .first() + .ok_or(AnimationEvaluationError::KeyframeNotPresent)?; + 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(AnimationEvaluationError::ComponentNotPresent)?; + animatable::interpolate_keyframes( + &mut component.scale, + &(*self)[..], + interpolation, + step_start, + time, + weight, + duration, + ) + } +} + +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(AnimationEvaluationError::ComponentNotPresent)?; + let value = self + .first() + .ok_or(AnimationEvaluationError::KeyframeNotPresent)?; + 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(AnimationEvaluationError::ComponentNotPresent)?; + animatable::interpolate_keyframes( + &mut component.rotation, + &(*self)[..], + interpolation, + step_start, + time, + weight, + duration, + ) + } +} + +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(AnimationEvaluationError::ComponentNotPresent)?; + let property = + P::get_mut(&mut component).ok_or(AnimationEvaluationError::PropertyNotPresent)?; + let value = self + .first() + .ok_or(AnimationEvaluationError::KeyframeNotPresent)?; + ::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(AnimationEvaluationError::ComponentNotPresent)?; + let property = + P::get_mut(&mut component).ok_or(AnimationEvaluationError::PropertyNotPresent)?; + 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(AnimationEvaluationError::ComponentNotPresent)?; + + // 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(AnimationEvaluationError::ComponentNotPresent)?; + + // 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..1b65dedc3d945 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; 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,10 @@ 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. /// /// `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,11 +87,21 @@ 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 { /// Find the index of the keyframe at or before the current time. /// @@ -183,8 +169,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 +549,34 @@ 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, + + /// 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, + + /// The component to be animated was present, but the `path` didn't name any + /// reflectable property on the component. + /// + /// This is usually because of a mistyped `ParsedPath`. For example, to + /// alter the color of the first section of text, you should use a path of + /// `sections[0].style.color.0` (correct) instead of, for example, + /// `sections.0.style.color` (incorrect). + PropertyNotPresent, +} + /// An animation that an [`AnimationPlayer`] is currently either playing or was /// playing, but is presently paused. /// @@ -564,16 +793,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 +816,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 +1036,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 xxx ({:?}); no animations will play this frame", + player_id, + //entity_mut.id(), FIXME + 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 +1120,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 @@ -1241,7 +1193,8 @@ impl Plugin for AnimationPlugin { expire_completed_transitions, ) .chain() - .before(TransformSystem::TransformPropagate), + .before(TransformSystem::TransformPropagate) + .before(UiSystem::Prepare), ); } } @@ -1296,10 +1249,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,14 +1263,15 @@ mod tests { ]; let interpolation = crate::Interpolation::Linear; + assert_eq!(keyframe_timestamps.len(), keyframes.len()); + let keyframe_count = keyframes.len(); + let variable_curve = VariableCurve { keyframe_timestamps, - keyframes: crate::Keyframes::Translation(keyframes), + keyframes: Box::new(TranslationKeyframes(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; for current_timestamp in &variable_curve.keyframe_timestamps { @@ -1328,12 +1283,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 +1319,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 +1333,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 +1346,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 +1365,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 +1378,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 e8b2c6f563f1c..79f492a8f75e1 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -1904,6 +1904,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 } } @@ -2042,6 +2043,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`. @@ -2050,6 +2052,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`. @@ -2143,6 +2146,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 } } @@ -2155,6 +2159,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) } 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 d407c1c88e5ab..52703e2e667b8 100644 --- a/crates/bevy_reflect/derive/src/impls/typed.rs +++ b/crates/bevy_reflect/derive/src/impls/typed.rs @@ -145,6 +145,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 fa74e51950570..d8f442165bc23 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 92e03582aa205..adfb3dc904560 100644 --- a/examples/README.md +++ b/examples/README.md @@ -186,6 +186,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..64025cb276e33 100644 --- a/examples/animation/animated_transform.rs +++ b/examples/animation/animated_transform.rs @@ -53,7 +53,7 @@ fn setup( planet_animation_target_id, VariableCurve { keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0, 4.0], - keyframes: Keyframes::Translation(vec![ + keyframes: Box::new(TranslationKeyframes(vec![ 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,7 +61,7 @@ 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, }, ); @@ -74,13 +74,13 @@ fn setup( orbit_controller_animation_target_id, VariableCurve { keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0, 4.0], - keyframes: Keyframes::Rotation(vec![ + keyframes: Box::new(RotationKeyframes(vec![ Quat::IDENTITY, Quat::from_axis_angle(Vec3::Y, PI / 2.), Quat::from_axis_angle(Vec3::Y, PI / 2. * 2.), Quat::from_axis_angle(Vec3::Y, PI / 2. * 3.), Quat::IDENTITY, - ]), + ])), interpolation: Interpolation::Linear, }, ); @@ -94,7 +94,7 @@ fn setup( 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![ + keyframes: Box::new(ScaleKeyframes(vec![ Vec3::splat(0.8), Vec3::splat(1.2), Vec3::splat(0.8), @@ -104,7 +104,7 @@ fn setup( Vec3::splat(0.8), Vec3::splat(1.2), Vec3::splat(0.8), - ]), + ])), interpolation: Interpolation::Linear, }, ); @@ -115,13 +115,13 @@ fn setup( ), VariableCurve { keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0, 4.0], - keyframes: Keyframes::Rotation(vec![ + keyframes: Box::new(RotationKeyframes(vec![ Quat::IDENTITY, Quat::from_axis_angle(Vec3::Y, PI / 2.), Quat::from_axis_angle(Vec3::Y, PI / 2. * 2.), Quat::from_axis_angle(Vec3::Y, PI / 2. * 3.), Quat::IDENTITY, - ]), + ])), interpolation: Interpolation::Linear, }, ); diff --git a/examples/animation/animated_ui.rs b/examples/animation/animated_ui.rs new file mode 100644 index 0000000000000..ecd87c0e00be0 --- /dev/null +++ b/examples/animation/animated_ui.rs @@ -0,0 +1,187 @@ +//! 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. + animation_clip.add_curve_to_target( + animation_target_id, + VariableCurve { + keyframe_timestamps: vec![0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0], + keyframes: Box::new(AnimatablePropertyKeyframes::(vec![ + 24.0, 80.0, 24.0, 80.0, 24.0, 80.0, 24.0, + ])), + interpolation: Interpolation::Linear, + }, + ); + + // 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 { + keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0], + keyframes: Box::new(AnimatablePropertyKeyframes::(vec![ + Srgba::RED, + Srgba::GREEN, + Srgba::BLUE, + Srgba::RED, + ])), + interpolation: Interpolation::Linear, + }, + ); + + // 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); + }); +} From 56265baca881f5177ed2e13d9a1c66fb0d7ee9e5 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Wed, 18 Sep 2024 11:14:58 -0700 Subject: [PATCH 2/7] Add convenience constructors for `VariableCurve` --- crates/bevy_animation/src/keyframes.rs | 75 +++++++++++++++--------- crates/bevy_animation/src/lib.rs | 55 ++++++++++++++++- examples/animation/animated_transform.rs | 33 +++++------ examples/animation/animated_ui.rs | 11 ++-- 4 files changed, 119 insertions(+), 55 deletions(-) diff --git a/crates/bevy_animation/src/keyframes.rs b/crates/bevy_animation/src/keyframes.rs index 8c7eab31a14ca..76905143bcc0e 100644 --- a/crates/bevy_animation/src/keyframes.rs +++ b/crates/bevy_animation/src/keyframes.rs @@ -56,13 +56,10 @@ use crate::{animatable, AnimationEvaluationError, AnimationPlayer, Interpolation /// let mut animation_clip = AnimationClip::default(); /// animation_clip.add_curve_to_target( /// animation_target_id, -/// VariableCurve { -/// keyframe_timestamps: vec![0.0, 1.0], -/// keyframes: Box::new(AnimatablePropertyKeyframes::(vec![ -/// 24.0, 80.0, -/// ])), -/// interpolation: Interpolation::Linear, -/// } +/// VariableCurve::linear::>( +/// vec![0.0, 1.0], +/// vec![24.0, 80.0], +/// ), /// ); pub trait AnimatableProperty: Reflect + TypePath + 'static { /// The type of the component that the property lives on. @@ -200,11 +197,10 @@ pub trait Keyframes: Reflect + Debug + Send + Sync { /// let mut animation_clip = AnimationClip::default(); /// animation_clip.add_curve_to_target( /// animation_target_id, -/// VariableCurve { -/// keyframe_timestamps: vec![0.0, 1.0], -/// keyframes: Box::new(TranslationKeyframes(vec![Vec3::ZERO, Vec3::ONE])), -/// interpolation: Interpolation::Linear, -/// }, +/// VariableCurve::linear::( +/// vec![0.0, 1.0], +/// vec![Vec3::ZERO, Vec3::ONE], +/// ), /// ); #[derive(Clone, Reflect, Debug, Deref, DerefMut)] pub struct TranslationKeyframes(pub Vec); @@ -221,11 +217,10 @@ pub struct TranslationKeyframes(pub Vec); /// let mut animation_clip = AnimationClip::default(); /// animation_clip.add_curve_to_target( /// animation_target_id, -/// VariableCurve { -/// keyframe_timestamps: vec![0.0, 1.0], -/// keyframes: Box::new(ScaleKeyframes(vec![Vec3::ONE, Vec3::splat(2.0)])), -/// interpolation: Interpolation::Linear, -/// }, +/// VariableCurve::linear::( +/// vec![0.0, 1.0], +/// vec![Vec3::ONE, Vec3::splat(2.0)], +/// ), /// ); #[derive(Clone, Reflect, Debug, Deref, DerefMut)] pub struct ScaleKeyframes(pub Vec); @@ -243,14 +238,13 @@ pub struct ScaleKeyframes(pub Vec); /// let mut animation_clip = AnimationClip::default(); /// animation_clip.add_curve_to_target( /// animation_target_id, -/// VariableCurve { -/// keyframe_timestamps: vec![0.0, 1.0], -/// keyframes: Box::new(RotationKeyframes(vec![ +/// VariableCurve::linear::( +/// vec![0.0, 1.0], +/// vec![ /// Quat::from_rotation_x(FRAC_PI_2), /// Quat::from_rotation_y(FRAC_PI_2), -/// ])), -/// interpolation: Interpolation::Linear, -/// }, +/// ], +/// ), /// ); #[derive(Clone, Reflect, Debug, Deref, DerefMut)] pub struct RotationKeyframes(pub Vec); @@ -268,9 +262,15 @@ pub struct MorphWeightsKeyframes { pub weights: Vec, } +impl From> for TranslationKeyframes { + fn from(value: Vec) -> Self { + Self(value) + } +} + impl Keyframes for TranslationKeyframes { fn clone_value(&self) -> Box { - Box::new((*self).clone()) + Box::new(self.clone()) } fn apply_single_keyframe<'a>( @@ -310,9 +310,15 @@ impl Keyframes for TranslationKeyframes { } } +impl From> for ScaleKeyframes { + fn from(value: Vec) -> Self { + Self(value) + } +} + impl Keyframes for ScaleKeyframes { fn clone_value(&self) -> Box { - Box::new((*self).clone()) + Box::new(self.clone()) } fn apply_single_keyframe<'a>( @@ -352,9 +358,15 @@ impl Keyframes for ScaleKeyframes { } } +impl From> for RotationKeyframes { + fn from(value: Vec) -> Self { + Self(value) + } +} + impl Keyframes for RotationKeyframes { fn clone_value(&self) -> Box { - Box::new((*self).clone()) + Box::new(self.clone()) } fn apply_single_keyframe<'a>( @@ -394,6 +406,15 @@ impl Keyframes for RotationKeyframes { } } +impl

From> for AnimatablePropertyKeyframes

+where + P: AnimatableProperty, +{ + fn from(value: Vec) -> Self { + Self(value) + } +} + impl

Keyframes for AnimatablePropertyKeyframes

where P: AnimatableProperty, @@ -481,7 +502,7 @@ struct GetMorphWeightKeyframe<'k> { impl Keyframes for MorphWeightsKeyframes { fn clone_value(&self) -> Box { - Box::new((*self).clone()) + Box::new(self.clone()) } fn apply_single_keyframe<'a>( diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 1b65dedc3d945..bae2c8ee06dce 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -103,6 +103,55 @@ impl Clone for VariableCurve { } 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: Vec, keyframes: impl Into) -> VariableCurve + where + K: Keyframes, + { + VariableCurve::new(keyframe_timestamps, 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: Vec, keyframes: impl Into) -> VariableCurve + where + K: Keyframes, + { + VariableCurve::new(keyframe_timestamps, 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: Vec, keyframes: impl Into) -> VariableCurve + where + K: Keyframes, + { + VariableCurve::new(keyframe_timestamps, 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. @@ -1266,11 +1315,11 @@ mod tests { assert_eq!(keyframe_timestamps.len(), keyframes.len()); let keyframe_count = keyframes.len(); - let variable_curve = VariableCurve { + let variable_curve = VariableCurve::new::( keyframe_timestamps, - keyframes: Box::new(TranslationKeyframes(keyframes)), + keyframes, interpolation, - }; + ); // f32 doesn't impl Ord so we can't easily sort it let mut maybe_last_timestamp = None; diff --git a/examples/animation/animated_transform.rs b/examples/animation/animated_transform.rs index 64025cb276e33..774eaf8e00b66 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: Box::new(TranslationKeyframes(vec![ + VariableCurve::linear::( + vec![0.0, 1.0, 2.0, 3.0, 4.0], + vec![ 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: Box::new(RotationKeyframes(vec![ + VariableCurve::linear::( + vec![0.0, 1.0, 2.0, 3.0, 4.0], + vec![ Quat::IDENTITY, Quat::from_axis_angle(Vec3::Y, PI / 2.), Quat::from_axis_angle(Vec3::Y, PI / 2. * 2.), Quat::from_axis_angle(Vec3::Y, PI / 2. * 3.), Quat::IDENTITY, - ])), - interpolation: Interpolation::Linear, - }, + ], + ), ); // 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: Box::new(ScaleKeyframes(vec![ + VariableCurve::linear::( + vec![0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0], + vec![ Vec3::splat(0.8), Vec3::splat(1.2), Vec3::splat(0.8), @@ -104,9 +102,8 @@ 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( diff --git a/examples/animation/animated_ui.rs b/examples/animation/animated_ui.rs index ecd87c0e00be0..f37b3a1ce9b50 100644 --- a/examples/animation/animated_ui.rs +++ b/examples/animation/animated_ui.rs @@ -78,13 +78,10 @@ impl AnimationInfo { // Create a curve that animates font size. animation_clip.add_curve_to_target( animation_target_id, - VariableCurve { - keyframe_timestamps: vec![0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0], - keyframes: Box::new(AnimatablePropertyKeyframes::(vec![ - 24.0, 80.0, 24.0, 80.0, 24.0, 80.0, 24.0, - ])), - interpolation: Interpolation::Linear, - }, + VariableCurve::linear::>( + vec![0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0], + vec![24.0, 80.0, 24.0, 80.0, 24.0, 80.0, 24.0], + ), ); // Create a curve that animates font color. From dc97fe1f4e50d5d996c276fc318bd8a2fa0c7975 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Wed, 18 Sep 2024 13:30:52 -0700 Subject: [PATCH 3/7] Doc check police --- crates/bevy_animation/src/animatable.rs | 2 +- crates/bevy_animation/src/keyframes.rs | 15 ++++++++------- crates/bevy_animation/src/lib.rs | 3 ++- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/crates/bevy_animation/src/animatable.rs b/crates/bevy_animation/src/animatable.rs index a1770de6a8199..bc5e74d2e3059 100644 --- a/crates/bevy_animation/src/animatable.rs +++ b/crates/bevy_animation/src/animatable.rs @@ -204,7 +204,7 @@ pub(crate) trait GetKeyframe { /// Interpolates between keyframes and stores the result in `dest`. /// /// This is factored out so that it can be shared between implementations of -/// [`Keyframes`]. +/// [`crate::keyframes::Keyframes`]. pub(crate) fn interpolate_keyframes( dest: &mut T, keyframes: &(impl GetKeyframe + ?Sized), diff --git a/crates/bevy_animation/src/keyframes.rs b/crates/bevy_animation/src/keyframes.rs index 76905143bcc0e..a4c58be6c6bae 100644 --- a/crates/bevy_animation/src/keyframes.rs +++ b/crates/bevy_animation/src/keyframes.rs @@ -36,7 +36,7 @@ use crate::{animatable, AnimationEvaluationError, AnimationPlayer, Interpolation /// } /// } /// -/// You can then create an [`AnimationClip`] to animate this property like so: +/// 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}; @@ -83,7 +83,8 @@ pub trait AnimatableProperty: Reflect + TypePath + 'static { fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property>; } -/// Keyframes in a [`VariableCurve`] that animate an [`AnimatableProperty`]. +/// 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 @@ -117,8 +118,8 @@ where } } -/// A low-level trait for use in [`VariableCurve`] that provides fine control -/// over how animations are evaluated. +/// 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 @@ -187,7 +188,7 @@ pub trait Keyframes: Reflect + Debug + Send + Sync { /// Keyframes for animating [`Transform::translation`]. /// -/// An example of a [`AnimationClip`] that animates translation: +/// An example of a [`crate::AnimationClip`] that animates translation: /// /// # use bevy_animation::{AnimationClip, AnimationTargetId, Interpolation}; /// # use bevy_animation::{VariableCurve, prelude::TranslationKeyframes}; @@ -207,7 +208,7 @@ pub struct TranslationKeyframes(pub Vec); /// Keyframes for animating [`Transform::scale`]. /// -/// An example of a [`AnimationClip`] that animates translation: +/// An example of a [`crate::AnimationClip`] that animates translation: /// /// # use bevy_animation::{AnimationClip, AnimationTargetId, Interpolation}; /// # use bevy_animation::{VariableCurve, prelude::ScaleKeyframes}; @@ -227,7 +228,7 @@ pub struct ScaleKeyframes(pub Vec); /// Keyframes for animating [`Transform::rotation`]. /// -/// An example of a [`AnimationClip`] that animates translation: +/// An example of a [`crate::AnimationClip`] that animates translation: /// /// # use bevy_animation::{AnimationClip, AnimationTargetId, Interpolation}; /// # use bevy_animation::{VariableCurve, prelude::RotationKeyframes}; diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index bae2c8ee06dce..a1d23f4c45708 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -73,7 +73,8 @@ use crate::{ /// [UUID namespace]: https://en.wikipedia.org/wiki/Universally_unique_identifier#Versions_3_and_5_(namespace_name-based) pub static ANIMATION_TARGET_NAMESPACE: Uuid = Uuid::from_u128(0x3179f519d9274ff2b5966fd077023911); -/// Describes how an attribute of a [`Transform`] or [`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(Debug, TypePath)] From b6c0fbdf73077a607d9e113d3fe950835dd64ba9 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Wed, 18 Sep 2024 17:29:51 -0700 Subject: [PATCH 4/7] Fix ambiguity testing --- crates/bevy_animation/src/lib.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index a1d23f4c45708..8936fc22b8317 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -1239,7 +1239,15 @@ 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() From 10de24b6f64f79267d53e48138e415f4e2db84b8 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Wed, 18 Sep 2024 17:47:36 -0700 Subject: [PATCH 5/7] Address review comment --- examples/animation/animated_ui.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/animation/animated_ui.rs b/examples/animation/animated_ui.rs index f37b3a1ce9b50..0fee526d0a8a3 100644 --- a/examples/animation/animated_ui.rs +++ b/examples/animation/animated_ui.rs @@ -76,6 +76,9 @@ impl AnimationInfo { 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::>( @@ -84,8 +87,8 @@ impl AnimationInfo { ), ); - // Create a curve that animates font color. - // Note that this should have the same time duration as the previous curve. + // 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 { From b9ce34aa7cfdec381c2a6f92a5a7e224d29cf6e4 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Sat, 21 Sep 2024 23:35:50 -0700 Subject: [PATCH 6/7] Address review comments --- crates/bevy_animation/src/animatable.rs | 8 +- crates/bevy_animation/src/keyframes.rs | 118 +++++++++++++---------- crates/bevy_animation/src/lib.rs | 46 +++++---- crates/bevy_ecs/src/world/entity_ref.rs | 14 +++ examples/animation/animated_transform.rs | 23 +++-- examples/animation/animated_ui.rs | 18 ++-- 6 files changed, 132 insertions(+), 95 deletions(-) diff --git a/crates/bevy_animation/src/animatable.rs b/crates/bevy_animation/src/animatable.rs index bc5e74d2e3059..1a0cce53bfeec 100644 --- a/crates/bevy_animation/src/animatable.rs +++ b/crates/bevy_animation/src/animatable.rs @@ -220,7 +220,7 @@ where let value = match interpolation { Interpolation::Step => { let Some(start_keyframe) = keyframes.get_keyframe(step_start) else { - return Err(AnimationEvaluationError::KeyframeNotPresent); + return Err(AnimationEvaluationError::KeyframeNotPresent(step_start)); }; (*start_keyframe).clone() } @@ -230,7 +230,7 @@ where keyframes.get_keyframe(step_start), keyframes.get_keyframe(step_start + 1), ) else { - return Err(AnimationEvaluationError::KeyframeNotPresent); + return Err(AnimationEvaluationError::KeyframeNotPresent(step_start + 1)); }; T::interpolate(start_keyframe, end_keyframe, time) @@ -249,7 +249,9 @@ where keyframes.get_keyframe(step_start * 3 + 4), ) else { - return Err(AnimationEvaluationError::KeyframeNotPresent); + return Err(AnimationEvaluationError::KeyframeNotPresent( + step_start * 3 + 4, + )); }; interpolate_with_cubic_bezier( diff --git a/crates/bevy_animation/src/keyframes.rs b/crates/bevy_animation/src/keyframes.rs index a4c58be6c6bae..f2694930c0be7 100644 --- a/crates/bevy_animation/src/keyframes.rs +++ b/crates/bevy_animation/src/keyframes.rs @@ -1,5 +1,6 @@ //! Keyframes of animation clips. +use std::any::TypeId; use std::fmt::{self, Debug, Formatter}; use bevy_asset::Handle; @@ -57,8 +58,8 @@ use crate::{animatable, AnimationEvaluationError, AnimationPlayer, Interpolation /// animation_clip.add_curve_to_target( /// animation_target_id, /// VariableCurve::linear::>( -/// vec![0.0, 1.0], -/// vec![24.0, 80.0], +/// [0.0, 1.0], +/// [24.0, 80.0], /// ), /// ); pub trait AnimatableProperty: Reflect + TypePath + 'static { @@ -199,8 +200,8 @@ pub trait Keyframes: Reflect + Debug + Send + Sync { /// animation_clip.add_curve_to_target( /// animation_target_id, /// VariableCurve::linear::( -/// vec![0.0, 1.0], -/// vec![Vec3::ZERO, Vec3::ONE], +/// [0.0, 1.0], +/// [Vec3::ZERO, Vec3::ONE], /// ), /// ); #[derive(Clone, Reflect, Debug, Deref, DerefMut)] @@ -219,8 +220,8 @@ pub struct TranslationKeyframes(pub Vec); /// animation_clip.add_curve_to_target( /// animation_target_id, /// VariableCurve::linear::( -/// vec![0.0, 1.0], -/// vec![Vec3::ONE, Vec3::splat(2.0)], +/// [0.0, 1.0], +/// [Vec3::ONE, Vec3::splat(2.0)], /// ), /// ); #[derive(Clone, Reflect, Debug, Deref, DerefMut)] @@ -240,11 +241,8 @@ pub struct ScaleKeyframes(pub Vec); /// animation_clip.add_curve_to_target( /// animation_target_id, /// VariableCurve::linear::( -/// vec![0.0, 1.0], -/// vec![ -/// Quat::from_rotation_x(FRAC_PI_2), -/// Quat::from_rotation_y(FRAC_PI_2), -/// ], +/// [0.0, 1.0], +/// [Quat::from_rotation_x(FRAC_PI_2), Quat::from_rotation_y(FRAC_PI_2)], /// ), /// ); #[derive(Clone, Reflect, Debug, Deref, DerefMut)] @@ -263,9 +261,12 @@ pub struct MorphWeightsKeyframes { pub weights: Vec, } -impl From> for TranslationKeyframes { - fn from(value: Vec) -> Self { - Self(value) +impl From for TranslationKeyframes +where + T: Into>, +{ + fn from(value: T) -> Self { + Self(value.into()) } } @@ -280,10 +281,12 @@ impl Keyframes for TranslationKeyframes { _: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, weight: f32, ) -> Result<(), AnimationEvaluationError> { - let mut component = transform.ok_or(AnimationEvaluationError::ComponentNotPresent)?; + let mut component = transform.ok_or_else(|| { + AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) + })?; let value = self .first() - .ok_or(AnimationEvaluationError::KeyframeNotPresent)?; + .ok_or(AnimationEvaluationError::KeyframeNotPresent(0))?; component.translation = Animatable::interpolate(&component.translation, value, weight); Ok(()) } @@ -298,7 +301,9 @@ impl Keyframes for TranslationKeyframes { weight: f32, duration: f32, ) -> Result<(), AnimationEvaluationError> { - let mut component = transform.ok_or(AnimationEvaluationError::ComponentNotPresent)?; + let mut component = transform.ok_or_else(|| { + AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) + })?; animatable::interpolate_keyframes( &mut component.translation, &(*self)[..], @@ -311,9 +316,12 @@ impl Keyframes for TranslationKeyframes { } } -impl From> for ScaleKeyframes { - fn from(value: Vec) -> Self { - Self(value) +impl From for ScaleKeyframes +where + T: Into>, +{ + fn from(value: T) -> Self { + Self(value.into()) } } @@ -328,10 +336,12 @@ impl Keyframes for ScaleKeyframes { _: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, weight: f32, ) -> Result<(), AnimationEvaluationError> { - let mut component = transform.ok_or(AnimationEvaluationError::ComponentNotPresent)?; + let mut component = transform.ok_or_else(|| { + AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) + })?; let value = self .first() - .ok_or(AnimationEvaluationError::KeyframeNotPresent)?; + .ok_or(AnimationEvaluationError::KeyframeNotPresent(0))?; component.scale = Animatable::interpolate(&component.scale, value, weight); Ok(()) } @@ -346,7 +356,9 @@ impl Keyframes for ScaleKeyframes { weight: f32, duration: f32, ) -> Result<(), AnimationEvaluationError> { - let mut component = transform.ok_or(AnimationEvaluationError::ComponentNotPresent)?; + let mut component = transform.ok_or_else(|| { + AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) + })?; animatable::interpolate_keyframes( &mut component.scale, &(*self)[..], @@ -359,9 +371,12 @@ impl Keyframes for ScaleKeyframes { } } -impl From> for RotationKeyframes { - fn from(value: Vec) -> Self { - Self(value) +impl From for RotationKeyframes +where + T: Into>, +{ + fn from(value: T) -> Self { + Self(value.into()) } } @@ -376,10 +391,12 @@ impl Keyframes for RotationKeyframes { _: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, weight: f32, ) -> Result<(), AnimationEvaluationError> { - let mut component = transform.ok_or(AnimationEvaluationError::ComponentNotPresent)?; + let mut component = transform.ok_or_else(|| { + AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) + })?; let value = self .first() - .ok_or(AnimationEvaluationError::KeyframeNotPresent)?; + .ok_or(AnimationEvaluationError::KeyframeNotPresent(0))?; component.rotation = Animatable::interpolate(&component.rotation, value, weight); Ok(()) } @@ -394,7 +411,9 @@ impl Keyframes for RotationKeyframes { weight: f32, duration: f32, ) -> Result<(), AnimationEvaluationError> { - let mut component = transform.ok_or(AnimationEvaluationError::ComponentNotPresent)?; + let mut component = transform.ok_or_else(|| { + AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) + })?; animatable::interpolate_keyframes( &mut component.rotation, &(*self)[..], @@ -407,12 +426,13 @@ impl Keyframes for RotationKeyframes { } } -impl

From> for AnimatablePropertyKeyframes

+impl From for AnimatablePropertyKeyframes

where P: AnimatableProperty, + T: Into>, { - fn from(value: Vec) -> Self { - Self(value) + fn from(value: T) -> Self { + Self(value.into()) } } @@ -430,14 +450,14 @@ where mut entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, weight: f32, ) -> Result<(), AnimationEvaluationError> { - let mut component = entity - .get_mut::() - .ok_or(AnimationEvaluationError::ComponentNotPresent)?; - let property = - P::get_mut(&mut component).ok_or(AnimationEvaluationError::PropertyNotPresent)?; + 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)?; + .ok_or(AnimationEvaluationError::KeyframeNotPresent(0))?; ::interpolate(property, value, weight); Ok(()) } @@ -452,11 +472,11 @@ where weight: f32, duration: f32, ) -> Result<(), AnimationEvaluationError> { - let mut component = entity - .get_mut::() - .ok_or(AnimationEvaluationError::ComponentNotPresent)?; - let property = - P::get_mut(&mut component).ok_or(AnimationEvaluationError::PropertyNotPresent)?; + 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, @@ -512,9 +532,9 @@ impl Keyframes for MorphWeightsKeyframes { mut entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, weight: f32, ) -> Result<(), AnimationEvaluationError> { - let mut dest = entity - .get_mut::() - .ok_or(AnimationEvaluationError::ComponentNotPresent)?; + 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() { @@ -535,9 +555,9 @@ impl Keyframes for MorphWeightsKeyframes { weight: f32, duration: f32, ) -> Result<(), AnimationEvaluationError> { - let mut dest = entity - .get_mut::() - .ok_or(AnimationEvaluationError::ComponentNotPresent)?; + 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() { diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 8936fc22b8317..837accc6fa02c 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -13,7 +13,7 @@ pub mod keyframes; pub mod transition; mod util; -use std::any::Any; +use std::any::{Any, TypeId}; use std::cell::RefCell; use std::collections::BTreeMap; use std::fmt::Debug; @@ -125,32 +125,45 @@ impl VariableCurve { /// Creates a new curve from timestamps and keyframes with no interpolation. /// /// The two arrays must have the same length. - pub fn step(keyframe_timestamps: Vec, keyframes: impl Into) -> VariableCurve + pub fn step( + keyframe_timestamps: impl Into>, + keyframes: impl Into, + ) -> VariableCurve where K: Keyframes, { - VariableCurve::new(keyframe_timestamps, keyframes, Interpolation::Step) + 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: Vec, keyframes: impl Into) -> VariableCurve + pub fn linear( + keyframe_timestamps: impl Into>, + keyframes: impl Into, + ) -> VariableCurve where K: Keyframes, { - VariableCurve::new(keyframe_timestamps, keyframes, Interpolation::Linear) + 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: Vec, keyframes: impl Into) -> VariableCurve + pub fn cubic_spline( + keyframe_timestamps: impl Into>, + keyframes: impl Into, + ) -> VariableCurve where K: Keyframes, { - VariableCurve::new(keyframe_timestamps, keyframes, Interpolation::CubicSpline) + VariableCurve::new( + keyframe_timestamps.into(), + keyframes, + Interpolation::CubicSpline, + ) } /// Find the index of the keyframe at or before the current time. @@ -609,22 +622,17 @@ pub enum AnimationEvaluationError { /// 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, + 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, + ComponentNotPresent(TypeId), - /// The component to be animated was present, but the `path` didn't name any - /// reflectable property on the component. - /// - /// This is usually because of a mistyped `ParsedPath`. For example, to - /// alter the color of the first section of text, you should use a path of - /// `sections[0].style.color.0` (correct) instead of, for example, - /// `sections.0.style.color` (incorrect). - PropertyNotPresent, + /// 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 @@ -1114,9 +1122,9 @@ pub fn animate_targets( } else { trace!( "Either an animation player {:?} or a graph was missing for the target \ - entity xxx ({:?}); no animations will play this frame", + entity {:?} ({:?}); no animations will play this frame", player_id, - //entity_mut.id(), FIXME + entity_mut.id(), entity_mut.get::(), ); return; diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 092da7232a47a..c91885469c766 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -2402,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. @@ -2483,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/examples/animation/animated_transform.rs b/examples/animation/animated_transform.rs index 774eaf8e00b66..38e3bdb24d380 100644 --- a/examples/animation/animated_transform.rs +++ b/examples/animation/animated_transform.rs @@ -52,8 +52,8 @@ fn setup( animation.add_curve_to_target( planet_animation_target_id, VariableCurve::linear::( - vec![0.0, 1.0, 2.0, 3.0, 4.0], - vec![ + [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), @@ -72,8 +72,8 @@ fn setup( animation.add_curve_to_target( orbit_controller_animation_target_id, VariableCurve::linear::( - vec![0.0, 1.0, 2.0, 3.0, 4.0], - vec![ + [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.), @@ -91,8 +91,8 @@ fn setup( animation.add_curve_to_target( satellite_animation_target_id, VariableCurve::linear::( - vec![0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0], - vec![ + [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), @@ -110,17 +110,16 @@ fn setup( 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: Box::new(RotationKeyframes(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 index 0fee526d0a8a3..ad55bac900358 100644 --- a/examples/animation/animated_ui.rs +++ b/examples/animation/animated_ui.rs @@ -82,8 +82,8 @@ impl AnimationInfo { animation_clip.add_curve_to_target( animation_target_id, VariableCurve::linear::>( - vec![0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0], - vec![24.0, 80.0, 24.0, 80.0, 24.0, 80.0, 24.0], + [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], ), ); @@ -91,16 +91,10 @@ impl AnimationInfo { // the same time duration as the previous curve. animation_clip.add_curve_to_target( animation_target_id, - VariableCurve { - keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0], - keyframes: Box::new(AnimatablePropertyKeyframes::(vec![ - Srgba::RED, - Srgba::GREEN, - Srgba::BLUE, - Srgba::RED, - ])), - interpolation: Interpolation::Linear, - }, + 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. From ff83d37b6c1961220ea37180348ad4eac6f154b3 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Sun, 22 Sep 2024 09:23:28 -0700 Subject: [PATCH 7/7] Fix compile error --- examples/animation/animated_transform.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/animation/animated_transform.rs b/examples/animation/animated_transform.rs index 38e3bdb24d380..7fae355b9becb 100644 --- a/examples/animation/animated_transform.rs +++ b/examples/animation/animated_transform.rs @@ -110,7 +110,7 @@ fn setup( AnimationTargetId::from_names( [planet.clone(), orbit_controller.clone(), satellite.clone()].iter(), ), - VariableCurve::linear( + VariableCurve::linear::( [0.0, 1.0, 2.0, 3.0, 4.0], [ Quat::IDENTITY,