diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index 4bea4384f7cac..b08310d459fa5 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -84,7 +84,7 @@ //! [`animated_field`]: crate::animated_field use core::{ - any::TypeId, + any::{Any, TypeId}, fmt::{self, Debug, Formatter}, marker::PhantomData, }; @@ -389,6 +389,11 @@ where }); Ok(()) } + + fn sample_clamped(&self, t: f32) -> Box { + let value = self.curve.sample_clamped(t); + Box::new(value) + } } impl AnimationCurveEvaluator for AnimatableCurveEvaluator { @@ -600,11 +605,14 @@ pub trait AnimationCurve: Debug + Send + Sync + 'static { weight: f32, graph_node: AnimationNodeIndex, ) -> Result<(), AnimationEvaluationError>; + + /// Samples the curve at the given time `t` and returns a Boxed value. + fn sample_clamped(&self, t: f32) -> Box; } /// The [`EvaluatorId`] is used to look up the [`AnimationCurveEvaluator`] for an [`AnimatableProperty`]. /// For a given animated property, this ID should always be the same to allow things like animation blending to occur. -#[derive(Clone)] +#[derive(Clone, PartialEq, Eq)] pub enum EvaluatorId<'a> { /// Corresponds to a specific field on a specific component type. /// The `TypeId` should correspond to the component type, and the `usize` @@ -794,6 +802,9 @@ macro_rules! animated_field { #[cfg(test)] mod tests { use super::*; + use crate::VariableCurve; + use bevy_math::Vec3; + use bevy_transform::components::Transform; #[test] fn test_animated_field_tuple_struct_simple_uses() { @@ -807,4 +818,28 @@ mod tests { let _ = AnimatedField::new_unchecked("1", |b: &mut B| &mut b.1); let _ = AnimatedField::new_unchecked("2", |b: &mut B| &mut b.2); } + + #[test] + fn test_sample_animation_curve() { + let variable_curve = VariableCurve::new(AnimatableCurve::new( + animated_field!(Transform::translation), + AnimatableKeyframeCurve::new([ + (0.0, Vec3::new(0., 0., 1.)), + (1.0, Vec3::new(1., 0., 0.)), + ]) + .expect("Failed to create power level curve"), + )); + let value = variable_curve + .0 + .sample_clamped(0.) + .downcast::() + .unwrap(); + assert_eq!(*value, Vec3::new(0., 0., 1.)); + let value = variable_curve + .0 + .sample_clamped(1.) + .downcast::() + .unwrap(); + assert_eq!(*value, Vec3::new(1., 0., 0.)); + } } diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 99820fba96c16..0a007560340ae 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -34,7 +34,7 @@ use prelude::AnimationCurveEvaluator; use crate::{ graph::{AnimationGraphHandle, ThreadedAnimationGraphs}, - prelude::EvaluatorId, + prelude::{AnimatableProperty, EvaluatorId}, }; use bevy_app::{AnimationSystems, App, Plugin, PostUpdate}; @@ -353,6 +353,49 @@ impl AnimationClip { ); } + /// Samples an [`AnimatableProperty`] of a specific [`AnimationTargetId`]. + /// + /// See [`crate::morph::WeightsCurveSample`] if you want to sample [`crate::morph::WeightsCurve`]. + /// + /// # Examples + /// ``` + /// # use bevy_animation::prelude::*; + /// # use bevy_animation::{animated_field, AnimationTargetId}; + /// # + /// # use bevy_ecs::prelude::Name; + /// # use bevy_math::Vec3; + /// # use bevy_transform::components::Transform; + /// let mut clip = AnimationClip::default(); + /// let animatable_curve = AnimatableCurve::new( + /// animated_field!(Transform::translation), + /// AnimatableKeyframeCurve::new([ + /// (0.0, Vec3::new(0., 0., 1.)), + /// (1.0, Vec3::new(1., 0., 0.)), + /// ]) + /// .expect("Failed to create power level curve"), + /// ); + /// let target_1 = AnimationTargetId::from_name(&Name::new("Target 1")); + /// clip.add_curve_to_target(target_1, animatable_curve); + /// let value = clip.sample_clamped(animated_field!(Transform::translation), target_1, 1.0); + /// assert_eq!(value, Some(Vec3::new(1., 0., 0.))); + /// ``` + pub fn sample_clamped( + &self, + animatable_property: P, + target: AnimationTargetId, + time: f32, + ) -> Option { + let curves = self.curves_for_target(target)?; + for curve in curves { + if curve.0.evaluator_id() == animatable_property.evaluator_id() + && let Ok(sample) = curve.0.sample_clamped(time).downcast::() + { + return Some(*sample); + } + } + None + } + /// Add an event function with no [`AnimationTargetId`] to this [`AnimationClip`]. /// /// The `func` will trigger on the [`AnimationPlayer`] entity once the `time` (in seconds) @@ -1516,8 +1559,13 @@ impl<'a> Iterator for TriggeredEventsIter<'a> { #[cfg(test)] mod tests { - use crate as bevy_animation; + use crate::{ + self as bevy_animation, + prelude::{AnimatableCurve, AnimatableKeyframeCurve, AnimatedField}, + }; + use bevy_math::Vec3; use bevy_reflect::map::{DynamicMap, Map}; + use bevy_transform::components::Transform; use super::*; @@ -1702,4 +1750,26 @@ mod tests { ); } } + + #[test] + fn test_sample_at_time() { + let mut clip = AnimationClip::default(); + let animatable_curve = AnimatableCurve::new( + animated_field!(Transform::translation), + AnimatableKeyframeCurve::new([ + (0.0, Vec3::new(0., 0., 1.)), + (1.0, Vec3::new(1., 0., 0.)), + ]) + .expect("Failed to create power level curve"), + ); + let target_1 = AnimationTargetId::from_name(&Name::new("Target 1")); + let target_2 = AnimationTargetId::from_name(&Name::new("Target 2")); + clip.add_curve_to_target(target_1, animatable_curve); + let value = clip.sample_clamped(animated_field!(Transform::translation), target_1, 1.0); + assert_eq!(value, Some(Vec3::new(1., 0., 0.))); + let value = clip.sample_clamped(animated_field!(Transform::scale), target_1, 1.0); + assert_eq!(value, None); + let value = clip.sample_clamped(animated_field!(Transform::translation), target_2, 1.0); + assert_eq!(value, None); + } } diff --git a/crates/bevy_animation/src/morph.rs b/crates/bevy_animation/src/morph.rs index 75836d9c20a5d..1dc73f0976c82 100644 --- a/crates/bevy_animation/src/morph.rs +++ b/crates/bevy_animation/src/morph.rs @@ -7,7 +7,10 @@ use crate::{ use bevy_math::curve::{iterable::IterableCurve, Interval}; use bevy_mesh::morph::MorphWeights; use bevy_reflect::{FromReflect, Reflect, Reflectable}; -use core::{any::TypeId, fmt::Debug}; +use core::{ + any::{Any, TypeId}, + fmt::Debug, +}; /// This type allows an [`IterableCurve`] valued in `f32` to be used as an [`AnimationCurve`] /// that animates [morph weights]. @@ -53,6 +56,14 @@ struct WeightsCurveEvaluator { morph_target_count: Option, } +/// Type indicating that the sampled value from an animation curve is coming from a +/// [`WeightsCurve`]. +/// +/// You shouldn't need to interact with this type unless you're manually evaluating animation +/// curves. +#[derive(Reflect)] +pub struct WeightsCurveSample(pub Vec); + impl AnimationCurve for WeightsCurve where C: IterableCurve + Debug + Clone + Reflectable, @@ -104,6 +115,12 @@ where .push((weight, graph_node)); Ok(()) } + + fn sample_clamped(&self, t: f32) -> Box { + Box::new(WeightsCurveSample( + self.0.sample_iter_clamped(t).collect::>(), + )) + } } impl WeightsCurveEvaluator {