Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions crates/bevy_animation/src/animation_curves.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
//! [`animated_field`]: crate::animated_field

use core::{
any::TypeId,
any::{Any, TypeId},
fmt::{self, Debug, Formatter},
marker::PhantomData,
};
Expand Down Expand Up @@ -389,6 +389,11 @@ where
});
Ok(())
}

fn sample_clamped(&self, t: f32) -> Box<dyn Any> {
let value = self.curve.sample_clamped(t);
Box::new(value)
}
}

impl<A: Animatable> AnimationCurveEvaluator for AnimatableCurveEvaluator<A> {
Expand Down Expand Up @@ -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<dyn Any>;
}

/// 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`
Expand Down Expand Up @@ -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() {
Expand All @@ -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::<Vec3>()
.unwrap();
assert_eq!(*value, Vec3::new(0., 0., 1.));
let value = variable_curve
.0
.sample_clamped(1.)
.downcast::<Vec3>()
.unwrap();
assert_eq!(*value, Vec3::new(1., 0., 0.));
}
}
74 changes: 72 additions & 2 deletions crates/bevy_animation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ use prelude::AnimationCurveEvaluator;

use crate::{
graph::{AnimationGraphHandle, ThreadedAnimationGraphs},
prelude::EvaluatorId,
prelude::{AnimatableProperty, EvaluatorId},
};

use bevy_app::{AnimationSystems, App, Plugin, PostUpdate};
Expand Down Expand Up @@ -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<P: AnimatableProperty>(
&self,
animatable_property: P,
target: AnimationTargetId,
time: f32,
) -> Option<P::Property> {
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::<P::Property>()
{
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)
Expand Down Expand Up @@ -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::*;

Expand Down Expand Up @@ -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);
}
}
19 changes: 18 additions & 1 deletion crates/bevy_animation/src/morph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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].
Expand Down Expand Up @@ -53,6 +56,14 @@ struct WeightsCurveEvaluator {
morph_target_count: Option<u32>,
}

/// 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<f32>);

impl<C> AnimationCurve for WeightsCurve<C>
where
C: IterableCurve<f32> + Debug + Clone + Reflectable,
Expand Down Expand Up @@ -104,6 +115,12 @@ where
.push((weight, graph_node));
Ok(())
}

fn sample_clamped(&self, t: f32) -> Box<dyn Any> {
Box::new(WeightsCurveSample(
self.0.sample_iter_clamped(t).collect::<Vec<f32>>(),
))
}
}

impl WeightsCurveEvaluator {
Expand Down
Loading