diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml index ae1e8ee23c..66d20c49fe 100644 --- a/crates/bevy_animation/Cargo.toml +++ b/crates/bevy_animation/Cargo.toml @@ -33,7 +33,6 @@ bevy_ui = { path = "../bevy_ui", version = "0.15.0-dev", features = [ bevy_text = { path = "../bevy_text", version = "0.15.0-dev" } # other -fixedbitset = "0.5" petgraph = { version = "0.6", features = ["serde-1"] } ron = "0.8" serde = "1" @@ -41,6 +40,7 @@ blake3 = { version = "1.0" } thiserror = "1" thread_local = "1" uuid = { version = "1.7", features = ["v4"] } +smallvec = "1" [lints] workspace = true diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index c7825e9066..26589b8e6e 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -89,13 +89,15 @@ use bevy_math::{ iterable::IterableCurve, Curve, Interval, }, - FloatExt, Quat, Vec3, + Quat, Vec3, }; use bevy_reflect::{FromReflect, Reflect, Reflectable, TypePath}; use bevy_render::mesh::morph::MorphWeights; use bevy_transform::prelude::Transform; -use crate::{prelude::Animatable, AnimationEntityMut, AnimationEvaluationError}; +use crate::{ + graph::AnimationNodeIndex, prelude::Animatable, AnimationEntityMut, AnimationEvaluationError, +}; /// A value on a component that Bevy can animate. /// @@ -188,6 +190,21 @@ pub struct AnimatableCurve { _phantom: PhantomData

, } +/// An [`AnimatableCurveEvaluator`] for [`AnimatableProperty`] instances. +/// +/// You shouldn't ordinarily need to instantiate one of these manually. Bevy +/// will automatically do so when you use an [`AnimatableCurve`] instance. +#[derive(Reflect, FromReflect)] +#[reflect(from_reflect = false)] +pub struct AnimatableCurveEvaluator

+where + P: AnimatableProperty, +{ + evaluator: BasicAnimationCurveEvaluator, + #[reflect(ignore)] + phantom: PhantomData

, +} + impl AnimatableCurve where P: AnimatableProperty, @@ -241,20 +258,72 @@ where self.curve.domain() } - fn apply<'a>( + fn evaluator_type(&self) -> TypeId { + TypeId::of::>() + } + + fn create_evaluator(&self) -> Box { + Box::new(AnimatableCurveEvaluator { + evaluator: BasicAnimationCurveEvaluator::default(), + phantom: PhantomData::

