From 2488ffa681e0d5694d53122d99b576ce58ae945d Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sat, 5 Oct 2024 05:49:20 -0400 Subject: [PATCH 01/17] Basic idea --- crates/bevy_animation/src/animation_curves.rs | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index e4a6e46734e14..4029dd84ee146 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -825,6 +825,87 @@ impl AnimationCurveEvaluator for WeightsCurveEvaluator { } } +#[derive(Default, Debug, Reflect)] +struct TransformCurveEvaluator { + stack: Vec, + blend_register: Option<(TransformParts, f32)>, +} + +#[derive(Default, Debug, Clone, Reflect)] +struct TransformParts { + translation: Option, + rotation: Option, + scale: Option, +} + +fn interpolate_option(a: Option<&A>, b: Option<&A>, time: f32) -> Option { + match (a, b) { + (None, None) => None, + (Some(a), None) => Some(*a), + (None, Some(b)) => Some(*b), + (Some(a), Some(b)) => Some(A::interpolate(a, b, time)), + } +} + +impl Animatable for TransformParts { + fn interpolate(a: &Self, b: &Self, time: f32) -> Self { + TransformParts { + translation: interpolate_option(a.translation.as_ref(), b.translation.as_ref(), time), + rotation: interpolate_option(a.rotation.as_ref(), b.rotation.as_ref(), time), + scale: interpolate_option(a.scale.as_ref(), b.scale.as_ref(), time), + } + } + + fn blend(inputs: impl Iterator>) -> Self { + let mut accum = TransformParts::default(); + for BlendInput { + weight, + value, + additive, + } in inputs + { + let TransformParts { + translation, + rotation, + scale, + } = value; + if additive { + } else { + } + } + } +} + +#[derive(Default, Debug, Reflect)] +struct TransformStackElement { + value: TransformParts, + weight: f32, + graph_node: AnimationNodeIndex, +} + +impl TransformCurveEvaluator { + fn combine( + &mut self, + graph_node: AnimationNodeIndex, + additive: bool, + ) -> Result<(), AnimationEvaluationError> { + let Some(top) = self.stack.last() else { + return Ok(()); + }; + if top.graph_node != graph_node { + return Ok(()); + } + + let TransformStackElement { + value: value_to_blend, + weight: weight_to_blend, + graph_node: _, + } = self.stack.pop().unwrap(); + + Ok(()) + } +} + #[derive(Reflect, FromReflect)] #[reflect(from_reflect = false)] struct BasicAnimationCurveEvaluator From 5fb81dbda7e3fbf3ccaeb95358b35589af122a07 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sat, 5 Oct 2024 08:02:46 -0400 Subject: [PATCH 02/17] Finish TransformParts: Animatable --- crates/bevy_animation/src/animation_curves.rs | 68 ++++++++----------- 1 file changed, 30 insertions(+), 38 deletions(-) diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index 4029dd84ee146..26e05f0f2972f 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -825,10 +825,9 @@ impl AnimationCurveEvaluator for WeightsCurveEvaluator { } } -#[derive(Default, Debug, Reflect)] +#[derive(Default, Reflect)] struct TransformCurveEvaluator { - stack: Vec, - blend_register: Option<(TransformParts, f32)>, + evaluator: BasicAnimationCurveEvaluator, } #[derive(Default, Debug, Clone, Reflect)] @@ -864,45 +863,38 @@ impl Animatable for TransformParts { additive, } in inputs { - let TransformParts { - translation, - rotation, - scale, - } = value; if additive { + let Self { + translation, + rotation, + scale, + } = value; + if let Some(translation) = translation { + let weighted_translation = translation * weight; + match accum.translation { + Some(mut v) => v += weighted_translation, + None => accum.translation = Some(weighted_translation), + } + } + if let Some(rotation) = rotation { + let weighted_rotation = Quat::slerp(Quat::IDENTITY, rotation, weight); + match accum.rotation { + Some(mut r) => r = weighted_rotation * r, + None => accum.rotation = Some(weighted_rotation), + } + } + if let Some(scale) = scale { + let weighted_scale = scale * weight; + match accum.scale { + Some(mut s) => s += weighted_scale, + None => accum.scale = Some(weighted_scale), + } + } } else { + accum = Self::interpolate(&accum, &value, weight); } } - } -} - -#[derive(Default, Debug, Reflect)] -struct TransformStackElement { - value: TransformParts, - weight: f32, - graph_node: AnimationNodeIndex, -} - -impl TransformCurveEvaluator { - fn combine( - &mut self, - graph_node: AnimationNodeIndex, - additive: bool, - ) -> Result<(), AnimationEvaluationError> { - let Some(top) = self.stack.last() else { - return Ok(()); - }; - if top.graph_node != graph_node { - return Ok(()); - } - - let TransformStackElement { - value: value_to_blend, - weight: weight_to_blend, - graph_node: _, - } = self.stack.pop().unwrap(); - - Ok(()) + accum } } From 60e212406f491cf59517055486504f644101d79e Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sat, 5 Oct 2024 08:27:40 -0400 Subject: [PATCH 03/17] Flesh out TransformParts, TransformCurveEvaluator --- crates/bevy_animation/src/animatable.rs | 116 ++++++++++++++++++ crates/bevy_animation/src/animation_curves.rs | 89 +++++--------- 2 files changed, 145 insertions(+), 60 deletions(-) diff --git a/crates/bevy_animation/src/animatable.rs b/crates/bevy_animation/src/animatable.rs index 298d0125a208c..826f4fd689fcf 100644 --- a/crates/bevy_animation/src/animatable.rs +++ b/crates/bevy_animation/src/animatable.rs @@ -5,6 +5,7 @@ use bevy_color::{Laba, LinearRgba, Oklaba, Srgba, Xyza}; use bevy_math::*; use bevy_reflect::Reflect; use bevy_transform::prelude::Transform; +use bevy_utils::default; /// An individual input for [`Animatable::blend`]. pub struct BlendInput { @@ -188,6 +189,121 @@ impl Animatable for Quat { } } +#[derive(Default, Debug, Clone, Reflect)] +pub(crate) struct TransformParts { + translation: Option, + rotation: Option, + scale: Option, +} + +impl TransformParts { + pub(crate) fn apply_to_transform(self, transform: &mut Transform) { + if let Some(translation) = self.translation { + transform.translation = translation; + } + if let Some(rotation) = self.rotation { + transform.rotation = rotation; + } + if let Some(scale) = self.scale { + transform.scale = scale; + } + } + + #[inline] + pub(crate) fn from_translation(translation: Vec3) -> Self { + Self { + translation: Some(translation), + ..default() + } + } + + #[inline] + pub(crate) fn from_rotation(rotation: Quat) -> Self { + Self { + rotation: Some(rotation), + ..default() + } + } + + #[inline] + pub(crate) fn from_scale(scale: Vec3) -> Self { + Self { + scale: Some(scale), + ..default() + } + } + + #[inline] + pub(crate) fn from_transform(transform: Transform) -> Self { + Self { + translation: Some(transform.translation), + rotation: Some(transform.rotation), + scale: Some(transform.scale), + } + } +} + +fn interpolate_option(a: Option<&A>, b: Option<&A>, time: f32) -> Option { + match (a, b) { + (None, None) => None, + (Some(a), None) => Some(*a), + (None, Some(b)) => Some(*b), + (Some(a), Some(b)) => Some(A::interpolate(a, b, time)), + } +} + +impl Animatable for TransformParts { + fn interpolate(a: &Self, b: &Self, time: f32) -> Self { + TransformParts { + translation: interpolate_option(a.translation.as_ref(), b.translation.as_ref(), time), + rotation: interpolate_option(a.rotation.as_ref(), b.rotation.as_ref(), time), + scale: interpolate_option(a.scale.as_ref(), b.scale.as_ref(), time), + } + } + + fn blend(inputs: impl Iterator>) -> Self { + let mut accum = TransformParts::default(); + for BlendInput { + weight, + value, + additive, + } in inputs + { + if additive { + let Self { + translation, + rotation, + scale, + } = value; + if let Some(translation) = translation { + let weighted_translation = translation * weight; + match accum.translation { + Some(ref mut v) => *v += weighted_translation, + None => accum.translation = Some(weighted_translation), + } + } + if let Some(rotation) = rotation { + let weighted_rotation = Quat::slerp(Quat::IDENTITY, rotation, weight); + match accum.rotation { + Some(ref mut r) => *r = weighted_rotation * *r, + None => accum.rotation = Some(weighted_rotation), + } + } + if let Some(scale) = scale { + let weighted_scale = scale * weight; + match accum.scale { + Some(ref mut s) => *s += weighted_scale, + None => accum.scale = Some(weighted_scale), + } + } + } else { + accum = Self::interpolate(&accum, &value, weight); + } + } + accum + } +} + /// Evaluates a cubic Bézier curve at a value `t`, given two endpoints and the /// derivatives at those endpoints. /// diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index 26e05f0f2972f..62a23302c18db 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -96,6 +96,7 @@ use bevy_render::mesh::morph::MorphWeights; use bevy_transform::prelude::Transform; use crate::{ + animatable::TransformParts, graph::AnimationNodeIndex, prelude::{Animatable, BlendInput}, AnimationEntityMut, AnimationEvaluationError, @@ -830,71 +831,39 @@ struct TransformCurveEvaluator { evaluator: BasicAnimationCurveEvaluator, } -#[derive(Default, Debug, Clone, Reflect)] -struct TransformParts { - translation: Option, - rotation: Option, - scale: Option, -} +impl AnimationCurveEvaluator for TransformCurveEvaluator { + fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { + self.evaluator.combine(graph_node, /*additive=*/ false) + } -fn interpolate_option(a: Option<&A>, b: Option<&A>, time: f32) -> Option { - match (a, b) { - (None, None) => None, - (Some(a), None) => Some(*a), - (None, Some(b)) => Some(*b), - (Some(a), Some(b)) => Some(A::interpolate(a, b, time)), + fn add(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { + self.evaluator.combine(graph_node, /*additive=*/ true) } -} -impl Animatable for TransformParts { - fn interpolate(a: &Self, b: &Self, time: f32) -> Self { - TransformParts { - translation: interpolate_option(a.translation.as_ref(), b.translation.as_ref(), time), - rotation: interpolate_option(a.rotation.as_ref(), b.rotation.as_ref(), time), - scale: interpolate_option(a.scale.as_ref(), b.scale.as_ref(), time), - } + fn push_blend_register( + &mut self, + weight: f32, + graph_node: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError> { + self.evaluator.push_blend_register(weight, graph_node) } - fn blend(inputs: impl Iterator>) -> Self { - let mut accum = TransformParts::default(); - for BlendInput { - weight, - value, - additive, - } in inputs - { - if additive { - let Self { - translation, - rotation, - scale, - } = value; - if let Some(translation) = translation { - let weighted_translation = translation * weight; - match accum.translation { - Some(mut v) => v += weighted_translation, - None => accum.translation = Some(weighted_translation), - } - } - if let Some(rotation) = rotation { - let weighted_rotation = Quat::slerp(Quat::IDENTITY, rotation, weight); - match accum.rotation { - Some(mut r) => r = weighted_rotation * r, - None => accum.rotation = Some(weighted_rotation), - } - } - if let Some(scale) = scale { - let weighted_scale = scale * weight; - match accum.scale { - Some(mut s) => s += weighted_scale, - None => accum.scale = Some(weighted_scale), - } - } - } else { - accum = Self::interpolate(&accum, &value, weight); - } - } - accum + fn commit<'a>( + &mut self, + transform: Option>, + _: AnimationEntityMut<'a>, + ) -> Result<(), AnimationEvaluationError> { + let mut component = transform.ok_or_else(|| { + AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) + })?; + let parts = self + .evaluator + .stack + .pop() + .ok_or_else(inconsistent::)? + .value; + parts.apply_to_transform(&mut component); + Ok(()) } } From f0b86c0835b5e2842b93d631debd0f4b819e5093 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sat, 5 Oct 2024 09:06:06 -0400 Subject: [PATCH 04/17] Migrate to shared blend Transform evaluator --- crates/bevy_animation/src/animation_curves.rs | 165 ++---------------- 1 file changed, 12 insertions(+), 153 deletions(-) diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index 62a23302c18db..87007ab3dc593 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -343,16 +343,6 @@ 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, @@ -366,13 +356,11 @@ where } fn evaluator_type(&self) -> TypeId { - TypeId::of::() + TypeId::of::() } fn create_evaluator(&self) -> Box { - Box::new(TranslationCurveEvaluator { - evaluator: BasicAnimationCurveEvaluator::default(), - }) + Box::new(TransformCurveEvaluator::default()) } fn apply( @@ -383,9 +371,9 @@ where graph_node: AnimationNodeIndex, ) -> Result<(), AnimationEvaluationError> { let curve_evaluator = (*Reflect::as_any_mut(curve_evaluator)) - .downcast_mut::() + .downcast_mut::() .unwrap(); - let value = self.0.sample_clamped(t); + let value = TransformParts::from_translation(self.0.sample_clamped(t)); curve_evaluator .evaluator .stack @@ -398,41 +386,6 @@ where } } -impl AnimationCurveEvaluator for TranslationCurveEvaluator { - fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { - self.evaluator.combine(graph_node, /*additive=*/ false) - } - - fn add(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { - self.evaluator.combine(graph_node, /*additive=*/ true) - } - - 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::()) - })?; - component.translation = self - .evaluator - .stack - .pop() - .ok_or_else(inconsistent::)? - .value; - Ok(()) - } -} - /// This type allows a [curve] valued in `Quat` to become an [`AnimationCurve`] that animates /// the rotation component of a transform. /// @@ -441,16 +394,6 @@ impl AnimationCurveEvaluator for TranslationCurveEvaluator { #[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, @@ -464,13 +407,11 @@ where } fn evaluator_type(&self) -> TypeId { - TypeId::of::() + TypeId::of::() } fn create_evaluator(&self) -> Box { - Box::new(RotationCurveEvaluator { - evaluator: BasicAnimationCurveEvaluator::default(), - }) + Box::new(TransformCurveEvaluator::default()) } fn apply( @@ -481,9 +422,9 @@ where graph_node: AnimationNodeIndex, ) -> Result<(), AnimationEvaluationError> { let curve_evaluator = (*Reflect::as_any_mut(curve_evaluator)) - .downcast_mut::() + .downcast_mut::() .unwrap(); - let value = self.0.sample_clamped(t); + let value = TransformParts::from_rotation(self.0.sample_clamped(t)); curve_evaluator .evaluator .stack @@ -496,41 +437,6 @@ where } } -impl AnimationCurveEvaluator for RotationCurveEvaluator { - fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { - self.evaluator.combine(graph_node, /*additive=*/ false) - } - - fn add(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { - self.evaluator.combine(graph_node, /*additive=*/ true) - } - - 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::()) - })?; - component.rotation = self - .evaluator - .stack - .pop() - .ok_or_else(inconsistent::)? - .value; - Ok(()) - } -} - /// This type allows a [curve] valued in `Vec3` to become an [`AnimationCurve`] that animates /// the scale component of a transform. /// @@ -539,16 +445,6 @@ impl AnimationCurveEvaluator for RotationCurveEvaluator { #[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, @@ -562,13 +458,11 @@ where } fn evaluator_type(&self) -> TypeId { - TypeId::of::() + TypeId::of::() } fn create_evaluator(&self) -> Box { - Box::new(ScaleCurveEvaluator { - evaluator: BasicAnimationCurveEvaluator::default(), - }) + Box::new(TransformCurveEvaluator::default()) } fn apply( @@ -579,9 +473,9 @@ where graph_node: AnimationNodeIndex, ) -> Result<(), AnimationEvaluationError> { let curve_evaluator = (*Reflect::as_any_mut(curve_evaluator)) - .downcast_mut::() + .downcast_mut::() .unwrap(); - let value = self.0.sample_clamped(t); + let value = TransformParts::from_scale(self.0.sample_clamped(t)); curve_evaluator .evaluator .stack @@ -594,41 +488,6 @@ where } } -impl AnimationCurveEvaluator for ScaleCurveEvaluator { - fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { - self.evaluator.combine(graph_node, /*additive=*/ false) - } - - fn add(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { - self.evaluator.combine(graph_node, /*additive=*/ true) - } - - 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::()) - })?; - component.scale = self - .evaluator - .stack - .pop() - .ok_or_else(inconsistent::)? - .value; - Ok(()) - } -} - /// This type allows an [`IterableCurve`] valued in `f32` to be used as an [`AnimationCurve`] /// that animates [morph weights]. /// From 2b785189a705a8a20489dc0750aaa842dc54d7a1 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sat, 5 Oct 2024 09:10:45 -0400 Subject: [PATCH 05/17] Reintroduce TransformCurve --- crates/bevy_animation/src/animation_curves.rs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index 87007ab3dc593..a6dd62e37b0fe 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -488,6 +488,57 @@ where } } +/// This type allows a [curve] valued in `Transform` to become an [`AnimationCurve`] that animates +/// the entire transform. +/// +/// [curve]: Curve +#[derive(Debug, Clone, Reflect, FromReflect)] +#[reflect(from_reflect = false)] +pub struct TransformCurve(pub C); + +impl AnimationCurve for TransformCurve +where + C: AnimationCompatibleCurve, +{ + fn clone_value(&self) -> Box { + Box::new(self.clone()) + } + + fn domain(&self) -> Interval { + self.0.domain() + } + + fn evaluator_type(&self) -> TypeId { + TypeId::of::() + } + + fn create_evaluator(&self) -> Box { + Box::new(TransformCurveEvaluator::default()) + } + + fn apply( + &self, + curve_evaluator: &mut dyn AnimationCurveEvaluator, + t: f32, + weight: f32, + graph_node: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError> { + let curve_evaluator = (*Reflect::as_any_mut(curve_evaluator)) + .downcast_mut::() + .unwrap(); + let value = TransformParts::from_transform(self.0.sample_clamped(t)); + curve_evaluator + .evaluator + .stack + .push(BasicAnimationCurveEvaluatorStackElement { + value, + weight, + graph_node, + }); + Ok(()) + } +} + /// This type allows an [`IterableCurve`] valued in `f32` to be used as an [`AnimationCurve`] /// that animates [morph weights]. /// From f03f234e49d4afd53c3e2a760cfda217cb074bb7 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sat, 5 Oct 2024 09:37:26 -0400 Subject: [PATCH 06/17] Return TransformCurve to docs --- crates/bevy_animation/src/animation_curves.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index a6dd62e37b0fe..3a8653841cfe6 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -65,6 +65,7 @@ //! - [`TranslationCurve`], which uses `Vec3` output to animate [`Transform::translation`] //! - [`RotationCurve`], which uses `Quat` output to animate [`Transform::rotation`] //! - [`ScaleCurve`], which uses `Vec3` output to animate [`Transform::scale`] +//! - [`TransformCurve`], which uses `Transform` output to animate `Transform` directly //! //! ## Animatable properties //! From df76a1e9178a2a16d3978d227792918596d8411d Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sat, 5 Oct 2024 09:45:56 -0400 Subject: [PATCH 07/17] Use more idiomatic curve type in animated_transform --- examples/animation/animated_transform.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/animation/animated_transform.rs b/examples/animation/animated_transform.rs index b6ca201cc5917..6b53a4e3ff94c 100644 --- a/examples/animation/animated_transform.rs +++ b/examples/animation/animated_transform.rs @@ -52,7 +52,7 @@ fn setup( let planet_animation_target_id = AnimationTargetId::from_name(&planet); animation.add_curve_to_target( planet_animation_target_id, - UnevenSampleAutoCurve::new([0.0, 1.0, 2.0, 3.0, 4.0].into_iter().zip([ + AnimatableKeyframeCurve::new([0.0, 1.0, 2.0, 3.0, 4.0].into_iter().zip([ Vec3::new(1.0, 0.0, 1.0), Vec3::new(-1.0, 0.0, 1.0), Vec3::new(-1.0, 0.0, -1.0), @@ -71,7 +71,7 @@ fn setup( AnimationTargetId::from_names([planet.clone(), orbit_controller.clone()].iter()); animation.add_curve_to_target( orbit_controller_animation_target_id, - UnevenSampleAutoCurve::new([0.0, 1.0, 2.0, 3.0, 4.0].into_iter().zip([ + AnimatableKeyframeCurve::new([0.0, 1.0, 2.0, 3.0, 4.0].into_iter().zip([ Quat::IDENTITY, Quat::from_axis_angle(Vec3::Y, PI / 2.), Quat::from_axis_angle(Vec3::Y, PI / 2. * 2.), @@ -89,7 +89,7 @@ fn setup( ); animation.add_curve_to_target( satellite_animation_target_id, - UnevenSampleAutoCurve::new( + AnimatableKeyframeCurve::new( [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0] .into_iter() .zip([ @@ -112,7 +112,7 @@ fn setup( AnimationTargetId::from_names( [planet.clone(), orbit_controller.clone(), satellite.clone()].iter(), ), - UnevenSampleAutoCurve::new([0.0, 1.0, 2.0, 3.0, 4.0].into_iter().zip([ + AnimatableKeyframeCurve::new([0.0, 1.0, 2.0, 3.0, 4.0].into_iter().zip([ Quat::IDENTITY, Quat::from_axis_angle(Vec3::Y, PI / 2.), Quat::from_axis_angle(Vec3::Y, PI / 2. * 2.), From e25a73fd5c3e1e436f63533d099f4a8fbd0a42dd Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sun, 6 Oct 2024 10:50:54 -0400 Subject: [PATCH 08/17] Document TransformParts --- crates/bevy_animation/src/animatable.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/bevy_animation/src/animatable.rs b/crates/bevy_animation/src/animatable.rs index 826f4fd689fcf..0607452ee6b14 100644 --- a/crates/bevy_animation/src/animatable.rs +++ b/crates/bevy_animation/src/animatable.rs @@ -189,6 +189,9 @@ impl Animatable for Quat { } } +/// Basically `Transform`, but with every part optional. Note that this is the true +/// output of `Transform` animation, since individual parts may be unconstrained +/// if they lack an animation curve to control them. #[derive(Default, Debug, Clone, Reflect)] pub(crate) struct TransformParts { translation: Option, From cb016c71057f175cfd74a5364817e1cf8554a5cb Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sun, 6 Oct 2024 12:25:37 -0400 Subject: [PATCH 09/17] Clean up merge --- crates/bevy_animation/src/animation_curves.rs | 41 ------------------- 1 file changed, 41 deletions(-) diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index d238f2028b839..a757d6d619a12 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -776,47 +776,6 @@ impl AnimationCurveEvaluator for TransformCurveEvaluator { } } -#[derive(Default, Reflect)] -struct TransformCurveEvaluator { - evaluator: BasicAnimationCurveEvaluator, -} - -impl AnimationCurveEvaluator for TransformCurveEvaluator { - fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { - self.evaluator.combine(graph_node, /*additive=*/ false) - } - - fn add(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { - self.evaluator.combine(graph_node, /*additive=*/ true) - } - - 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 parts = self - .evaluator - .stack - .pop() - .ok_or_else(inconsistent::)? - .value; - parts.apply_to_transform(&mut component); - Ok(()) - } -} - #[derive(Reflect)] struct BasicAnimationCurveEvaluator where From 581cb0ae704d64b440b328b86b56b29b4153f122 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sun, 6 Oct 2024 17:43:05 -0400 Subject: [PATCH 10/17] Try to fix additive blending --- Cargo.toml | 11 ++ crates/bevy_animation/src/animatable.rs | 31 ++++ crates/bevy_animation/src/animation_curves.rs | 12 +- crates/bevy_animation/src/lib.rs | 37 ++++- examples/README.md | 1 + examples/animation/transform_curve.rs | 156 ++++++++++++++++++ 6 files changed, 241 insertions(+), 7 deletions(-) create mode 100644 examples/animation/transform_curve.rs diff --git a/Cargo.toml b/Cargo.toml index 320ab099775d2..c68eda0a085aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1287,6 +1287,17 @@ description = "Skinned mesh example with mesh and joints data loaded from a glTF category = "Animation" wasm = true +[[example]] +name = "transform_curve" +path = "examples/animation/transform_curve.rs" +doc-scrape-examples = true + +[package.metadata.example.transform_curve] +name = "Transform Curve" +description = "Direct animation of an entity's Transform using curves" +category = "Animation" +wasm = true + # Application [[example]] name = "custom_loop" diff --git a/crates/bevy_animation/src/animatable.rs b/crates/bevy_animation/src/animatable.rs index e5dd9667d1e3f..6fcebe8107c9f 100644 --- a/crates/bevy_animation/src/animatable.rs +++ b/crates/bevy_animation/src/animatable.rs @@ -390,3 +390,34 @@ where let p1p2p3 = T::interpolate(&p1p2, &p2p3, t); T::interpolate(&p0p1p2, &p1p2p3, t) } + +#[cfg(test)] +mod tests { + use super::Animatable; + use super::TransformParts; + use crate::prelude::BlendInput; + use bevy_math::vec3; + use bevy_transform::components::Transform; + + #[test] + fn add_parts() { + let parts = TransformParts::from_transform(Transform::from_xyz(1.0, 2.0, 3.0)); + let incoming = TransformParts::from_translation(vec3(1.0, 1.0, 1.0)); + let blend = TransformParts::blend( + [ + BlendInput { + weight: 1.0, + value: parts, + additive: true, + }, + BlendInput { + weight: 0.1, + value: incoming, + additive: true, + }, + ] + .into_iter(), + ); + println!("Blend: {:?}", blend); + } +} diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index a757d6d619a12..c5e9957af55c9 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -832,13 +832,12 @@ where match self.blend_register.take() { None => self.blend_register = Some((value_to_blend, weight_to_blend)), Some((mut current_value, mut current_weight)) => { - current_weight += weight_to_blend; - + if additive { current_value = A::blend( [ BlendInput { - weight: 1.0, + weight: current_weight, value: current_value, additive: true, }, @@ -847,9 +846,10 @@ where value: value_to_blend, additive: true, }, - ] - .into_iter(), - ); + ] + .into_iter(), + ); + current_weight += weight_to_blend; } else { current_value = A::interpolate( ¤t_value, diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index b5cc25cfed0da..5e6b35b0a7ccb 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -1098,7 +1098,7 @@ pub fn animate_targets( }; match animation_graph_node.node_type { - AnimationNodeType::Blend | AnimationNodeType::Add => { + AnimationNodeType::Blend => { // This is a blend node. for edge_index in threaded_animation_graph.sorted_edge_ranges [animation_graph_node_index.index()] @@ -1119,6 +1119,27 @@ pub fn animate_targets( } } + AnimationNodeType::Add => { + // This is an additive blend node. + for edge_index in threaded_animation_graph.sorted_edge_ranges + [animation_graph_node_index.index()] + .clone() + { + if let Err(err) = evaluation_state + .add_all(threaded_animation_graph.sorted_edges[edge_index as usize]) + { + warn!("Failed to blend animation: {:?}", err); + } + } + + if let Err(err) = evaluation_state.push_blend_register_all( + animation_graph_node.weight, + animation_graph_node_index, + ) { + warn!("Animation blending failed: {:?}", err); + } + } + AnimationNodeType::Clip(ref animation_clip_handle) => { // This is a clip node. let Some(active_animation) = animation_player @@ -1317,6 +1338,20 @@ impl AnimationEvaluationState { Ok(()) } + /// Calls [`AnimationCurveEvaluator::add`] 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 add_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() + .add(node_index)?; + } + Ok(()) + } + /// Calls [`AnimationCurveEvaluator::push_blend_register`] on all curve /// evaluator types that we've been building up for a single target. /// diff --git a/examples/README.md b/examples/README.md index 7754978c9307d..ff52cdcc45f08 100644 --- a/examples/README.md +++ b/examples/README.md @@ -200,6 +200,7 @@ Example | Description [Cubic Curve](../examples/animation/cubic_curve.rs) | Bezier curve example showing a cube following a cubic curve [Custom Skinned Mesh](../examples/animation/custom_skinned_mesh.rs) | Skinned mesh example with mesh and joints data defined in code [Morph Targets](../examples/animation/morph_targets.rs) | Plays an animation from a glTF file with meshes with morph targets +[Transform Curve](../examples/animation/transform_curve.rs) | Direct animation of an entity's Transform using curves [glTF Skinned Mesh](../examples/animation/gltf_skinned_mesh.rs) | Skinned mesh example with mesh and joints data loaded from a glTF file ## Application diff --git a/examples/animation/transform_curve.rs b/examples/animation/transform_curve.rs new file mode 100644 index 0000000000000..0d6addff13ad3 --- /dev/null +++ b/examples/animation/transform_curve.rs @@ -0,0 +1,156 @@ +//! Create and play an animation defined by a curve valued in `Transform`. + +use bevy::{ + animation::{AnimationTarget, AnimationTargetId}, + math::vec3, + prelude::*, +}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .insert_resource(AmbientLight { + color: Color::WHITE, + brightness: 150.0, + }) + .add_systems(Startup, setup) + .run(); +} + +// 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_indices: Vec, +} + +impl AnimationInfo { + // Programmatically creates the UI animation. + fn create( + animation_graphs: &mut Assets, + animation_clips: &mut Assets, + ) -> AnimationInfo { + // Create an ID that identifies the thing we're going to animate. + let animation_target_name = Name::new("Ship"); + let animation_target_id = AnimationTargetId::from_name(&animation_target_name); + + // Allocate an animation clip. + let mut main_clip = AnimationClip::default(); + + let wobbly_circle_curve = + function_curve(Interval::new(0.0, std::f32::consts::TAU).unwrap(), |t| { + vec3(t.sin() * 5.0, t.sin() * 1.5, t.cos() * 5.0) + }); + + let transform_curve = wobbly_circle_curve.map(|position| { + Transform::from_translation(position).aligned_by( + Dir3::NEG_X, + vec3(0.0, -2.0, 0.0) - position, + Dir3::Y, + Dir3::Y, + ) + }); + + main_clip.add_curve_to_target(animation_target_id, TransformCurve(transform_curve)); + + // Set up an additional additive clip to blend with the first. + let mut additive_clip = AnimationClip::default(); + + let turbulence_curve = + function_curve(Interval::new(0.0, std::f32::consts::TAU).unwrap(), |t| { + vec3(f32::cos(20.0 * t), 0.0, f32::sin(20.0 * t)) + }); + + additive_clip.add_curve_to_target(animation_target_id, TranslationCurve(turbulence_curve)); + + // Save our animation clips as assets. + let main_clip_handle = animation_clips.add(main_clip); + let additive_clip_handle = animation_clips.add(additive_clip); + + let mut node_indices = vec![]; + + // Build the animation graph: + let mut animation_graph = AnimationGraph::new(); + let blend_node = animation_graph.add_additive_blend(1.0, animation_graph.root); + node_indices.push(animation_graph.add_clip(main_clip_handle, 1.0, blend_node)); + node_indices.push(animation_graph.add_clip(additive_clip_handle, 0.01, blend_node)); + + 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_indices, + } + } +} + +fn setup( + mut commands: Commands, + asset_server: Res, + mut animations: ResMut>, + mut graphs: ResMut>, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // Create the animation. + let AnimationInfo { + target_name: animation_target_name, + target_id: animation_target_id, + graph: animation_graph, + node_indices: animation_node_indices, + } = AnimationInfo::create(&mut graphs, &mut animations); + + // Camera + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-4.0, 5.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y), + )); + + // A light source + commands.spawn(( + PointLight { + shadows_enabled: true, + ..default() + }, + Transform::from_xyz(4.0, 7.0, -4.0), + )); + + // A plane that we can use to situate ourselves + commands.spawn(( + Mesh3d(meshes.add(Circle::new(20.0))), + MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), + Transform::from_xyz(0., -2., 0.) + .with_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), + )); + + // Create the animation player, and set it to repeat. + let mut player = AnimationPlayer::default(); + for index in animation_node_indices { + player.play(index).repeat(); + } + + // Finally, our ship that is going to be animated. + let ship_entity = commands + .spawn(( + SceneRoot( + asset_server + .load(GltfAssetLabel::Scene(0).from_asset("models/ship/craft_speederD.gltf")), + ), + animation_target_name, + animation_graph, + player, + )) + .id(); + + commands.entity(ship_entity).insert(AnimationTarget { + id: animation_target_id, + player: ship_entity, + }); +} From 96e4166d4ea28b46d946902726a60a11aa4fd425 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sun, 6 Oct 2024 20:51:39 -0400 Subject: [PATCH 11/17] Use animation graph weight --- crates/bevy_animation/src/animatable.rs | 6 +- crates/bevy_animation/src/animation_curves.rs | 91 ++++++++++++------- crates/bevy_animation/src/lib.rs | 4 +- examples/animation/transform_curve.rs | 10 +- 4 files changed, 69 insertions(+), 42 deletions(-) diff --git a/crates/bevy_animation/src/animatable.rs b/crates/bevy_animation/src/animatable.rs index 6fcebe8107c9f..256fe8494675d 100644 --- a/crates/bevy_animation/src/animatable.rs +++ b/crates/bevy_animation/src/animatable.rs @@ -204,9 +204,9 @@ impl Animatable for Quat { /// if they lack an animation curve to control them. #[derive(Default, Debug, Clone, Reflect)] pub(crate) struct TransformParts { - translation: Option, - rotation: Option, - scale: Option, + pub(crate) translation: Option, + pub(crate) rotation: Option, + pub(crate) scale: Option, } impl TransformParts { diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index c5e9957af55c9..66b3be48a28e4 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -373,15 +373,22 @@ where let curve_evaluator = (*Reflect::as_any_mut(curve_evaluator)) .downcast_mut::() .unwrap(); - let value = TransformParts::from_translation(self.0.sample_clamped(t)); - curve_evaluator - .evaluator - .stack - .push(BasicAnimationCurveEvaluatorStackElement { - value, - weight, - graph_node, - }); + let translation = self.0.sample_clamped(t); + let stack = &mut curve_evaluator.evaluator.stack; + let last_node = stack.last().map(|el| el.graph_node); + match last_node { + Some(index) if index == graph_node => { + (*stack.last_mut().unwrap()).value.translation = Some(translation); + } + _ => { + let value = TransformParts::from_translation(translation); + stack.push(BasicAnimationCurveEvaluatorStackElement { + value, + weight, + graph_node, + }); + } + } Ok(()) } } @@ -424,15 +431,22 @@ where let curve_evaluator = (*Reflect::as_any_mut(curve_evaluator)) .downcast_mut::() .unwrap(); - let value = TransformParts::from_rotation(self.0.sample_clamped(t)); - curve_evaluator - .evaluator - .stack - .push(BasicAnimationCurveEvaluatorStackElement { - value, - weight, - graph_node, - }); + let rotation = self.0.sample_clamped(t); + let stack = &mut curve_evaluator.evaluator.stack; + let last_node = stack.last().map(|el| el.graph_node); + match last_node { + Some(index) if index == graph_node => { + (*stack.last_mut().unwrap()).value.rotation = Some(rotation); + } + _ => { + let value = TransformParts::from_rotation(rotation); + stack.push(BasicAnimationCurveEvaluatorStackElement { + value, + weight, + graph_node, + }); + } + } Ok(()) } } @@ -475,15 +489,22 @@ where let curve_evaluator = (*Reflect::as_any_mut(curve_evaluator)) .downcast_mut::() .unwrap(); - let value = TransformParts::from_scale(self.0.sample_clamped(t)); - curve_evaluator - .evaluator - .stack - .push(BasicAnimationCurveEvaluatorStackElement { - value, - weight, - graph_node, - }); + let scale = self.0.sample_clamped(t); + let stack = &mut curve_evaluator.evaluator.stack; + let last_node = stack.last().map(|el| el.graph_node); + match last_node { + Some(index) if index == graph_node => { + (*stack.last_mut().unwrap()).value.scale = Some(scale); + } + _ => { + let value = TransformParts::from_scale(scale); + stack.push(BasicAnimationCurveEvaluatorStackElement { + value, + weight, + graph_node, + }); + } + } Ok(()) } } @@ -832,12 +853,16 @@ where match self.blend_register.take() { None => self.blend_register = Some((value_to_blend, weight_to_blend)), Some((mut current_value, mut current_weight)) => { - + current_weight += weight_to_blend; if additive { + // println!( + // "Performed additive blending with weight {:?}", + // weight_to_blend + // ); current_value = A::blend( [ BlendInput { - weight: current_weight, + weight: 1.0, value: current_value, additive: true, }, @@ -846,10 +871,9 @@ where value: value_to_blend, additive: true, }, - ] - .into_iter(), - ); - current_weight += weight_to_blend; + ] + .into_iter(), + ); } else { current_value = A::interpolate( ¤t_value, @@ -857,7 +881,6 @@ where weight_to_blend / current_weight, ); } - self.blend_register = Some((current_value, current_weight)); } } diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 5e6b35b0a7ccb..d2a8159b2ea1e 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -1190,7 +1190,7 @@ pub fn animate_targets( continue; }; - let weight = active_animation.weight; + let weight = active_animation.weight * animation_graph_node.weight; let seek_time = active_animation.seek_time; for curve in curves { @@ -1341,7 +1341,7 @@ impl AnimationEvaluationState { /// Calls [`AnimationCurveEvaluator::add`] 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 + /// The given `node_index` is the node that we're evaluating. fn add_all(&mut self, node_index: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { for curve_evaluator_type in self.current_curve_evaluator_types.keys() { self.curve_evaluators diff --git a/examples/animation/transform_curve.rs b/examples/animation/transform_curve.rs index 0d6addff13ad3..b52e83f024632 100644 --- a/examples/animation/transform_curve.rs +++ b/examples/animation/transform_curve.rs @@ -76,9 +76,13 @@ impl AnimationInfo { // Build the animation graph: let mut animation_graph = AnimationGraph::new(); - let blend_node = animation_graph.add_additive_blend(1.0, animation_graph.root); - node_indices.push(animation_graph.add_clip(main_clip_handle, 1.0, blend_node)); - node_indices.push(animation_graph.add_clip(additive_clip_handle, 0.01, blend_node)); + let additive_blend_node = animation_graph.add_additive_blend(1.0, animation_graph.root); + node_indices.push(animation_graph.add_clip(main_clip_handle, 1.0, additive_blend_node)); + node_indices.push(animation_graph.add_clip( + additive_clip_handle, + 0.05, + additive_blend_node, + )); let animation_graph_handle = animation_graphs.add(animation_graph); From 8296b09949059ecbbf5d218283fe97c5a9dbd089 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sun, 6 Oct 2024 20:56:08 -0400 Subject: [PATCH 12/17] Remove debug artifacts --- crates/bevy_animation/src/animation_curves.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index 66b3be48a28e4..c4b33759fc9f8 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -855,10 +855,6 @@ where Some((mut current_value, mut current_weight)) => { current_weight += weight_to_blend; if additive { - // println!( - // "Performed additive blending with weight {:?}", - // weight_to_blend - // ); current_value = A::blend( [ BlendInput { From e4e13e28e800dc447807efe1ce0fc0c414279cfb Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 7 Oct 2024 09:41:32 -0400 Subject: [PATCH 13/17] Add code comments explaining unification of stack elements --- crates/bevy_animation/src/animation_curves.rs | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index c4b33759fc9f8..fc06bdbb179df 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -377,13 +377,27 @@ where let stack = &mut curve_evaluator.evaluator.stack; let last_node = stack.last().map(|el| el.graph_node); match last_node { + // A couple things to note here: + // 1. `apply` is called for all curves on a single node in sequence; i.e. + // the `graph_node` values of stack elements are not interleaved. + // 2. Similarly, the weight depends only on the graph node. + // With these in mind, what's happening here is that we are joining all + // of the `Transform`-targeting curves from a single clip by peeking at + // the top of the evaluator stack and seeing if the last curve added was + // from this node. + // - If it was: append to that stack element instead of creating a new one. + // - If it wasn't: this is the first one, so we push a new stack element with + // the expectation that other curves on this node may immediately append + // to it. + // This has the effect of unifying the output values of all `Transform`- + // targeted curves into a single `TransformParts` on the blend stack. Some(index) if index == graph_node => { - (*stack.last_mut().unwrap()).value.translation = Some(translation); + // stack.last() succeeded => this unwrap always succeeds. + stack.last_mut().unwrap().value.translation = Some(translation); } _ => { - let value = TransformParts::from_translation(translation); stack.push(BasicAnimationCurveEvaluatorStackElement { - value, + value: TransformParts::from_translation(translation), weight, graph_node, }); @@ -435,8 +449,9 @@ where let stack = &mut curve_evaluator.evaluator.stack; let last_node = stack.last().map(|el| el.graph_node); match last_node { + // See `TranslationCurve::apply` implementation for details. Some(index) if index == graph_node => { - (*stack.last_mut().unwrap()).value.rotation = Some(rotation); + stack.last_mut().unwrap().value.rotation = Some(rotation); } _ => { let value = TransformParts::from_rotation(rotation); @@ -493,8 +508,9 @@ where let stack = &mut curve_evaluator.evaluator.stack; let last_node = stack.last().map(|el| el.graph_node); match last_node { + // See `TranslationCurve::apply` implementation for details. Some(index) if index == graph_node => { - (*stack.last_mut().unwrap()).value.scale = Some(scale); + stack.last_mut().unwrap().value.scale = Some(scale); } _ => { let value = TransformParts::from_scale(scale); From cdf78590c81d7735082e4a6d23bdf93729502e8a Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 7 Oct 2024 10:44:01 -0400 Subject: [PATCH 14/17] Improve turbulence in example --- crates/bevy_animation/src/animation_curves.rs | 4 ++- crates/bevy_math/src/curve/adaptors.rs | 9 ++++-- examples/animation/transform_curve.rs | 29 ++++++++++++------- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index fc06bdbb179df..ec7fd888b9445 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -379,8 +379,10 @@ where match last_node { // A couple things to note here: // 1. `apply` is called for all curves on a single node in sequence; i.e. - // the `graph_node` values of stack elements are not interleaved. + // the `graph_node` values of stack elements from one node are not + // interleaved. // 2. Similarly, the weight depends only on the graph node. + // // With these in mind, what's happening here is that we are joining all // of the `Transform`-targeting curves from a single clip by peeking at // the top of the evaluator stack and seeing if the last curve added was diff --git a/crates/bevy_math/src/curve/adaptors.rs b/crates/bevy_math/src/curve/adaptors.rs index 227a8ec3e0452..9a54d7faf2041 100644 --- a/crates/bevy_math/src/curve/adaptors.rs +++ b/crates/bevy_math/src/curve/adaptors.rs @@ -9,7 +9,7 @@ use core::fmt::{self, Debug}; use core::marker::PhantomData; #[cfg(feature = "bevy_reflect")] -use bevy_reflect::{utility::GenericTypePathCell, Reflect, TypePath}; +use bevy_reflect::{utility::GenericTypePathCell, FromReflect, Reflect, TypePath}; #[cfg(feature = "bevy_reflect")] mod paths { @@ -430,7 +430,12 @@ where /// produced by [`Curve::graph`]. #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + derive(FromReflect), + reflect(from_reflect = false) +)] pub struct GraphCurve { pub(crate) base: C, #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] diff --git a/examples/animation/transform_curve.rs b/examples/animation/transform_curve.rs index b52e83f024632..1f5bebba8d0c8 100644 --- a/examples/animation/transform_curve.rs +++ b/examples/animation/transform_curve.rs @@ -30,7 +30,7 @@ struct AnimationInfo { } impl AnimationInfo { - // Programmatically creates the UI animation. + // Programmatically creates the ship animation. fn create( animation_graphs: &mut Assets, animation_clips: &mut Assets, @@ -42,11 +42,14 @@ impl AnimationInfo { // Allocate an animation clip. let mut main_clip = AnimationClip::default(); + // This curve describes the position of the ship over time. let wobbly_circle_curve = function_curve(Interval::new(0.0, std::f32::consts::TAU).unwrap(), |t| { vec3(t.sin() * 5.0, t.sin() * 1.5, t.cos() * 5.0) }); + // This curve uses the position of the ship to make its inward wing point toward + // the center of the platform that we'll position. let transform_curve = wobbly_circle_curve.map(|position| { Transform::from_translation(position).aligned_by( Dir3::NEG_X, @@ -58,14 +61,21 @@ impl AnimationInfo { main_clip.add_curve_to_target(animation_target_id, TransformCurve(transform_curve)); - // Set up an additional additive clip to blend with the first. + // Set up an additional radial wobble to additively blend onto the ship's trajectory, + // mimicking some kind of turbulence. let mut additive_clip = AnimationClip::default(); - let turbulence_curve = + // This curve describes the change in the ship's radius relative to its center. + let radial_displacement_curve = function_curve(Interval::new(0.0, std::f32::consts::TAU).unwrap(), |t| { - vec3(f32::cos(20.0 * t), 0.0, f32::sin(20.0 * t)) + f32::cos(15.0 * t) }); + // This curve assigns the radius from the previous curve as an actual radius + let turbulence_curve = radial_displacement_curve + .graph() + .map(|(t, radius)| vec3(t.sin() * radius, 0.0, t.cos() * radius)); + additive_clip.add_curve_to_target(animation_target_id, TranslationCurve(turbulence_curve)); // Save our animation clips as assets. @@ -74,15 +84,14 @@ impl AnimationInfo { let mut node_indices = vec![]; - // Build the animation graph: + // Start building the animation graph. let mut animation_graph = AnimationGraph::new(); + + // The first child of the additive blend node describes the base animation, and the second + // is blended additively on top of it with its given weight. let additive_blend_node = animation_graph.add_additive_blend(1.0, animation_graph.root); node_indices.push(animation_graph.add_clip(main_clip_handle, 1.0, additive_blend_node)); - node_indices.push(animation_graph.add_clip( - additive_clip_handle, - 0.05, - additive_blend_node, - )); + node_indices.push(animation_graph.add_clip(additive_clip_handle, 0.1, additive_blend_node)); let animation_graph_handle = animation_graphs.add(animation_graph); From 5b22ab6bdc4137a3c7100ef23fa26b5a7acb1e0d Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 7 Oct 2024 10:51:05 -0400 Subject: [PATCH 15/17] Rename example to be more descriptive --- Cargo.toml | 8 ++++---- examples/README.md | 2 +- .../{transform_curve.rs => transform_curve_animation.rs} | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) rename examples/animation/{transform_curve.rs => transform_curve_animation.rs} (98%) diff --git a/Cargo.toml b/Cargo.toml index c68eda0a085aa..dc563fba258d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1288,12 +1288,12 @@ category = "Animation" wasm = true [[example]] -name = "transform_curve" -path = "examples/animation/transform_curve.rs" +name = "transform_curve_animation" +path = "examples/animation/transform_curve_animation.rs" doc-scrape-examples = true -[package.metadata.example.transform_curve] -name = "Transform Curve" +[package.metadata.example.transform_curve_animation] +name = "Transform Curve Animation" description = "Direct animation of an entity's Transform using curves" category = "Animation" wasm = true diff --git a/examples/README.md b/examples/README.md index ff52cdcc45f08..099fae2edc629 100644 --- a/examples/README.md +++ b/examples/README.md @@ -200,7 +200,7 @@ Example | Description [Cubic Curve](../examples/animation/cubic_curve.rs) | Bezier curve example showing a cube following a cubic curve [Custom Skinned Mesh](../examples/animation/custom_skinned_mesh.rs) | Skinned mesh example with mesh and joints data defined in code [Morph Targets](../examples/animation/morph_targets.rs) | Plays an animation from a glTF file with meshes with morph targets -[Transform Curve](../examples/animation/transform_curve.rs) | Direct animation of an entity's Transform using curves +[Transform Curve Animation](../examples/animation/transform_curve_animation.rs) | Direct animation of an entity's Transform using curves [glTF Skinned Mesh](../examples/animation/gltf_skinned_mesh.rs) | Skinned mesh example with mesh and joints data loaded from a glTF file ## Application diff --git a/examples/animation/transform_curve.rs b/examples/animation/transform_curve_animation.rs similarity index 98% rename from examples/animation/transform_curve.rs rename to examples/animation/transform_curve_animation.rs index 1f5bebba8d0c8..6a84e73c4722f 100644 --- a/examples/animation/transform_curve.rs +++ b/examples/animation/transform_curve_animation.rs @@ -1,4 +1,4 @@ -//! Create and play an animation defined by a curve valued in `Transform`. +//! Create and play an animation defined by a `Transform`-valued curve. use bevy::{ animation::{AnimationTarget, AnimationTargetId}, From cde046bbdd078276176ee639f01be861047055d3 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 7 Oct 2024 11:23:00 -0400 Subject: [PATCH 16/17] Ensure the transform blend stack only gets one item per node no matter what --- crates/bevy_animation/src/animation_curves.rs | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index ec7fd888b9445..cff1d8e2dd58a 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -565,15 +565,22 @@ where let curve_evaluator = (*Reflect::as_any_mut(curve_evaluator)) .downcast_mut::() .unwrap(); - let value = TransformParts::from_transform(self.0.sample_clamped(t)); - curve_evaluator - .evaluator - .stack - .push(BasicAnimationCurveEvaluatorStackElement { - value, - weight, - graph_node, - }); + let parts = TransformParts::from_transform(self.0.sample_clamped(t)); + let stack = &mut curve_evaluator.evaluator.stack; + let last_node = stack.last().map(|el| el.graph_node); + match last_node { + // See `TranslationCurve::apply` implementation for details. + Some(index) if index == graph_node => { + stack.last_mut().unwrap().value = parts; + } + _ => { + stack.push(BasicAnimationCurveEvaluatorStackElement { + value: parts, + weight, + graph_node, + }); + } + } Ok(()) } } From ef38afff517122a1ae6811e60a6045452fbd0d6f Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 7 Oct 2024 11:49:34 -0400 Subject: [PATCH 17/17] Silence clippy --- examples/animation/transform_curve_animation.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/animation/transform_curve_animation.rs b/examples/animation/transform_curve_animation.rs index 6a84e73c4722f..898221132d71b 100644 --- a/examples/animation/transform_curve_animation.rs +++ b/examples/animation/transform_curve_animation.rs @@ -1,5 +1,8 @@ //! Create and play an animation defined by a `Transform`-valued curve. +// We use `std` versions of trigonometry functions which are disallowed within Bevy itself. +#![allow(clippy::disallowed_methods)] + use bevy::{ animation::{AnimationTarget, AnimationTargetId}, math::vec3,