diff --git a/Cargo.toml b/Cargo.toml index 320ab099775d2..dc563fba258d6 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_animation" +path = "examples/animation/transform_curve_animation.rs" +doc-scrape-examples = true + +[package.metadata.example.transform_curve_animation] +name = "Transform Curve Animation" +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 6af653d3077b4..256fe8494675d 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 { @@ -198,6 +199,124 @@ 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 { + pub(crate) translation: Option, + pub(crate) rotation: Option, + pub(crate) 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. /// @@ -271,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 2ca23727b57f9..cff1d8e2dd58a 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 //! @@ -96,6 +97,7 @@ use bevy_render::mesh::morph::MorphWeights; use bevy_transform::prelude::Transform; use crate::{ + animatable::TransformParts, graph::AnimationNodeIndex, prelude::{Animatable, BlendInput}, AnimationEntityMut, AnimationEvaluationError, @@ -341,15 +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)] -pub struct TranslationCurveEvaluator { - evaluator: BasicAnimationCurveEvaluator, -} - impl AnimationCurve for TranslationCurve where C: AnimationCompatibleCurve, @@ -363,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( @@ -380,52 +371,40 @@ 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); - 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.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; + 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 { + // 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 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 + // 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() succeeded => this unwrap always succeeds. + stack.last_mut().unwrap().value.translation = Some(translation); + } + _ => { + stack.push(BasicAnimationCurveEvaluatorStackElement { + value: TransformParts::from_translation(translation), + weight, + graph_node, + }); + } + } Ok(()) } } @@ -438,15 +417,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)] -pub struct RotationCurveEvaluator { - evaluator: BasicAnimationCurveEvaluator, -} - impl AnimationCurve for RotationCurve where C: AnimationCompatibleCurve, @@ -460,13 +430,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( @@ -477,52 +445,25 @@ 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); - 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.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; + 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 { + // See `TranslationCurve::apply` implementation for details. + 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(()) } } @@ -535,15 +476,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)] -pub struct ScaleCurveEvaluator { - evaluator: BasicAnimationCurveEvaluator, -} - impl AnimationCurve for ScaleCurve where C: AnimationCompatibleCurve, @@ -557,13 +489,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( @@ -574,52 +504,83 @@ 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); - 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 { + // See `TranslationCurve::apply` implementation for details. + 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(()) } } -impl AnimationCurveEvaluator for ScaleCurveEvaluator { - fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { - self.evaluator.combine(graph_node, /*additive=*/ false) +/// 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 add(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { - self.evaluator.combine(graph_node, /*additive=*/ true) + fn domain(&self) -> Interval { + self.0.domain() } - fn push_blend_register( - &mut self, - weight: f32, - graph_node: AnimationNodeIndex, - ) -> Result<(), AnimationEvaluationError> { - self.evaluator.push_blend_register(weight, graph_node) + fn evaluator_type(&self) -> TypeId { + TypeId::of::() } - fn commit<'a>( - &mut self, - transform: Option>, - _: AnimationEntityMut<'a>, + 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 mut component = transform.ok_or_else(|| { - AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) - })?; - component.scale = self - .evaluator - .stack - .pop() - .ok_or_else(inconsistent::)? - .value; + let curve_evaluator = (*Reflect::as_any_mut(curve_evaluator)) + .downcast_mut::() + .unwrap(); + 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(()) } } @@ -820,6 +781,47 @@ impl AnimationCurveEvaluator for WeightsCurveEvaluator { } } +#[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 @@ -877,7 +879,6 @@ where 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( [ @@ -901,7 +902,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 b5cc25cfed0da..d2a8159b2ea1e 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 @@ -1169,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 { @@ -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/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/README.md b/examples/README.md index 7754978c9307d..099fae2edc629 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 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/animated_transform.rs b/examples/animation/animated_transform.rs index f5eecd3d8ddb2..d103fa0a9faad 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.), diff --git a/examples/animation/transform_curve_animation.rs b/examples/animation/transform_curve_animation.rs new file mode 100644 index 0000000000000..898221132d71b --- /dev/null +++ b/examples/animation/transform_curve_animation.rs @@ -0,0 +1,172 @@ +//! 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, + 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 ship 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(); + + // 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, + 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 radial wobble to additively blend onto the ship's trajectory, + // mimicking some kind of turbulence. + let mut additive_clip = AnimationClip::default(); + + // 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| { + 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. + let main_clip_handle = animation_clips.add(main_clip); + let additive_clip_handle = animation_clips.add(additive_clip); + + let mut node_indices = vec![]; + + // 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.1, additive_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, + }); +}