, + }) + } + + fn apply( &self, + curve_evaluator: &mut dyn AnimationCurveEvaluator, t: f32, - _transform: Option>, - mut entity: AnimationEntityMut<'a>, weight: f32, + graph_node: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError> { + let curve_evaluator = (*Reflect::as_any_mut(curve_evaluator)) + .downcast_mut::>() + .unwrap(); + let value = self.curve.sample_clamped(t); + curve_evaluator + .evaluator + .stack + .push(BasicAnimationCurveEvaluatorStackElement { + value, + weight, + graph_node, + }); + Ok(()) + } +} + +impl

AnimationCurveEvaluator for AnimatableCurveEvaluator

+where + P: AnimatableProperty, +{ + fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { + self.evaluator.blend(graph_node) + } + + fn push_blend_register( + &mut self, + weight: f32, + graph_node: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError> { + self.evaluator.push_blend_register(weight, graph_node) + } + + fn commit<'a>( + &mut self, + _: Option>, + mut entity: AnimationEntityMut<'a>, ) -> Result<(), AnimationEvaluationError> { let mut component = entity.get_mut::().ok_or_else(|| { AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) })?; let property = P::get_mut(&mut component) .ok_or_else(|| AnimationEvaluationError::PropertyNotPresent(TypeId::of::

()))?; - let value = self.curve.sample_clamped(t); - *property = ::interpolate(property, &value, weight); + *property = self + .evaluator + .stack + .pop() + .ok_or_else(inconsistent::>)? + .value; Ok(()) } } @@ -267,6 +336,16 @@ where #[reflect(from_reflect = false)] pub struct TranslationCurve(pub C); +/// An [`AnimationCurveEvaluator`] for use with [`TranslationCurve`]s. +/// +/// You shouldn't need to instantiate this manually; Bevy will automatically do +/// so. +#[derive(Reflect, FromReflect)] +#[reflect(from_reflect = false)] +pub struct TranslationCurveEvaluator { + evaluator: BasicAnimationCurveEvaluator, +} + impl AnimationCurve for TranslationCurve where C: AnimationCompatibleCurve, @@ -279,19 +358,66 @@ where self.0.domain() } - fn apply<'a>( + fn evaluator_type(&self) -> TypeId { + TypeId::of::() + } + + fn create_evaluator(&self) -> Box { + Box::new(TranslationCurveEvaluator { + evaluator: BasicAnimationCurveEvaluator::default(), + }) + } + + fn apply( &self, + curve_evaluator: &mut dyn AnimationCurveEvaluator, t: f32, - transform: Option>, - _entity: AnimationEntityMut<'a>, weight: f32, + graph_node: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError> { + let curve_evaluator = (*Reflect::as_any_mut(curve_evaluator)) + .downcast_mut::() + .unwrap(); + let value = self.0.sample_clamped(t); + curve_evaluator + .evaluator + .stack + .push(BasicAnimationCurveEvaluatorStackElement { + value, + weight, + graph_node, + }); + Ok(()) + } +} + +impl AnimationCurveEvaluator for TranslationCurveEvaluator { + fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { + self.evaluator.blend(graph_node) + } + + fn push_blend_register( + &mut self, + weight: f32, + graph_node: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError> { + self.evaluator.push_blend_register(weight, graph_node) + } + + fn commit<'a>( + &mut self, + transform: Option>, + _: AnimationEntityMut<'a>, ) -> Result<(), AnimationEvaluationError> { let mut component = transform.ok_or_else(|| { AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) })?; - let new_value = self.0.sample_clamped(t); - component.translation = - ::interpolate(&component.translation, &new_value, weight); + component.translation = self + .evaluator + .stack + .pop() + .ok_or_else(inconsistent::)? + .value; Ok(()) } } @@ -304,6 +430,16 @@ where #[reflect(from_reflect = false)] pub struct RotationCurve(pub C); +/// An [`AnimationCurveEvaluator`] for use with [`RotationCurve`]s. +/// +/// You shouldn't need to instantiate this manually; Bevy will automatically do +/// so. +#[derive(Reflect, FromReflect)] +#[reflect(from_reflect = false)] +pub struct RotationCurveEvaluator { + evaluator: BasicAnimationCurveEvaluator, +} + impl AnimationCurve for RotationCurve where C: AnimationCompatibleCurve, @@ -316,19 +452,66 @@ where self.0.domain() } - fn apply<'a>( + fn evaluator_type(&self) -> TypeId { + TypeId::of::() + } + + fn create_evaluator(&self) -> Box { + Box::new(RotationCurveEvaluator { + evaluator: BasicAnimationCurveEvaluator::default(), + }) + } + + fn apply( &self, + curve_evaluator: &mut dyn AnimationCurveEvaluator, t: f32, - transform: Option>, - _entity: AnimationEntityMut<'a>, weight: f32, + graph_node: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError> { + let curve_evaluator = (*Reflect::as_any_mut(curve_evaluator)) + .downcast_mut::() + .unwrap(); + let value = self.0.sample_clamped(t); + curve_evaluator + .evaluator + .stack + .push(BasicAnimationCurveEvaluatorStackElement { + value, + weight, + graph_node, + }); + Ok(()) + } +} + +impl AnimationCurveEvaluator for RotationCurveEvaluator { + fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { + self.evaluator.blend(graph_node) + } + + fn push_blend_register( + &mut self, + weight: f32, + graph_node: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError> { + self.evaluator.push_blend_register(weight, graph_node) + } + + fn commit<'a>( + &mut self, + transform: Option>, + _: AnimationEntityMut<'a>, ) -> Result<(), AnimationEvaluationError> { let mut component = transform.ok_or_else(|| { AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) })?; - let new_value = self.0.sample_clamped(t); - component.rotation = - ::interpolate(&component.rotation, &new_value, weight); + component.rotation = self + .evaluator + .stack + .pop() + .ok_or_else(inconsistent::)? + .value; Ok(()) } } @@ -341,6 +524,16 @@ where #[reflect(from_reflect = false)] pub struct ScaleCurve(pub C); +/// An [`AnimationCurveEvaluator`] for use with [`ScaleCurve`]s. +/// +/// You shouldn't need to instantiate this manually; Bevy will automatically do +/// so. +#[derive(Reflect, FromReflect)] +#[reflect(from_reflect = false)] +pub struct ScaleCurveEvaluator { + evaluator: BasicAnimationCurveEvaluator, +} + impl AnimationCurve for ScaleCurve where C: AnimationCompatibleCurve, @@ -353,18 +546,66 @@ where self.0.domain() } - fn apply<'a>( + fn evaluator_type(&self) -> TypeId { + TypeId::of::() + } + + fn create_evaluator(&self) -> Box { + Box::new(ScaleCurveEvaluator { + evaluator: BasicAnimationCurveEvaluator::default(), + }) + } + + fn apply( &self, + curve_evaluator: &mut dyn AnimationCurveEvaluator, t: f32, - transform: Option>, - _entity: AnimationEntityMut<'a>, weight: f32, + graph_node: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError> { + let curve_evaluator = (*Reflect::as_any_mut(curve_evaluator)) + .downcast_mut::() + .unwrap(); + let value = self.0.sample_clamped(t); + curve_evaluator + .evaluator + .stack + .push(BasicAnimationCurveEvaluatorStackElement { + value, + weight, + graph_node, + }); + Ok(()) + } +} + +impl AnimationCurveEvaluator for ScaleCurveEvaluator { + fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { + self.evaluator.blend(graph_node) + } + + fn push_blend_register( + &mut self, + weight: f32, + graph_node: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError> { + self.evaluator.push_blend_register(weight, graph_node) + } + + fn commit<'a>( + &mut self, + transform: Option>, + _: AnimationEntityMut<'a>, ) -> Result<(), AnimationEvaluationError> { let mut component = transform.ok_or_else(|| { AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) })?; - let new_value = self.0.sample_clamped(t); - component.scale = ::interpolate(&component.scale, &new_value, weight); + component.scale = self + .evaluator + .stack + .pop() + .ok_or_else(inconsistent::)? + .value; Ok(()) } } @@ -377,6 +618,43 @@ where #[reflect(from_reflect = false)] pub struct WeightsCurve(pub C); +#[derive(Reflect, FromReflect)] +#[reflect(from_reflect = false)] +struct WeightsCurveEvaluator { + /// The values of the stack, in which each element is a list of morph target + /// weights. + /// + /// The stack elements are concatenated and tightly packed together. + /// + /// The number of elements in this stack will always be a multiple of + /// [`Self::morph_target_count`]. + stack_morph_target_weights: Vec, + + /// The blend weights and graph node indices for each element of the stack. + /// + /// This should have as many elements as there are stack nodes. In other + /// words, `Self::stack_morph_target_weights.len() * + /// Self::morph_target_counts as usize == + /// Self::stack_blend_weights_and_graph_nodes`. + stack_blend_weights_and_graph_nodes: Vec<(f32, AnimationNodeIndex)>, + + /// The morph target weights in the blend register, if any. + /// + /// This field should be ignored if [`Self::blend_register_blend_weight`] is + /// `None`. If non-empty, it will always have [`Self::morph_target_count`] + /// elements in it. + blend_register_morph_target_weights: Vec, + + /// The weight in the blend register. + /// + /// This will be `None` if the blend register is empty. In that case, + /// [`Self::blend_register_morph_target_weights`] will be empty. + blend_register_blend_weight: Option, + + /// The number of morph targets that are to be animated. + morph_target_count: Option, +} + impl AnimationCurve for WeightsCurve where C: IterableCurve + Debug + Clone + Reflectable, @@ -389,45 +667,222 @@ where self.0.domain() } - fn apply<'a>( + fn evaluator_type(&self) -> TypeId { + TypeId::of::() + } + + fn create_evaluator(&self) -> Box { + Box::new(WeightsCurveEvaluator { + stack_morph_target_weights: vec![], + stack_blend_weights_and_graph_nodes: vec![], + blend_register_morph_target_weights: vec![], + blend_register_blend_weight: None, + morph_target_count: None, + }) + } + + fn apply( &self, + curve_evaluator: &mut dyn AnimationCurveEvaluator, t: f32, - _transform: Option>, - mut entity: AnimationEntityMut<'a>, weight: f32, + graph_node: AnimationNodeIndex, ) -> Result<(), AnimationEvaluationError> { - let mut dest = entity.get_mut::().ok_or_else(|| { - AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) - })?; - lerp_morph_weights(dest.weights_mut(), self.0.sample_iter_clamped(t), weight); + let curve_evaluator = (*Reflect::as_any_mut(curve_evaluator)) + .downcast_mut::() + .unwrap(); + + let prev_morph_target_weights_len = curve_evaluator.stack_morph_target_weights.len(); + curve_evaluator + .stack_morph_target_weights + .extend(self.0.sample_iter_clamped(t)); + curve_evaluator.morph_target_count = Some( + (curve_evaluator.stack_morph_target_weights.len() - prev_morph_target_weights_len) + as u32, + ); + + curve_evaluator + .stack_blend_weights_and_graph_nodes + .push((weight, graph_node)); Ok(()) } } -/// Update `morph_weights` based on weights in `incoming_weights` with a linear interpolation -/// on `lerp_weight`. -fn lerp_morph_weights( - morph_weights: &mut [f32], - incoming_weights: impl Iterator, - lerp_weight: f32, -) { - let zipped = morph_weights.iter_mut().zip(incoming_weights); - for (morph_weight, incoming_weights) in zipped { - *morph_weight = morph_weight.lerp(incoming_weights, lerp_weight); +impl AnimationCurveEvaluator for WeightsCurveEvaluator { + fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { + let Some(&(_, top_graph_node)) = self.stack_blend_weights_and_graph_nodes.last() else { + return Ok(()); + }; + if top_graph_node != graph_node { + return Ok(()); + } + + let (weight_to_blend, _) = self.stack_blend_weights_and_graph_nodes.pop().unwrap(); + let stack_iter = self.stack_morph_target_weights.drain( + (self.stack_morph_target_weights.len() - self.morph_target_count.unwrap() as usize).., + ); + + match self.blend_register_blend_weight { + None => { + self.blend_register_blend_weight = Some(weight_to_blend); + self.blend_register_morph_target_weights.clear(); + self.blend_register_morph_target_weights.extend(stack_iter); + } + + Some(ref mut current_weight) => { + *current_weight += weight_to_blend; + for (dest, src) in self + .blend_register_morph_target_weights + .iter_mut() + .zip(stack_iter) + { + *dest = f32::interpolate(dest, &src, weight_to_blend / *current_weight); + } + } + } + + Ok(()) + } + + fn push_blend_register( + &mut self, + weight: f32, + graph_node: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError> { + if self.blend_register_blend_weight.take().is_some() { + self.stack_morph_target_weights + .append(&mut self.blend_register_morph_target_weights); + self.stack_blend_weights_and_graph_nodes + .push((weight, graph_node)); + } + Ok(()) + } + + fn commit<'a>( + &mut self, + _: Option>, + mut entity: AnimationEntityMut<'a>, + ) -> Result<(), AnimationEvaluationError> { + if self.stack_morph_target_weights.is_empty() { + return Ok(()); + } + + // Compute the index of the first morph target in the last element of + // the stack. + let index_of_first_morph_target = + self.stack_morph_target_weights.len() - self.morph_target_count.unwrap() as usize; + + for (dest, src) in entity + .get_mut::() + .ok_or_else(|| { + AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) + })? + .weights_mut() + .iter_mut() + .zip(self.stack_morph_target_weights[index_of_first_morph_target..].iter()) + { + *dest = *src; + } + self.stack_morph_target_weights.clear(); + self.stack_blend_weights_and_graph_nodes.clear(); + Ok(()) } } -/// A low-level trait that provides control over how curves are actually applied to entities -/// by the animation system. +#[derive(Reflect, FromReflect)] +#[reflect(from_reflect = false)] +struct BasicAnimationCurveEvaluator +where + A: Animatable, +{ + stack: Vec>, + blend_register: Option<(A, f32)>, +} + +#[derive(Reflect, FromReflect)] +#[reflect(from_reflect = false)] +struct BasicAnimationCurveEvaluatorStackElement +where + A: Animatable, +{ + value: A, + weight: f32, + graph_node: AnimationNodeIndex, +} + +impl Default for BasicAnimationCurveEvaluator +where + A: Animatable, +{ + fn default() -> Self { + BasicAnimationCurveEvaluator { + stack: vec![], + blend_register: None, + } + } +} + +impl BasicAnimationCurveEvaluator +where + A: Animatable, +{ + fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { + let Some(top) = self.stack.last() else { + return Ok(()); + }; + if top.graph_node != graph_node { + return Ok(()); + } + + let BasicAnimationCurveEvaluatorStackElement { + value: value_to_blend, + weight: weight_to_blend, + graph_node: _, + } = self.stack.pop().unwrap(); + + match self.blend_register { + None => self.blend_register = Some((value_to_blend, weight_to_blend)), + Some((ref mut current_value, ref mut current_weight)) => { + *current_weight += weight_to_blend; + *current_value = A::interpolate( + current_value, + &value_to_blend, + weight_to_blend / *current_weight, + ); + } + } + + Ok(()) + } + + fn push_blend_register( + &mut self, + weight: f32, + graph_node: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError> { + if let Some((value, _)) = self.blend_register.take() { + self.stack.push(BasicAnimationCurveEvaluatorStackElement { + value, + weight, + graph_node, + }); + } + Ok(()) + } +} + +/// A low-level trait that provides control over how curves are actually applied +/// to entities by the animation system. /// -/// Typically, this will not need to be implemented manually, since it is automatically -/// implemented by [`AnimatableCurve`] and other curves used by the animation system -/// (e.g. those that animate parts of transforms or morph weights). However, this can be -/// implemented manually when `AnimatableCurve` is not sufficiently expressive. +/// Typically, this will not need to be implemented manually, since it is +/// automatically implemented by [`AnimatableCurve`] and other curves used by +/// the animation system (e.g. those that animate parts of transforms or morph +/// weights). However, this can be implemented manually when `AnimatableCurve` +/// is not sufficiently expressive. /// -/// In many respects, this behaves like a type-erased form of [`Curve`], where the output -/// type of the curve is remembered only in the components that are mutated in the -/// implementation of [`apply`]. +/// In many respects, this behaves like a type-erased form of [`Curve`], where +/// the output type of the curve is remembered only in the components that are +/// mutated in the implementation of [`apply`]. /// /// [`apply`]: AnimationCurve::apply pub trait AnimationCurve: Reflect + Debug + Send + Sync { @@ -437,15 +892,111 @@ pub trait AnimationCurve: Reflect + Debug + Send + Sync { /// The range of times for which this animation is defined. fn domain(&self) -> Interval; - /// Write the value of sampling this curve at time `t` into `transform` or `entity`, - /// as appropriate, interpolating between the existing value and the sampled value - /// using the given `weight`. - fn apply<'a>( + /// Returns the type ID of the [`AnimationCurveEvaluator`]. + /// + /// This must match the type returned by [`Self::create_evaluator`]. It must + /// be a single type that doesn't depend on the type of the curve. + fn evaluator_type(&self) -> TypeId; + + /// Returns a newly-instantiated [`AnimationCurveEvaluator`] for use with + /// this curve. + /// + /// All curve types must return the same type of + /// [`AnimationCurveEvaluator`]. The returned value must match the type + /// returned by [`Self::evaluator_type`]. + fn create_evaluator(&self) -> Box; + + /// Samples the curve at the given time `t`, and pushes the sampled value + /// onto the evaluation stack of the `curve_evaluator`. + /// + /// The `curve_evaluator` parameter points to the value returned by + /// [`Self::create_evaluator`], upcast to an `&mut dyn + /// AnimationCurveEvaluator`. Typically, implementations of [`Self::apply`] + /// will want to downcast the `curve_evaluator` parameter to the concrete + /// type [`Self::evaluator_type`] in order to push values of the appropriate + /// type onto its evaluation stack. + /// + /// Be sure not to confuse the `t` and `weight` values. The former + /// determines the position at which the *curve* is sampled, while `weight` + /// ultimately determines how much the *stack values* will be blended + /// together (see the definition of [`AnimationCurveEvaluator::blend`]). + fn apply( &self, + curve_evaluator: &mut dyn AnimationCurveEvaluator, t: f32, + weight: f32, + graph_node: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError>; +} + +/// 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 [`AnimatableCurveEvaluator`] +/// isn't sufficiently-expressive for your needs. For example, [`MorphWeights`] +/// implements this trait instead of using [`AnimatableCurveEvaluator`] 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. +/// +/// If you implement this trait, you should also implement [`AnimationCurve`] on +/// your curve type, as that trait allows creating instances of this one. +/// +/// Implementations of [`AnimatableCurveEvaluator`] should maintain a *stack* of +/// (value, weight, node index) triples, as well as a *blend register*, which is +/// either a (value, weight) pair or empty. *Value* here refers to an instance +/// of the value being animated: for example, [`Vec3`] in the case of +/// translation keyframes. The stack stores intermediate values generated while +/// evaluating the [`crate::graph::AnimationGraph`], while the blend register +/// stores the result of a blend operation. +pub trait AnimationCurveEvaluator: Reflect { + /// Blends the top element of the stack with the blend register. + /// + /// The semantics of this method are as follows: + /// + /// 1. Pop the top element of the stack. Call its value vₘ and its weight + /// wₘ. If the stack was empty, return success. + /// + /// 2. If the blend register is empty, set the blend register value to vₘ + /// and the blend register weight to wₘ; then, return success. + /// + /// 3. If the blend register is nonempty, call its current value vₙ and its + /// current weight wₙ. Then, set the value of the blend register to + /// `interpolate(vₙ, vₘ, wₘ / (wₘ + wₙ))`, and set the weight of the blend + /// register to wₘ + wₙ. + /// + /// 4. Return success. + fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError>; + + /// Pushes the current value of the blend register onto the stack. + /// + /// If the blend register is empty, this method does nothing successfully. + /// Otherwise, this method pushes the current value of the blend register + /// onto the stack, alongside the weight and graph node supplied to this + /// function. The weight present in the blend register is discarded; only + /// the weight parameter to this function is pushed onto the stack. The + /// blend register is emptied after this process. + fn push_blend_register( + &mut self, + weight: f32, + graph_node: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError>; + + /// Pops the top value off the stack and writes it into the appropriate + /// component. + /// + /// If the stack is empty, this method does nothing successfully. Otherwise, + /// it pops the top value off the stack, fetches the associated component + /// from either the `transform` or `entity` values as appropriate, and + /// updates the appropriate property with the value popped from the stack. + /// The weight and node index associated with the popped stack element are + /// discarded. After doing this, the stack is emptied. + /// + /// The property on the component must be overwritten with the value from + /// the stack, not blended with it. + fn commit<'a>( + &mut self, transform: Option>, entity: AnimationEntityMut<'a>, - weight: f32, ) -> Result<(), AnimationEvaluationError>; } @@ -496,3 +1047,10 @@ where }) } } + +fn inconsistent

() -> AnimationEvaluationError +where + P: 'static + ?Sized, +{ + AnimationEvaluationError::InconsistentEvaluatorImplementation(TypeId::of::

()) +} diff --git a/crates/bevy_animation/src/graph.rs b/crates/bevy_animation/src/graph.rs index 5264cf9a23..22c0e1a608 100644 --- a/crates/bevy_animation/src/graph.rs +++ b/crates/bevy_animation/src/graph.rs @@ -1,14 +1,25 @@ //! The animation graph, which allows animations to be blended together. -use core::ops::{Index, IndexMut}; +use core::iter; +use core::ops::{Index, IndexMut, Range}; use std::io::{self, Write}; -use bevy_asset::{io::Reader, Asset, AssetId, AssetLoader, AssetPath, Handle, LoadContext}; +use bevy_asset::{ + io::Reader, Asset, AssetEvent, AssetId, AssetLoader, AssetPath, Assets, Handle, LoadContext, +}; +use bevy_ecs::{ + event::EventReader, + system::{Res, ResMut, Resource}, +}; use bevy_reflect::{Reflect, ReflectSerialize}; use bevy_utils::HashMap; -use petgraph::graph::{DiGraph, NodeIndex}; +use petgraph::{ + graph::{DiGraph, NodeIndex}, + Direction, +}; use ron::de::SpannedError; use serde::{Deserialize, Serialize}; +use smallvec::SmallVec; use thiserror::Error; use crate::{AnimationClip, AnimationTargetId}; @@ -172,6 +183,99 @@ pub enum AnimationGraphLoadError { SpannedRon(#[from] SpannedError), } +/// Acceleration structures for animation graphs that allows Bevy to evaluate +/// them quickly. +/// +/// These are kept up to date as [`AnimationGraph`] instances are added, +/// modified, and removed. +#[derive(Default, Reflect, Resource)] +pub struct ThreadedAnimationGraphs( + pub(crate) HashMap, ThreadedAnimationGraph>, +); + +/// An acceleration structure for an animation graph that allows Bevy to +/// evaluate it quickly. +/// +/// This is kept up to date as the associated [`AnimationGraph`] instance is +/// added, modified, or removed. +#[derive(Default, Reflect)] +pub struct ThreadedAnimationGraph { + /// A cached postorder traversal of the graph. + /// + /// The node indices here are stored in postorder. Siblings are stored in + /// descending order. This is because the + /// [`crate::animation_curves::AnimationCurveEvaluator`] uses a stack for + /// evaluation. Consider this graph: + /// + /// ```text + /// ┌─────┐ + /// │ │ + /// │ 1 │ + /// │ │ + /// └──┬──┘ + /// │ + /// ┌───────┼───────┐ + /// │ │ │ + /// ▼ ▼ ▼ + /// ┌─────┐ ┌─────┐ ┌─────┐ + /// │ │ │ │ │ │ + /// │ 2 │ │ 3 │ │ 4 │ + /// │ │ │ │ │ │ + /// └──┬──┘ └─────┘ └─────┘ + /// │ + /// ┌───┴───┐ + /// │ │ + /// ▼ ▼ + /// ┌─────┐ ┌─────┐ + /// │ │ │ │ + /// │ 5 │ │ 6 │ + /// │ │ │ │ + /// └─────┘ └─────┘ + /// ``` + /// + /// The postorder traversal in this case will be (4, 3, 6, 5, 2, 1). + /// + /// The fact that the children of each node are sorted in reverse ensures + /// that, at each level, the order of blending proceeds in ascending order + /// by node index, as we guarantee. To illustrate this, consider the way + /// the graph above is evaluated. (Interpolation is represented with the ⊕ + /// symbol.) + /// + /// | Step | Node | Operation | Stack (after operation) | Blend Register | + /// | ---- | ---- | ---------- | ----------------------- | -------------- | + /// | 1 | 4 | Push | 4 | | + /// | 2 | 3 | Push | 4 3 | | + /// | 3 | 6 | Push | 4 3 6 | | + /// | 4 | 5 | Push | 4 3 6 5 | | + /// | 5 | 2 | Blend 5 | 4 3 6 | 5 | + /// | 6 | 2 | Blend 6 | 4 3 | 5 ⊕ 6 | + /// | 7 | 2 | Push Blend | 4 3 2 | | + /// | 8 | 1 | Blend 2 | 4 3 | 2 | + /// | 9 | 1 | Blend 3 | 4 | 2 ⊕ 3 | + /// | 10 | 1 | Blend 4 | | 2 ⊕ 3 ⊕ 4 | + /// | 11 | 1 | Push Blend | 1 | | + /// | 12 | | Commit | | | + pub threaded_graph: Vec, + + /// A mapping from each parent node index to the range within + /// [`Self::sorted_edges`]. + /// + /// This allows for quick lookup of the children of each node, sorted in + /// ascending order of node index, without having to sort the result of the + /// `petgraph` traversal functions every frame. + pub sorted_edge_ranges: Vec>, + + /// A list of the children of each node, sorted in ascending order. + pub sorted_edges: Vec, + + /// A mapping from node index to a bitfield specifying the mask groups that + /// this node masks *out* (i.e. doesn't animate). + /// + /// A 1 in bit position N indicates that this node doesn't animate any + /// targets of mask group N. + pub computed_masks: Vec, +} + /// A version of [`AnimationGraph`] suitable for serializing as an asset. /// /// Animation nodes can refer to external animation clips, and the [`AssetId`] @@ -571,3 +675,112 @@ impl From for SerializedAnimationGraph { } } } + +/// A system that creates, updates, and removes [`ThreadedAnimationGraph`] +/// structures for every changed [`AnimationGraph`]. +/// +/// The [`ThreadedAnimationGraph`] contains acceleration structures that allow +/// for quick evaluation of that graph's animations. +pub(crate) fn thread_animation_graphs( + mut threaded_animation_graphs: ResMut, + animation_graphs: Res>, + mut animation_graph_asset_events: EventReader>, +) { + for animation_graph_asset_event in animation_graph_asset_events.read() { + match *animation_graph_asset_event { + AssetEvent::Added { id } + | AssetEvent::Modified { id } + | AssetEvent::LoadedWithDependencies { id } => { + // Fetch the animation graph. + let Some(animation_graph) = animation_graphs.get(id) else { + continue; + }; + + // Reuse the allocation if possible. + let mut threaded_animation_graph = + threaded_animation_graphs.0.remove(&id).unwrap_or_default(); + threaded_animation_graph.clear(); + + // Recursively thread the graph in postorder. + threaded_animation_graph.init(animation_graph); + threaded_animation_graph.build_from( + &animation_graph.graph, + animation_graph.root, + 0, + ); + + // Write in the threaded graph. + threaded_animation_graphs + .0 + .insert(id, threaded_animation_graph); + } + + AssetEvent::Removed { id } => { + threaded_animation_graphs.0.remove(&id); + } + AssetEvent::Unused { .. } => {} + } + } +} + +impl ThreadedAnimationGraph { + /// Removes all the data in this [`ThreadedAnimationGraph`], keeping the + /// memory around for later reuse. + fn clear(&mut self) { + self.threaded_graph.clear(); + self.sorted_edge_ranges.clear(); + self.sorted_edges.clear(); + } + + /// Prepares the [`ThreadedAnimationGraph`] for recursion. + fn init(&mut self, animation_graph: &AnimationGraph) { + let node_count = animation_graph.graph.node_count(); + let edge_count = animation_graph.graph.edge_count(); + + self.threaded_graph.reserve(node_count); + self.sorted_edges.reserve(edge_count); + + self.sorted_edge_ranges.clear(); + self.sorted_edge_ranges + .extend(iter::repeat(0..0).take(node_count)); + + self.computed_masks.clear(); + self.computed_masks.extend(iter::repeat(0).take(node_count)); + } + + /// Recursively constructs the [`ThreadedAnimationGraph`] for the subtree + /// rooted at the given node. + /// + /// `mask` specifies the computed mask of the parent node. (It could be + /// fetched from the [`Self::computed_masks`] field, but we pass it + /// explicitly as a micro-optimization.) + fn build_from( + &mut self, + graph: &AnimationDiGraph, + node_index: AnimationNodeIndex, + mut mask: u64, + ) { + // Accumulate the mask. + mask |= graph.node_weight(node_index).unwrap().mask; + self.computed_masks.insert(node_index.index(), mask); + + // Gather up the indices of our children, and sort them. + let mut kids: SmallVec<[AnimationNodeIndex; 8]> = graph + .neighbors_directed(node_index, Direction::Outgoing) + .collect(); + kids.sort_unstable(); + + // Write in the list of kids. + self.sorted_edge_ranges[node_index.index()] = + (self.sorted_edges.len() as u32)..((self.sorted_edges.len() + kids.len()) as u32); + self.sorted_edges.extend_from_slice(&kids); + + // Recurse. (This is a postorder traversal.) + for kid in kids.into_iter().rev() { + self.build_from(graph, kid, mask); + } + + // Finally, push our index. + self.threaded_graph.push(node_index); + } +} diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 39a8349d81..2358543204 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -16,7 +16,6 @@ pub mod graph; pub mod transition; mod util; -use alloc::collections::BTreeMap; use core::{ any::{Any, TypeId}, cell::RefCell, @@ -24,6 +23,9 @@ use core::{ hash::{Hash, Hasher}, iter, }; +use prelude::AnimationCurveEvaluator; + +use crate::graph::ThreadedAnimationGraphs; use bevy_app::{App, Plugin, PostUpdate}; use bevy_asset::{Asset, AssetApp, Assets, Handle}; @@ -46,11 +48,9 @@ use bevy_ui::UiSystem; use bevy_utils::{ hashbrown::HashMap, tracing::{trace, warn}, - NoOpHash, + NoOpHash, TypeIdMap, }; -use fixedbitset::FixedBitSet; -use graph::AnimationMask; -use petgraph::{graph::NodeIndex, Direction}; +use petgraph::graph::NodeIndex; use serde::{Deserialize, Serialize}; use thread_local::ThreadLocal; use uuid::Uuid; @@ -461,6 +461,14 @@ pub enum AnimationEvaluationError { /// The component to be animated was present, but the property on the /// component wasn't present. PropertyNotPresent(TypeId), + + /// An internal error occurred in the implementation of + /// [`AnimationCurveEvaluator`]. + /// + /// You shouldn't ordinarily see this error unless you implemented + /// [`AnimationCurveEvaluator`] yourself. The contained [`TypeId`] is the ID + /// of the curve evaluator. + InconsistentEvaluatorImplementation(TypeId), } /// An animation that an [`AnimationPlayer`] is currently either playing or was @@ -471,12 +479,8 @@ pub enum AnimationEvaluationError { pub struct ActiveAnimation { /// The factor by which the weight from the [`AnimationGraph`] is multiplied. weight: f32, - /// The actual weight of this animation this frame, taking the - /// [`AnimationGraph`] into account. - computed_weight: f32, /// The mask groups that are masked out (i.e. won't be animated) this frame, /// taking the `AnimationGraph` into account. - computed_mask: AnimationMask, repeat: RepeatAnimation, speed: f32, /// Total time the animation has been played. @@ -497,8 +501,6 @@ impl Default for ActiveAnimation { fn default() -> Self { Self { weight: 1.0, - computed_weight: 1.0, - computed_mask: 0, repeat: RepeatAnimation::default(), speed: 1.0, elapsed: 0.0, @@ -658,9 +660,7 @@ impl ActiveAnimation { #[derive(Component, Default, Reflect)] #[reflect(Component, Default)] pub struct AnimationPlayer { - /// We use a `BTreeMap` instead of a `HashMap` here to ensure a consistent - /// ordering when applying the animations. - active_animations: BTreeMap, + active_animations: HashMap, blend_weights: HashMap, } @@ -679,27 +679,29 @@ impl Clone for AnimationPlayer { } } -/// Information needed during the traversal of the animation graph in -/// [`advance_animations`]. +/// Temporary data that the [`animate_targets`] system maintains. #[derive(Default)] -pub struct AnimationGraphEvaluator { - /// The stack used for the depth-first search of the graph. - dfs_stack: Vec, - /// The list of visited nodes during the depth-first traversal. - dfs_visited: FixedBitSet, - /// Accumulated weights and masks for each node. - nodes: Vec, -} +pub struct AnimationEvaluationState { + /// Stores all [`AnimationCurveEvaluator`]s corresponding to properties that + /// we've seen so far. + /// + /// This is a mapping from the type ID of an animation curve evaluator to + /// the animation curve evaluator itself. + /// + /// For efficiency's sake, the [`AnimationCurveEvaluator`]s are cached from + /// frame to frame and animation target to animation target. Therefore, + /// there may be entries in this list corresponding to properties that the + /// current [`AnimationPlayer`] doesn't animate. To iterate only over the + /// properties that are currently being animated, consult the + /// [`Self::current_curve_evaluator_types`] set. + curve_evaluators: TypeIdMap>, -/// The accumulated weight and computed mask for a single node. -#[derive(Clone, Copy, Default, Debug)] -struct EvaluatedAnimationGraphNode { - /// The weight that has been accumulated for this node, taking its - /// ancestors' weights into account. - weight: f32, - /// The mask that has been computed for this node, taking its ancestors' - /// masks into account. - mask: AnimationMask, + /// The set of [`AnimationCurveEvaluator`] types that the current + /// [`AnimationPlayer`] is animating. + /// + /// This is built up as new curve evaluators are encountered during graph + /// traversal. + current_curve_evaluator_types: TypeIdMap<()>, } impl AnimationPlayer { @@ -845,7 +847,6 @@ pub fn advance_animations( animation_clips: Res>, animation_graphs: Res>, mut players: Query<(&mut AnimationPlayer, &Handle)>, - animation_graph_evaluator: Local>>, ) { let delta_seconds = time.delta_seconds(); players @@ -856,40 +857,15 @@ pub fn advance_animations( }; // Tick animations, and schedule them. - // - // We use a thread-local here so we can reuse allocations across - // frames. - let mut evaluator = animation_graph_evaluator.get_or_default().borrow_mut(); let AnimationPlayer { ref mut active_animations, - ref blend_weights, .. } = *player; - // Reset our state. - evaluator.reset(animation_graph.root, animation_graph.graph.node_count()); - - while let Some(node_index) = evaluator.dfs_stack.pop() { - // Skip if we've already visited this node. - if evaluator.dfs_visited.put(node_index.index()) { - continue; - } - + for node_index in animation_graph.graph.node_indices() { let node = &animation_graph[node_index]; - // Calculate weight and mask from the graph. - let (mut weight, mut mask) = (node.weight, node.mask); - for parent_index in animation_graph - .graph - .neighbors_directed(node_index, Direction::Incoming) - { - let evaluated_parent = &evaluator.nodes[parent_index.index()]; - weight *= evaluated_parent.weight; - mask |= evaluated_parent.mask; - } - evaluator.nodes[node_index.index()] = EvaluatedAnimationGraphNode { weight, mask }; - if let Some(active_animation) = active_animations.get_mut(&node_index) { // Tick the animation if necessary. if !active_animation.paused { @@ -899,24 +875,7 @@ pub fn advance_animations( } } } - - weight *= active_animation.weight; - } else if let Some(&blend_weight) = blend_weights.get(&node_index) { - weight *= blend_weight; } - - // Write in the computed weight and mask for this node. - if let Some(active_animation) = active_animations.get_mut(&node_index) { - active_animation.computed_weight = weight; - active_animation.computed_mask = mask; - } - - // Push children. - evaluator.dfs_stack.extend( - animation_graph - .graph - .neighbors_directed(node_index, Direction::Outgoing), - ); } }); } @@ -937,13 +896,15 @@ pub type AnimationEntityMut<'w> = EntityMutExcept< pub fn animate_targets( clips: Res>, graphs: Res>, + threaded_animation_graphs: Res, players: Query<(&AnimationPlayer, &Handle)>, mut targets: Query<(&AnimationTarget, Option<&mut Transform>, AnimationEntityMut)>, + animation_evaluation_state: Local>>, ) { // Evaluate all animation targets in parallel. targets .par_iter_mut() - .for_each(|(target, mut transform, mut entity_mut)| { + .for_each(|(target, transform, entity_mut)| { let &AnimationTarget { id: target_id, player: player_id, @@ -955,7 +916,7 @@ pub fn animate_targets( } else { trace!( "Either an animation player {:?} or a graph was missing for the target \ - entity {:?} ({:?}); no animations will play this frame", + entity {:?} ({:?}); no animations will play this frame", player_id, entity_mut.id(), entity_mut.get::(), @@ -968,6 +929,12 @@ pub fn animate_targets( return; }; + let Some(threaded_animation_graph) = + threaded_animation_graphs.0.get(&animation_graph_id) + else { + return; + }; + // Determine which mask groups this animation target belongs to. let target_mask = animation_graph .mask_groups @@ -975,63 +942,104 @@ pub fn animate_targets( .cloned() .unwrap_or_default(); - // Apply the animations one after another. The way we accumulate - // weights ensures that the order we apply them in doesn't matter. - // - // Proof: Consider three animations A₀, A₁, A₂, … with weights w₀, - // w₁, w₂, … respectively. We seek the value: - // - // A₀w₀ + A₁w₁ + A₂w₂ + ⋯ - // - // Defining lerp(a, b, t) = a + t(b - a), we have: - // - // ⎛ ⎛ w₁ ⎞ w₂ ⎞ - // A₀w₀ + A₁w₁ + A₂w₂ + ⋯ = ⋯ lerp⎜lerp⎜A₀, A₁, ⎯⎯⎯⎯⎯⎯⎯⎯⎟, A₂, ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎟ ⋯ - // ⎝ ⎝ w₀ + w₁⎠ w₀ + w₁ + w₂⎠ - // - // Each step of the following loop corresponds to one of the lerp - // operations above. - let mut total_weight = 0.0; - for (&animation_graph_node_index, active_animation) in - animation_player.active_animations.iter() - { - // If the weight is zero or the current animation target is - // masked out, stop here. - if active_animation.weight == 0.0 - || (target_mask & active_animation.computed_mask) != 0 - { - continue; - } + let mut evaluation_state = animation_evaluation_state.get_or_default().borrow_mut(); + let evaluation_state = &mut *evaluation_state; - let Some(clip) = animation_graph - .get(animation_graph_node_index) - .and_then(|animation_graph_node| animation_graph_node.clip.as_ref()) - .and_then(|animation_clip_handle| clips.get(animation_clip_handle)) + // Evaluate the graph. + for &animation_graph_node_index in threaded_animation_graph.threaded_graph.iter() { + let Some(animation_graph_node) = animation_graph.get(animation_graph_node_index) else { continue; }; - let Some(curves) = clip.curves_for_target(target_id) else { - continue; - }; + match animation_graph_node.clip { + None => { + // This is a blend node. + for edge_index in threaded_animation_graph.sorted_edge_ranges + [animation_graph_node_index.index()] + .clone() + { + if let Err(err) = evaluation_state.blend_all( + threaded_animation_graph.sorted_edges[edge_index as usize], + ) { + warn!("Failed to blend animation: {:?}", err); + } + } - let weight = active_animation.computed_weight; - total_weight += weight; + if let Err(err) = evaluation_state.push_blend_register_all( + animation_graph_node.weight, + animation_graph_node_index, + ) { + warn!("Animation blending failed: {:?}", err); + } + } - let weight = weight / total_weight; - let seek_time = active_animation.seek_time; + Some(ref animation_clip_handle) => { + // This is a clip node. + let Some(active_animation) = animation_player + .active_animations + .get(&animation_graph_node_index) + else { + continue; + }; - for curve in curves { - if let Err(err) = curve.0.apply( - seek_time, - transform.as_mut().map(|transform| transform.reborrow()), - entity_mut.reborrow(), - weight, - ) { - warn!("Animation application failed: {:?}", err); + // If the weight is zero or the current animation target is + // masked out, stop here. + if active_animation.weight == 0.0 + || (target_mask + & threaded_animation_graph.computed_masks + [animation_graph_node_index.index()]) + != 0 + { + continue; + } + + let Some(clip) = clips.get(animation_clip_handle) else { + continue; + }; + + let Some(curves) = clip.curves_for_target(target_id) else { + continue; + }; + + let weight = active_animation.weight; + let seek_time = active_animation.seek_time; + + for curve in curves { + // Fetch the curve evaluator. Curve evaluator types + // are unique to each property, but shared among all + // curve types. For example, given two curve types A + // and B, `RotationCurve` and `RotationCurve` + // will both yield a `RotationCurveEvaluator` and + // therefore will share the same evaluator in this + // table. + let curve_evaluator_type_id = (*curve.0).evaluator_type(); + let curve_evaluator = evaluation_state + .curve_evaluators + .entry(curve_evaluator_type_id) + .or_insert_with(|| curve.0.create_evaluator()); + + evaluation_state + .current_curve_evaluator_types + .insert(curve_evaluator_type_id, ()); + + if let Err(err) = AnimationCurve::apply( + &*curve.0, + &mut **curve_evaluator, + seek_time, + weight, + animation_graph_node_index, + ) { + warn!("Animation application failed: {:?}", err); + } + } } } } + + if let Err(err) = evaluation_state.commit_all(transform, entity_mut) { + warn!("Animation application failed: {:?}", err); + } }); } @@ -1050,9 +1058,12 @@ impl Plugin for AnimationPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() + .init_resource::() .add_systems( PostUpdate, ( + graph::thread_animation_graphs, advance_transitions, advance_animations, // TODO: `animate_targets` can animate anything, so @@ -1100,17 +1111,63 @@ impl From<&Name> for AnimationTargetId { } } -impl AnimationGraphEvaluator { - // Starts a new depth-first search. - fn reset(&mut self, root: AnimationNodeIndex, node_count: usize) { - self.dfs_stack.clear(); - self.dfs_stack.push(root); +impl AnimationEvaluationState { + /// Calls [`AnimationCurveEvaluator::blend`] on all curve evaluator types + /// that we've been building up for a single target. + /// + /// The given `node_index` is the node that we're evaluating. + fn blend_all( + &mut self, + node_index: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError> { + for curve_evaluator_type in self.current_curve_evaluator_types.keys() { + self.curve_evaluators + .get_mut(curve_evaluator_type) + .unwrap() + .blend(node_index)?; + } + Ok(()) + } - self.dfs_visited.grow(node_count); - self.dfs_visited.clear(); + /// Calls [`AnimationCurveEvaluator::push_blend_register`] on all curve + /// evaluator types that we've been building up for a single target. + /// + /// The `weight` parameter is the weight that should be pushed onto the + /// stack, while the `node_index` parameter is the node that we're + /// evaluating. + fn push_blend_register_all( + &mut self, + weight: f32, + node_index: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError> { + for curve_evaluator_type in self.current_curve_evaluator_types.keys() { + self.curve_evaluators + .get_mut(curve_evaluator_type) + .unwrap() + .push_blend_register(weight, node_index)?; + } + Ok(()) + } - self.nodes.clear(); - self.nodes - .extend(iter::repeat(EvaluatedAnimationGraphNode::default()).take(node_count)); + /// Calls [`AnimationCurveEvaluator::commit`] on all curve evaluator types + /// that we've been building up for a single target. + /// + /// This is the call that actually writes the computed values into the + /// components being animated. + fn commit_all( + &mut self, + mut transform: Option>, + mut entity_mut: AnimationEntityMut, + ) -> Result<(), AnimationEvaluationError> { + for (curve_evaluator_type, _) in self.current_curve_evaluator_types.drain() { + self.curve_evaluators + .get_mut(&curve_evaluator_type) + .unwrap() + .commit( + transform.as_mut().map(|transform| transform.reborrow()), + entity_mut.reborrow(), + )?; + } + Ok(()) } } diff --git a/examples/animation/animation_graph.rs b/examples/animation/animation_graph.rs index 33a30a8879..4336151fef 100644 --- a/examples/animation/animation_graph.rs +++ b/examples/animation/animation_graph.rs @@ -47,24 +47,24 @@ static NODE_RECTS: [NodeRect; 5] = [ NodeRect::new(10.00, 10.00, 97.64, 48.41), NodeRect::new(10.00, 78.41, 97.64, 48.41), NodeRect::new(286.08, 78.41, 97.64, 48.41), - NodeRect::new(148.04, 44.20, 97.64, 48.41), + NodeRect::new(148.04, 112.61, 97.64, 48.41), // was 44.20 NodeRect::new(10.00, 146.82, 97.64, 48.41), ]; /// The positions of the horizontal lines in the UI. static HORIZONTAL_LINES: [Line; 6] = [ - Line::new(107.64, 34.21, 20.20), + Line::new(107.64, 34.21, 158.24), Line::new(107.64, 102.61, 20.20), - Line::new(107.64, 171.02, 158.24), - Line::new(127.84, 68.41, 20.20), - Line::new(245.68, 68.41, 20.20), + Line::new(107.64, 171.02, 20.20), + Line::new(127.84, 136.82, 20.20), + Line::new(245.68, 136.82, 20.20), Line::new(265.88, 102.61, 20.20), ]; /// The positions of the vertical lines in the UI. static VERTICAL_LINES: [Line; 2] = [ - Line::new(127.83, 34.21, 68.40), - Line::new(265.88, 68.41, 102.61), + Line::new(127.83, 102.61, 68.40), + Line::new(265.88, 34.21, 102.61), ]; /// Initializes the app.