mirror of
https://github.com/bevyengine/bevy
synced 2024-11-25 22:20:20 +00:00
Allow animation clips to animate arbitrary properties. (#15282)
Currently, Bevy restricts animation clips to animating `Transform::translation`, `Transform::rotation`, `Transform::scale`, or `MorphWeights`, which correspond to the properties that glTF can animate. This is insufficient for many use cases such as animating UI, as the UI layout systems expect to have exclusive control over UI elements' `Transform`s and therefore the `Style` properties must be animated instead. This commit fixes this, allowing for `AnimationClip`s to animate arbitrary properties. The `Keyframes` structure has been turned into a low-level trait that can be implemented to achieve arbitrary animation behavior. Along with `Keyframes`, this patch adds a higher-level trait, `AnimatableProperty`, that simplifies the task of animating single interpolable properties. Built-in `Keyframes` implementations exist for translation, rotation, scale, and morph weights. For the most part, you can migrate by simply changing your code from `Keyframes::Translation(...)` to `TranslationKeyframes(...)`, and likewise for rotation, scale, and morph weights. An example `AnimatableProperty` implementation for the font size of a text section follows: #[derive(Reflect)] struct FontSizeProperty; impl AnimatableProperty for FontSizeProperty { type Component = Text; type Property = f32; fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property> { Some(&mut component.sections.get_mut(0)?.style.font_size) } } In order to keep this patch relatively small, this patch doesn't include an implementation of `AnimatableProperty` on top of the reflection system. That can be a follow-up. This patch builds on top of the new `EntityMutExcept<>` type in order to widen the `AnimationTarget` query to include write access to all components. Because `EntityMutExcept<>` has some performance overhead over an explicit query, we continue to explicitly query `Transform` in order to avoid regressing the performance of skeletal animation, such as the `many_foxes` benchmark. I've measured the performance of that benchmark and have found no significant regressions. A new example, `animated_ui`, has been added. This example shows how to use Bevy's built-in animation infrastructure to animate font size and color, which wasn't possible before this patch. ## Showcase https://github.com/user-attachments/assets/1fa73492-a9ce-405a-a8f2-4aacd7f6dc97 ## Migration Guide * Animation keyframes are now an extensible trait, not an enum. Replace `Keyframes::Translation(...)`, `Keyframes::Scale(...)`, `Keyframes::Rotation(...)`, and `Keyframes::Weights(...)` with `Box::new(TranslationKeyframes(...))`, `Box::new(ScaleKeyframes(...))`, `Box::new(RotationKeyframes(...))`, and `Box::new(MorphWeightsKeyframes(...))` respectively.
This commit is contained in:
parent
6e95f297ea
commit
8154164f1b
14 changed files with 1440 additions and 449 deletions
11
Cargo.toml
11
Cargo.toml
|
@ -3500,6 +3500,17 @@ description = "Demonstrates percentage-closer soft shadows (PCSS)"
|
||||||
category = "3D Rendering"
|
category = "3D Rendering"
|
||||||
wasm = false
|
wasm = false
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "animated_ui"
|
||||||
|
path = "examples/animation/animated_ui.rs"
|
||||||
|
doc-scrape-examples = true
|
||||||
|
|
||||||
|
[package.metadata.example.animated_ui]
|
||||||
|
name = "Animated UI"
|
||||||
|
description = "Shows how to use animation clips to animate UI properties"
|
||||||
|
category = "Animation"
|
||||||
|
wasm = true
|
||||||
|
|
||||||
[profile.wasm-release]
|
[profile.wasm-release]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
opt-level = "z"
|
opt-level = "z"
|
||||||
|
|
|
@ -27,6 +27,10 @@ bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
|
||||||
bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" }
|
bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" }
|
||||||
bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" }
|
bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" }
|
||||||
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" }
|
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" }
|
||||||
|
bevy_ui = { path = "../bevy_ui", version = "0.15.0-dev", features = [
|
||||||
|
"bevy_text",
|
||||||
|
] }
|
||||||
|
bevy_text = { path = "../bevy_text", version = "0.15.0-dev" }
|
||||||
|
|
||||||
# other
|
# other
|
||||||
fixedbitset = "0.5"
|
fixedbitset = "0.5"
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
//! Traits and type for interpolating between values.
|
//! Traits and type for interpolating between values.
|
||||||
|
|
||||||
use crate::util;
|
use crate::{util, AnimationEvaluationError, Interpolation};
|
||||||
use bevy_color::{Laba, LinearRgba, Oklaba, Srgba, Xyza};
|
use bevy_color::{Laba, LinearRgba, Oklaba, Srgba, Xyza};
|
||||||
use bevy_ecs::world::World;
|
|
||||||
use bevy_math::*;
|
use bevy_math::*;
|
||||||
use bevy_reflect::Reflect;
|
use bevy_reflect::Reflect;
|
||||||
use bevy_transform::prelude::Transform;
|
use bevy_transform::prelude::Transform;
|
||||||
|
@ -28,10 +27,6 @@ pub trait Animatable: Reflect + Sized + Send + Sync + 'static {
|
||||||
///
|
///
|
||||||
/// Implementors should return a default value when no inputs are provided here.
|
/// Implementors should return a default value when no inputs are provided here.
|
||||||
fn blend(inputs: impl Iterator<Item = BlendInput<Self>>) -> Self;
|
fn blend(inputs: impl Iterator<Item = BlendInput<Self>>) -> Self;
|
||||||
|
|
||||||
/// Post-processes the value using resources in the [`World`].
|
|
||||||
/// Most animatable types do not need to implement this.
|
|
||||||
fn post_process(&mut self, _world: &World) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! impl_float_animatable {
|
macro_rules! impl_float_animatable {
|
||||||
|
@ -192,3 +187,159 @@ impl Animatable for Quat {
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An abstraction over a list of keyframes.
|
||||||
|
///
|
||||||
|
/// Using this abstraction instead of `Vec<T>` enables more flexibility in how
|
||||||
|
/// keyframes are stored. In particular, morph weights use this trait in order
|
||||||
|
/// to flatten the keyframes for all morph weights into a single vector instead
|
||||||
|
/// of nesting vectors.
|
||||||
|
pub(crate) trait GetKeyframe {
|
||||||
|
/// The type of the property to be animated.
|
||||||
|
type Output;
|
||||||
|
/// Retrieves the value of the keyframe at the given index.
|
||||||
|
fn get_keyframe(&self, index: usize) -> Option<&Self::Output>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Interpolates between keyframes and stores the result in `dest`.
|
||||||
|
///
|
||||||
|
/// This is factored out so that it can be shared between implementations of
|
||||||
|
/// [`crate::keyframes::Keyframes`].
|
||||||
|
pub(crate) fn interpolate_keyframes<T>(
|
||||||
|
dest: &mut T,
|
||||||
|
keyframes: &(impl GetKeyframe<Output = T> + ?Sized),
|
||||||
|
interpolation: Interpolation,
|
||||||
|
step_start: usize,
|
||||||
|
time: f32,
|
||||||
|
weight: f32,
|
||||||
|
duration: f32,
|
||||||
|
) -> Result<(), AnimationEvaluationError>
|
||||||
|
where
|
||||||
|
T: Animatable + Clone,
|
||||||
|
{
|
||||||
|
let value = match interpolation {
|
||||||
|
Interpolation::Step => {
|
||||||
|
let Some(start_keyframe) = keyframes.get_keyframe(step_start) else {
|
||||||
|
return Err(AnimationEvaluationError::KeyframeNotPresent(step_start));
|
||||||
|
};
|
||||||
|
(*start_keyframe).clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
Interpolation::Linear => {
|
||||||
|
let (Some(start_keyframe), Some(end_keyframe)) = (
|
||||||
|
keyframes.get_keyframe(step_start),
|
||||||
|
keyframes.get_keyframe(step_start + 1),
|
||||||
|
) else {
|
||||||
|
return Err(AnimationEvaluationError::KeyframeNotPresent(step_start + 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
T::interpolate(start_keyframe, end_keyframe, time)
|
||||||
|
}
|
||||||
|
|
||||||
|
Interpolation::CubicSpline => {
|
||||||
|
let (
|
||||||
|
Some(start_keyframe),
|
||||||
|
Some(start_tangent_keyframe),
|
||||||
|
Some(end_tangent_keyframe),
|
||||||
|
Some(end_keyframe),
|
||||||
|
) = (
|
||||||
|
keyframes.get_keyframe(step_start * 3 + 1),
|
||||||
|
keyframes.get_keyframe(step_start * 3 + 2),
|
||||||
|
keyframes.get_keyframe(step_start * 3 + 3),
|
||||||
|
keyframes.get_keyframe(step_start * 3 + 4),
|
||||||
|
)
|
||||||
|
else {
|
||||||
|
return Err(AnimationEvaluationError::KeyframeNotPresent(
|
||||||
|
step_start * 3 + 4,
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
interpolate_with_cubic_bezier(
|
||||||
|
start_keyframe,
|
||||||
|
start_tangent_keyframe,
|
||||||
|
end_tangent_keyframe,
|
||||||
|
end_keyframe,
|
||||||
|
time,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
*dest = T::interpolate(dest, &value, weight);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates a cubic Bézier curve at a value `t`, given two endpoints and the
|
||||||
|
/// derivatives at those endpoints.
|
||||||
|
///
|
||||||
|
/// The derivatives are linearly scaled by `duration`.
|
||||||
|
fn interpolate_with_cubic_bezier<T>(p0: &T, d0: &T, d3: &T, p3: &T, t: f32, duration: f32) -> T
|
||||||
|
where
|
||||||
|
T: Animatable + Clone,
|
||||||
|
{
|
||||||
|
// We're given two endpoints, along with the derivatives at those endpoints,
|
||||||
|
// and have to evaluate the cubic Bézier curve at time t using only
|
||||||
|
// (additive) blending and linear interpolation.
|
||||||
|
//
|
||||||
|
// Evaluating a Bézier curve via repeated linear interpolation when the
|
||||||
|
// control points are known is straightforward via [de Casteljau
|
||||||
|
// subdivision]. So the only remaining problem is to get the two off-curve
|
||||||
|
// control points. The [derivative of the cubic Bézier curve] is:
|
||||||
|
//
|
||||||
|
// B′(t) = 3(1 - t)²(P₁ - P₀) + 6(1 - t)t(P₂ - P₁) + 3t²(P₃ - P₂)
|
||||||
|
//
|
||||||
|
// Setting t = 0 and t = 1 and solving gives us:
|
||||||
|
//
|
||||||
|
// P₁ = P₀ + B′(0) / 3
|
||||||
|
// P₂ = P₃ - B′(1) / 3
|
||||||
|
//
|
||||||
|
// These P₁ and P₂ formulas can be expressed as additive blends.
|
||||||
|
//
|
||||||
|
// So, to sum up, first we calculate the off-curve control points via
|
||||||
|
// additive blending, and then we use repeated linear interpolation to
|
||||||
|
// evaluate the curve.
|
||||||
|
//
|
||||||
|
// [de Casteljau subdivision]: https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm
|
||||||
|
// [derivative of the cubic Bézier curve]: https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B%C3%A9zier_curves
|
||||||
|
|
||||||
|
// Compute control points from derivatives.
|
||||||
|
let p1 = T::blend(
|
||||||
|
[
|
||||||
|
BlendInput {
|
||||||
|
weight: duration / 3.0,
|
||||||
|
value: (*d0).clone(),
|
||||||
|
additive: true,
|
||||||
|
},
|
||||||
|
BlendInput {
|
||||||
|
weight: 1.0,
|
||||||
|
value: (*p0).clone(),
|
||||||
|
additive: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
.into_iter(),
|
||||||
|
);
|
||||||
|
let p2 = T::blend(
|
||||||
|
[
|
||||||
|
BlendInput {
|
||||||
|
weight: duration / -3.0,
|
||||||
|
value: (*d3).clone(),
|
||||||
|
additive: true,
|
||||||
|
},
|
||||||
|
BlendInput {
|
||||||
|
weight: 1.0,
|
||||||
|
value: (*p3).clone(),
|
||||||
|
additive: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
.into_iter(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use de Casteljau subdivision to evaluate.
|
||||||
|
let p0p1 = T::interpolate(p0, &p1, t);
|
||||||
|
let p1p2 = T::interpolate(&p1, &p2, t);
|
||||||
|
let p2p3 = T::interpolate(&p2, p3, t);
|
||||||
|
let p0p1p2 = T::interpolate(&p0p1, &p1p2, t);
|
||||||
|
let p1p2p3 = T::interpolate(&p1p2, &p2p3, t);
|
||||||
|
T::interpolate(&p0p1p2, &p1p2p3, t)
|
||||||
|
}
|
||||||
|
|
591
crates/bevy_animation/src/keyframes.rs
Normal file
591
crates/bevy_animation/src/keyframes.rs
Normal file
|
@ -0,0 +1,591 @@
|
||||||
|
//! Keyframes of animation clips.
|
||||||
|
|
||||||
|
use std::any::TypeId;
|
||||||
|
use std::fmt::{self, Debug, Formatter};
|
||||||
|
|
||||||
|
use bevy_asset::Handle;
|
||||||
|
use bevy_derive::{Deref, DerefMut};
|
||||||
|
use bevy_ecs::component::Component;
|
||||||
|
use bevy_ecs::world::{EntityMutExcept, Mut};
|
||||||
|
use bevy_math::{Quat, Vec3};
|
||||||
|
use bevy_reflect::{FromReflect, GetTypeRegistration, Reflect, TypePath, Typed};
|
||||||
|
use bevy_render::mesh::morph::MorphWeights;
|
||||||
|
use bevy_transform::prelude::Transform;
|
||||||
|
|
||||||
|
use crate::graph::AnimationGraph;
|
||||||
|
use crate::prelude::{Animatable, GetKeyframe};
|
||||||
|
use crate::{animatable, AnimationEvaluationError, AnimationPlayer, Interpolation};
|
||||||
|
|
||||||
|
/// A value on a component that Bevy can animate.
|
||||||
|
///
|
||||||
|
/// You can implement this trait on a unit struct in order to support animating
|
||||||
|
/// custom components other than transforms and morph weights. Use that type in
|
||||||
|
/// conjunction with [`AnimatablePropertyKeyframes`]. For example, in order to
|
||||||
|
/// animate font size of a text section from 24 pt. to 80 pt., you might use:
|
||||||
|
///
|
||||||
|
/// # use bevy_animation::prelude::AnimatableProperty;
|
||||||
|
/// # use bevy_reflect::Reflect;
|
||||||
|
/// # use bevy_text::Text;
|
||||||
|
/// #[derive(Reflect)]
|
||||||
|
/// struct FontSizeProperty;
|
||||||
|
///
|
||||||
|
/// impl AnimatableProperty for FontSizeProperty {
|
||||||
|
/// type Component = Text;
|
||||||
|
/// type Property = f32;
|
||||||
|
/// fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property> {
|
||||||
|
/// Some(&mut component.sections.get_mut(0)?.style.font_size)
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// You can then create a [`crate::AnimationClip`] to animate this property like so:
|
||||||
|
///
|
||||||
|
/// # use bevy_animation::{AnimationClip, AnimationTargetId, Interpolation, VariableCurve};
|
||||||
|
/// # use bevy_animation::prelude::{AnimatableProperty, AnimatablePropertyKeyframes};
|
||||||
|
/// # use bevy_core::Name;
|
||||||
|
/// # use bevy_reflect::Reflect;
|
||||||
|
/// # use bevy_text::Text;
|
||||||
|
/// # let animation_target_id = AnimationTargetId::from(&Name::new("Test"));
|
||||||
|
/// # #[derive(Reflect)]
|
||||||
|
/// # struct FontSizeProperty;
|
||||||
|
/// # impl AnimatableProperty for FontSizeProperty {
|
||||||
|
/// # type Component = Text;
|
||||||
|
/// # type Property = f32;
|
||||||
|
/// # fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property> {
|
||||||
|
/// # Some(&mut component.sections.get_mut(0)?.style.font_size)
|
||||||
|
/// # }
|
||||||
|
/// # }
|
||||||
|
/// let mut animation_clip = AnimationClip::default();
|
||||||
|
/// animation_clip.add_curve_to_target(
|
||||||
|
/// animation_target_id,
|
||||||
|
/// VariableCurve::linear::<AnimatablePropertyKeyframes<FontSizeProperty>>(
|
||||||
|
/// [0.0, 1.0],
|
||||||
|
/// [24.0, 80.0],
|
||||||
|
/// ),
|
||||||
|
/// );
|
||||||
|
pub trait AnimatableProperty: Reflect + TypePath + 'static {
|
||||||
|
/// The type of the component that the property lives on.
|
||||||
|
type Component: Component;
|
||||||
|
|
||||||
|
/// The type of the property to be animated.
|
||||||
|
type Property: Animatable
|
||||||
|
+ FromReflect
|
||||||
|
+ GetTypeRegistration
|
||||||
|
+ Reflect
|
||||||
|
+ TypePath
|
||||||
|
+ Typed
|
||||||
|
+ Clone
|
||||||
|
+ Sync
|
||||||
|
+ Debug
|
||||||
|
+ 'static;
|
||||||
|
|
||||||
|
/// Given a reference to the component, returns a reference to the property.
|
||||||
|
///
|
||||||
|
/// If the property couldn't be found, returns `None`.
|
||||||
|
fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Keyframes in a [`crate::VariableCurve`] that animate an
|
||||||
|
/// [`AnimatableProperty`].
|
||||||
|
///
|
||||||
|
/// This is the a generic type of [`Keyframes`] that can animate any
|
||||||
|
/// [`AnimatableProperty`]. See the documentation for [`AnimatableProperty`] for
|
||||||
|
/// more information as to how to use this type.
|
||||||
|
///
|
||||||
|
/// If you're animating scale, rotation, or translation of a [`Transform`],
|
||||||
|
/// [`ScaleKeyframes`], [`RotationKeyframes`], and [`TranslationKeyframes`] are
|
||||||
|
/// faster, and simpler, alternatives to this type.
|
||||||
|
#[derive(Reflect, Deref, DerefMut)]
|
||||||
|
pub struct AnimatablePropertyKeyframes<P>(pub Vec<P::Property>)
|
||||||
|
where
|
||||||
|
P: AnimatableProperty;
|
||||||
|
|
||||||
|
impl<P> Clone for AnimatablePropertyKeyframes<P>
|
||||||
|
where
|
||||||
|
P: AnimatableProperty,
|
||||||
|
{
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self(self.0.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P> Debug for AnimatablePropertyKeyframes<P>
|
||||||
|
where
|
||||||
|
P: AnimatableProperty,
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_tuple("AnimatablePropertyKeyframes")
|
||||||
|
.field(&self.0)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A low-level trait for use in [`crate::VariableCurve`] that provides fine
|
||||||
|
/// control over how animations are evaluated.
|
||||||
|
///
|
||||||
|
/// You can implement this trait when the generic
|
||||||
|
/// [`AnimatablePropertyKeyframes`] isn't sufficiently-expressive for your
|
||||||
|
/// needs. For example, [`MorphWeights`] implements this trait instead of using
|
||||||
|
/// [`AnimatablePropertyKeyframes`] because it needs to animate arbitrarily many
|
||||||
|
/// weights at once, which can't be done with [`Animatable`] as that works on
|
||||||
|
/// fixed-size values only.
|
||||||
|
pub trait Keyframes: Reflect + Debug + Send + Sync {
|
||||||
|
/// Returns a boxed clone of this value.
|
||||||
|
fn clone_value(&self) -> Box<dyn Keyframes>;
|
||||||
|
|
||||||
|
/// Interpolates between the existing value and the value of the first
|
||||||
|
/// keyframe, and writes the value into `transform` and/or `entity` as
|
||||||
|
/// appropriate.
|
||||||
|
///
|
||||||
|
/// Arguments:
|
||||||
|
///
|
||||||
|
/// * `transform`: The transform of the entity, if present.
|
||||||
|
///
|
||||||
|
/// * `entity`: Allows access to the rest of the components of the entity.
|
||||||
|
///
|
||||||
|
/// * `weight`: The blend weight between the existing component value (0.0)
|
||||||
|
/// and the one computed from the keyframes (1.0).
|
||||||
|
fn apply_single_keyframe<'a>(
|
||||||
|
&self,
|
||||||
|
transform: Option<Mut<'a, Transform>>,
|
||||||
|
entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle<AnimationGraph>)>,
|
||||||
|
weight: f32,
|
||||||
|
) -> Result<(), AnimationEvaluationError>;
|
||||||
|
|
||||||
|
/// Interpolates between the existing value and the value of the two nearest
|
||||||
|
/// keyframes, and writes the value into `transform` and/or `entity` as
|
||||||
|
/// appropriate.
|
||||||
|
///
|
||||||
|
/// Arguments:
|
||||||
|
///
|
||||||
|
/// * `transform`: The transform of the entity, if present.
|
||||||
|
///
|
||||||
|
/// * `entity`: Allows access to the rest of the components of the entity.
|
||||||
|
///
|
||||||
|
/// * `interpolation`: The type of interpolation to use.
|
||||||
|
///
|
||||||
|
/// * `step_start`: The index of the first keyframe.
|
||||||
|
///
|
||||||
|
/// * `time`: The blend weight between the first keyframe (0.0) and the next
|
||||||
|
/// keyframe (1.0).
|
||||||
|
///
|
||||||
|
/// * `weight`: The blend weight between the existing component value (0.0)
|
||||||
|
/// and the one computed from the keyframes (1.0).
|
||||||
|
///
|
||||||
|
/// If `interpolation` is `Interpolation::Linear`, then pseudocode for this
|
||||||
|
/// function could be `property = lerp(property, lerp(keyframes[step_start],
|
||||||
|
/// keyframes[step_start + 1], time), weight)`.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn apply_tweened_keyframes<'a>(
|
||||||
|
&self,
|
||||||
|
transform: Option<Mut<'a, Transform>>,
|
||||||
|
entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle<AnimationGraph>)>,
|
||||||
|
interpolation: Interpolation,
|
||||||
|
step_start: usize,
|
||||||
|
time: f32,
|
||||||
|
weight: f32,
|
||||||
|
duration: f32,
|
||||||
|
) -> Result<(), AnimationEvaluationError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Keyframes for animating [`Transform::translation`].
|
||||||
|
///
|
||||||
|
/// An example of a [`crate::AnimationClip`] that animates translation:
|
||||||
|
///
|
||||||
|
/// # use bevy_animation::{AnimationClip, AnimationTargetId, Interpolation};
|
||||||
|
/// # use bevy_animation::{VariableCurve, prelude::TranslationKeyframes};
|
||||||
|
/// # use bevy_core::Name;
|
||||||
|
/// # use bevy_math::Vec3;
|
||||||
|
/// # let animation_target_id = AnimationTargetId::from(&Name::new("Test"));
|
||||||
|
/// let mut animation_clip = AnimationClip::default();
|
||||||
|
/// animation_clip.add_curve_to_target(
|
||||||
|
/// animation_target_id,
|
||||||
|
/// VariableCurve::linear::<TranslationKeyframes>(
|
||||||
|
/// [0.0, 1.0],
|
||||||
|
/// [Vec3::ZERO, Vec3::ONE],
|
||||||
|
/// ),
|
||||||
|
/// );
|
||||||
|
#[derive(Clone, Reflect, Debug, Deref, DerefMut)]
|
||||||
|
pub struct TranslationKeyframes(pub Vec<Vec3>);
|
||||||
|
|
||||||
|
/// Keyframes for animating [`Transform::scale`].
|
||||||
|
///
|
||||||
|
/// An example of a [`crate::AnimationClip`] that animates translation:
|
||||||
|
///
|
||||||
|
/// # use bevy_animation::{AnimationClip, AnimationTargetId, Interpolation};
|
||||||
|
/// # use bevy_animation::{VariableCurve, prelude::ScaleKeyframes};
|
||||||
|
/// # use bevy_core::Name;
|
||||||
|
/// # use bevy_math::Vec3;
|
||||||
|
/// # let animation_target_id = AnimationTargetId::from(&Name::new("Test"));
|
||||||
|
/// let mut animation_clip = AnimationClip::default();
|
||||||
|
/// animation_clip.add_curve_to_target(
|
||||||
|
/// animation_target_id,
|
||||||
|
/// VariableCurve::linear::<ScaleKeyframes>(
|
||||||
|
/// [0.0, 1.0],
|
||||||
|
/// [Vec3::ONE, Vec3::splat(2.0)],
|
||||||
|
/// ),
|
||||||
|
/// );
|
||||||
|
#[derive(Clone, Reflect, Debug, Deref, DerefMut)]
|
||||||
|
pub struct ScaleKeyframes(pub Vec<Vec3>);
|
||||||
|
|
||||||
|
/// Keyframes for animating [`Transform::rotation`].
|
||||||
|
///
|
||||||
|
/// An example of a [`crate::AnimationClip`] that animates translation:
|
||||||
|
///
|
||||||
|
/// # use bevy_animation::{AnimationClip, AnimationTargetId, Interpolation};
|
||||||
|
/// # use bevy_animation::{VariableCurve, prelude::RotationKeyframes};
|
||||||
|
/// # use bevy_core::Name;
|
||||||
|
/// # use bevy_math::Quat;
|
||||||
|
/// # use std::f32::consts::FRAC_PI_2;
|
||||||
|
/// # let animation_target_id = AnimationTargetId::from(&Name::new("Test"));
|
||||||
|
/// let mut animation_clip = AnimationClip::default();
|
||||||
|
/// animation_clip.add_curve_to_target(
|
||||||
|
/// animation_target_id,
|
||||||
|
/// VariableCurve::linear::<RotationKeyframes>(
|
||||||
|
/// [0.0, 1.0],
|
||||||
|
/// [Quat::from_rotation_x(FRAC_PI_2), Quat::from_rotation_y(FRAC_PI_2)],
|
||||||
|
/// ),
|
||||||
|
/// );
|
||||||
|
#[derive(Clone, Reflect, Debug, Deref, DerefMut)]
|
||||||
|
pub struct RotationKeyframes(pub Vec<Quat>);
|
||||||
|
|
||||||
|
/// Keyframes for animating [`MorphWeights`].
|
||||||
|
#[derive(Clone, Debug, Reflect)]
|
||||||
|
pub struct MorphWeightsKeyframes {
|
||||||
|
/// The total number of morph weights.
|
||||||
|
pub morph_target_count: usize,
|
||||||
|
|
||||||
|
/// The morph weights.
|
||||||
|
///
|
||||||
|
/// The length of this vector should be the total number of morph weights
|
||||||
|
/// times the number of keyframes.
|
||||||
|
pub weights: Vec<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<T> for TranslationKeyframes
|
||||||
|
where
|
||||||
|
T: Into<Vec<Vec3>>,
|
||||||
|
{
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Self(value.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Keyframes for TranslationKeyframes {
|
||||||
|
fn clone_value(&self) -> Box<dyn Keyframes> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_single_keyframe<'a>(
|
||||||
|
&self,
|
||||||
|
transform: Option<Mut<'a, Transform>>,
|
||||||
|
_: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle<AnimationGraph>)>,
|
||||||
|
weight: f32,
|
||||||
|
) -> Result<(), AnimationEvaluationError> {
|
||||||
|
let mut component = transform.ok_or_else(|| {
|
||||||
|
AnimationEvaluationError::ComponentNotPresent(TypeId::of::<Transform>())
|
||||||
|
})?;
|
||||||
|
let value = self
|
||||||
|
.first()
|
||||||
|
.ok_or(AnimationEvaluationError::KeyframeNotPresent(0))?;
|
||||||
|
component.translation = Animatable::interpolate(&component.translation, value, weight);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_tweened_keyframes<'a>(
|
||||||
|
&self,
|
||||||
|
transform: Option<Mut<'a, Transform>>,
|
||||||
|
_: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle<AnimationGraph>)>,
|
||||||
|
interpolation: Interpolation,
|
||||||
|
step_start: usize,
|
||||||
|
time: f32,
|
||||||
|
weight: f32,
|
||||||
|
duration: f32,
|
||||||
|
) -> Result<(), AnimationEvaluationError> {
|
||||||
|
let mut component = transform.ok_or_else(|| {
|
||||||
|
AnimationEvaluationError::ComponentNotPresent(TypeId::of::<Transform>())
|
||||||
|
})?;
|
||||||
|
animatable::interpolate_keyframes(
|
||||||
|
&mut component.translation,
|
||||||
|
&(*self)[..],
|
||||||
|
interpolation,
|
||||||
|
step_start,
|
||||||
|
time,
|
||||||
|
weight,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<T> for ScaleKeyframes
|
||||||
|
where
|
||||||
|
T: Into<Vec<Vec3>>,
|
||||||
|
{
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Self(value.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Keyframes for ScaleKeyframes {
|
||||||
|
fn clone_value(&self) -> Box<dyn Keyframes> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_single_keyframe<'a>(
|
||||||
|
&self,
|
||||||
|
transform: Option<Mut<'a, Transform>>,
|
||||||
|
_: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle<AnimationGraph>)>,
|
||||||
|
weight: f32,
|
||||||
|
) -> Result<(), AnimationEvaluationError> {
|
||||||
|
let mut component = transform.ok_or_else(|| {
|
||||||
|
AnimationEvaluationError::ComponentNotPresent(TypeId::of::<Transform>())
|
||||||
|
})?;
|
||||||
|
let value = self
|
||||||
|
.first()
|
||||||
|
.ok_or(AnimationEvaluationError::KeyframeNotPresent(0))?;
|
||||||
|
component.scale = Animatable::interpolate(&component.scale, value, weight);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_tweened_keyframes<'a>(
|
||||||
|
&self,
|
||||||
|
transform: Option<Mut<'a, Transform>>,
|
||||||
|
_: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle<AnimationGraph>)>,
|
||||||
|
interpolation: Interpolation,
|
||||||
|
step_start: usize,
|
||||||
|
time: f32,
|
||||||
|
weight: f32,
|
||||||
|
duration: f32,
|
||||||
|
) -> Result<(), AnimationEvaluationError> {
|
||||||
|
let mut component = transform.ok_or_else(|| {
|
||||||
|
AnimationEvaluationError::ComponentNotPresent(TypeId::of::<Transform>())
|
||||||
|
})?;
|
||||||
|
animatable::interpolate_keyframes(
|
||||||
|
&mut component.scale,
|
||||||
|
&(*self)[..],
|
||||||
|
interpolation,
|
||||||
|
step_start,
|
||||||
|
time,
|
||||||
|
weight,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<T> for RotationKeyframes
|
||||||
|
where
|
||||||
|
T: Into<Vec<Quat>>,
|
||||||
|
{
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Self(value.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Keyframes for RotationKeyframes {
|
||||||
|
fn clone_value(&self) -> Box<dyn Keyframes> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_single_keyframe<'a>(
|
||||||
|
&self,
|
||||||
|
transform: Option<Mut<'a, Transform>>,
|
||||||
|
_: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle<AnimationGraph>)>,
|
||||||
|
weight: f32,
|
||||||
|
) -> Result<(), AnimationEvaluationError> {
|
||||||
|
let mut component = transform.ok_or_else(|| {
|
||||||
|
AnimationEvaluationError::ComponentNotPresent(TypeId::of::<Transform>())
|
||||||
|
})?;
|
||||||
|
let value = self
|
||||||
|
.first()
|
||||||
|
.ok_or(AnimationEvaluationError::KeyframeNotPresent(0))?;
|
||||||
|
component.rotation = Animatable::interpolate(&component.rotation, value, weight);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_tweened_keyframes<'a>(
|
||||||
|
&self,
|
||||||
|
transform: Option<Mut<'a, Transform>>,
|
||||||
|
_: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle<AnimationGraph>)>,
|
||||||
|
interpolation: Interpolation,
|
||||||
|
step_start: usize,
|
||||||
|
time: f32,
|
||||||
|
weight: f32,
|
||||||
|
duration: f32,
|
||||||
|
) -> Result<(), AnimationEvaluationError> {
|
||||||
|
let mut component = transform.ok_or_else(|| {
|
||||||
|
AnimationEvaluationError::ComponentNotPresent(TypeId::of::<Transform>())
|
||||||
|
})?;
|
||||||
|
animatable::interpolate_keyframes(
|
||||||
|
&mut component.rotation,
|
||||||
|
&(*self)[..],
|
||||||
|
interpolation,
|
||||||
|
step_start,
|
||||||
|
time,
|
||||||
|
weight,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P, T> From<T> for AnimatablePropertyKeyframes<P>
|
||||||
|
where
|
||||||
|
P: AnimatableProperty,
|
||||||
|
T: Into<Vec<P::Property>>,
|
||||||
|
{
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Self(value.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P> Keyframes for AnimatablePropertyKeyframes<P>
|
||||||
|
where
|
||||||
|
P: AnimatableProperty,
|
||||||
|
{
|
||||||
|
fn clone_value(&self) -> Box<dyn Keyframes> {
|
||||||
|
Box::new((*self).clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_single_keyframe<'a>(
|
||||||
|
&self,
|
||||||
|
_: Option<Mut<'a, Transform>>,
|
||||||
|
mut entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle<AnimationGraph>)>,
|
||||||
|
weight: f32,
|
||||||
|
) -> Result<(), AnimationEvaluationError> {
|
||||||
|
let mut component = entity.get_mut::<P::Component>().ok_or_else(|| {
|
||||||
|
AnimationEvaluationError::ComponentNotPresent(TypeId::of::<P::Component>())
|
||||||
|
})?;
|
||||||
|
let property = P::get_mut(&mut component)
|
||||||
|
.ok_or_else(|| AnimationEvaluationError::PropertyNotPresent(TypeId::of::<P>()))?;
|
||||||
|
let value = self
|
||||||
|
.first()
|
||||||
|
.ok_or(AnimationEvaluationError::KeyframeNotPresent(0))?;
|
||||||
|
<P::Property>::interpolate(property, value, weight);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_tweened_keyframes<'a>(
|
||||||
|
&self,
|
||||||
|
_: Option<Mut<'a, Transform>>,
|
||||||
|
mut entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle<AnimationGraph>)>,
|
||||||
|
interpolation: Interpolation,
|
||||||
|
step_start: usize,
|
||||||
|
time: f32,
|
||||||
|
weight: f32,
|
||||||
|
duration: f32,
|
||||||
|
) -> Result<(), AnimationEvaluationError> {
|
||||||
|
let mut component = entity.get_mut::<P::Component>().ok_or_else(|| {
|
||||||
|
AnimationEvaluationError::ComponentNotPresent(TypeId::of::<P::Component>())
|
||||||
|
})?;
|
||||||
|
let property = P::get_mut(&mut component)
|
||||||
|
.ok_or_else(|| AnimationEvaluationError::PropertyNotPresent(TypeId::of::<P>()))?;
|
||||||
|
animatable::interpolate_keyframes(
|
||||||
|
property,
|
||||||
|
self,
|
||||||
|
interpolation,
|
||||||
|
step_start,
|
||||||
|
time,
|
||||||
|
weight,
|
||||||
|
duration,
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A> GetKeyframe for [A]
|
||||||
|
where
|
||||||
|
A: Animatable,
|
||||||
|
{
|
||||||
|
type Output = A;
|
||||||
|
|
||||||
|
fn get_keyframe(&self, index: usize) -> Option<&Self::Output> {
|
||||||
|
self.get(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P> GetKeyframe for AnimatablePropertyKeyframes<P>
|
||||||
|
where
|
||||||
|
P: AnimatableProperty,
|
||||||
|
{
|
||||||
|
type Output = P::Property;
|
||||||
|
|
||||||
|
fn get_keyframe(&self, index: usize) -> Option<&Self::Output> {
|
||||||
|
self.get(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Information needed to look up morph weight values in the flattened morph
|
||||||
|
/// weight keyframes vector.
|
||||||
|
struct GetMorphWeightKeyframe<'k> {
|
||||||
|
/// The morph weights keyframe structure that we're animating.
|
||||||
|
keyframes: &'k MorphWeightsKeyframes,
|
||||||
|
/// The index of the morph target in that structure.
|
||||||
|
morph_target_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Keyframes for MorphWeightsKeyframes {
|
||||||
|
fn clone_value(&self) -> Box<dyn Keyframes> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_single_keyframe<'a>(
|
||||||
|
&self,
|
||||||
|
_: Option<Mut<'a, Transform>>,
|
||||||
|
mut entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle<AnimationGraph>)>,
|
||||||
|
weight: f32,
|
||||||
|
) -> Result<(), AnimationEvaluationError> {
|
||||||
|
let mut dest = entity.get_mut::<MorphWeights>().ok_or_else(|| {
|
||||||
|
AnimationEvaluationError::ComponentNotPresent(TypeId::of::<MorphWeights>())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// TODO: Go 4 weights at a time to make better use of SIMD.
|
||||||
|
for (morph_target_index, morph_weight) in dest.weights_mut().iter_mut().enumerate() {
|
||||||
|
*morph_weight =
|
||||||
|
f32::interpolate(morph_weight, &self.weights[morph_target_index], weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_tweened_keyframes<'a>(
|
||||||
|
&self,
|
||||||
|
_: Option<Mut<'a, Transform>>,
|
||||||
|
mut entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle<AnimationGraph>)>,
|
||||||
|
interpolation: Interpolation,
|
||||||
|
step_start: usize,
|
||||||
|
time: f32,
|
||||||
|
weight: f32,
|
||||||
|
duration: f32,
|
||||||
|
) -> Result<(), AnimationEvaluationError> {
|
||||||
|
let mut dest = entity.get_mut::<MorphWeights>().ok_or_else(|| {
|
||||||
|
AnimationEvaluationError::ComponentNotPresent(TypeId::of::<MorphWeights>())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// TODO: Go 4 weights at a time to make better use of SIMD.
|
||||||
|
for (morph_target_index, morph_weight) in dest.weights_mut().iter_mut().enumerate() {
|
||||||
|
animatable::interpolate_keyframes(
|
||||||
|
morph_weight,
|
||||||
|
&GetMorphWeightKeyframe {
|
||||||
|
keyframes: self,
|
||||||
|
morph_target_index,
|
||||||
|
},
|
||||||
|
interpolation,
|
||||||
|
step_start,
|
||||||
|
time,
|
||||||
|
weight,
|
||||||
|
duration,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetKeyframe for GetMorphWeightKeyframe<'_> {
|
||||||
|
type Output = f32;
|
||||||
|
|
||||||
|
fn get_keyframe(&self, keyframe_index: usize) -> Option<&Self::Output> {
|
||||||
|
self.keyframes
|
||||||
|
.weights
|
||||||
|
.as_slice()
|
||||||
|
.get(keyframe_index * self.keyframes.morph_target_count + self.morph_target_index)
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,28 +9,39 @@
|
||||||
|
|
||||||
pub mod animatable;
|
pub mod animatable;
|
||||||
pub mod graph;
|
pub mod graph;
|
||||||
|
pub mod keyframes;
|
||||||
pub mod transition;
|
pub mod transition;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
|
use std::any::{Any, TypeId};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
use std::fmt::Debug;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::iter;
|
use std::iter;
|
||||||
use std::ops::{Add, Mul};
|
|
||||||
|
|
||||||
use bevy_app::{App, Plugin, PostUpdate};
|
use bevy_app::{App, Plugin, PostUpdate};
|
||||||
use bevy_asset::{Asset, AssetApp, Assets, Handle};
|
use bevy_asset::{Asset, AssetApp, Assets, Handle};
|
||||||
use bevy_core::Name;
|
use bevy_core::Name;
|
||||||
use bevy_ecs::{entity::MapEntities, prelude::*, reflect::ReflectMapEntities};
|
use bevy_ecs::entity::MapEntities;
|
||||||
use bevy_math::{FloatExt, FloatPow, Quat, Vec3};
|
use bevy_ecs::prelude::*;
|
||||||
use bevy_reflect::std_traits::ReflectDefault;
|
use bevy_ecs::reflect::ReflectMapEntities;
|
||||||
use bevy_reflect::Reflect;
|
use bevy_ecs::world::EntityMutExcept;
|
||||||
use bevy_render::mesh::morph::MorphWeights;
|
use bevy_math::FloatExt;
|
||||||
|
use bevy_reflect::utility::NonGenericTypeInfoCell;
|
||||||
|
use bevy_reflect::{prelude::ReflectDefault, Reflect};
|
||||||
|
use bevy_reflect::{
|
||||||
|
ApplyError, DynamicStruct, FieldIter, FromReflect, FromType, GetTypeRegistration, NamedField,
|
||||||
|
PartialReflect, ReflectFromPtr, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, Struct,
|
||||||
|
StructInfo, TypeInfo, TypePath, TypeRegistration, Typed,
|
||||||
|
};
|
||||||
use bevy_time::Time;
|
use bevy_time::Time;
|
||||||
use bevy_transform::{prelude::Transform, TransformSystem};
|
use bevy_transform::prelude::Transform;
|
||||||
|
use bevy_transform::TransformSystem;
|
||||||
|
use bevy_ui::UiSystem;
|
||||||
|
use bevy_utils::hashbrown::HashMap;
|
||||||
use bevy_utils::{
|
use bevy_utils::{
|
||||||
hashbrown::HashMap,
|
tracing::{trace, warn},
|
||||||
tracing::{error, trace},
|
|
||||||
NoOpHash,
|
NoOpHash,
|
||||||
};
|
};
|
||||||
use fixedbitset::FixedBitSet;
|
use fixedbitset::FixedBitSet;
|
||||||
|
@ -46,13 +57,14 @@ use uuid::Uuid;
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub use crate::{
|
pub use crate::{
|
||||||
animatable::*, graph::*, transition::*, AnimationClip, AnimationPlayer, AnimationPlugin,
|
animatable::*, graph::*, keyframes::*, transition::*, AnimationClip, AnimationPlayer,
|
||||||
Interpolation, Keyframes, VariableCurve,
|
AnimationPlugin, Interpolation, VariableCurve,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
graph::{AnimationGraph, AnimationGraphAssetLoader, AnimationNodeIndex},
|
graph::{AnimationGraph, AnimationGraphAssetLoader, AnimationNodeIndex},
|
||||||
|
keyframes::Keyframes,
|
||||||
transition::{advance_transitions, expire_completed_transitions, AnimationTransitions},
|
transition::{advance_transitions, expire_completed_transitions, AnimationTransitions},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -61,46 +73,11 @@ use crate::{
|
||||||
/// [UUID namespace]: https://en.wikipedia.org/wiki/Universally_unique_identifier#Versions_3_and_5_(namespace_name-based)
|
/// [UUID namespace]: https://en.wikipedia.org/wiki/Universally_unique_identifier#Versions_3_and_5_(namespace_name-based)
|
||||||
pub static ANIMATION_TARGET_NAMESPACE: Uuid = Uuid::from_u128(0x3179f519d9274ff2b5966fd077023911);
|
pub static ANIMATION_TARGET_NAMESPACE: Uuid = Uuid::from_u128(0x3179f519d9274ff2b5966fd077023911);
|
||||||
|
|
||||||
/// List of keyframes for one of the attribute of a [`Transform`].
|
/// Describes how an attribute of a [`Transform`] or
|
||||||
#[derive(Reflect, Clone, Debug)]
|
/// [`bevy_render::mesh::morph::MorphWeights`] should be animated.
|
||||||
pub enum Keyframes {
|
|
||||||
/// Keyframes for rotation.
|
|
||||||
Rotation(Vec<Quat>),
|
|
||||||
/// Keyframes for translation.
|
|
||||||
Translation(Vec<Vec3>),
|
|
||||||
/// Keyframes for scale.
|
|
||||||
Scale(Vec<Vec3>),
|
|
||||||
/// Keyframes for morph target weights.
|
|
||||||
///
|
|
||||||
/// Note that in `.0`, each contiguous `target_count` values is a single
|
|
||||||
/// keyframe representing the weight values at given keyframe.
|
|
||||||
///
|
|
||||||
/// This follows the [glTF design].
|
|
||||||
///
|
|
||||||
/// [glTF design]: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#animations
|
|
||||||
Weights(Vec<f32>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Keyframes {
|
|
||||||
/// Returns the number of keyframes.
|
|
||||||
pub fn len(&self) -> usize {
|
|
||||||
match self {
|
|
||||||
Keyframes::Weights(vec) => vec.len(),
|
|
||||||
Keyframes::Translation(vec) | Keyframes::Scale(vec) => vec.len(),
|
|
||||||
Keyframes::Rotation(vec) => vec.len(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the number of keyframes is zero.
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.len() == 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Describes how an attribute of a [`Transform`] or [`MorphWeights`] should be animated.
|
|
||||||
///
|
///
|
||||||
/// `keyframe_timestamps` and `keyframes` should have the same length.
|
/// `keyframe_timestamps` and `keyframes` should have the same length.
|
||||||
#[derive(Reflect, Clone, Debug)]
|
#[derive(Debug, TypePath)]
|
||||||
pub struct VariableCurve {
|
pub struct VariableCurve {
|
||||||
/// Timestamp for each of the keyframes.
|
/// Timestamp for each of the keyframes.
|
||||||
pub keyframe_timestamps: Vec<f32>,
|
pub keyframe_timestamps: Vec<f32>,
|
||||||
|
@ -111,12 +88,84 @@ pub struct VariableCurve {
|
||||||
/// - for `Interpolation::Step` and `Interpolation::Linear`, each keyframe is a single value
|
/// - for `Interpolation::Step` and `Interpolation::Linear`, each keyframe is a single value
|
||||||
/// - for `Interpolation::CubicSpline`, each keyframe is made of three values for `tangent_in`,
|
/// - for `Interpolation::CubicSpline`, each keyframe is made of three values for `tangent_in`,
|
||||||
/// `keyframe_value` and `tangent_out`
|
/// `keyframe_value` and `tangent_out`
|
||||||
pub keyframes: Keyframes,
|
pub keyframes: Box<dyn Keyframes>,
|
||||||
/// Interpolation method to use between keyframes.
|
/// Interpolation method to use between keyframes.
|
||||||
pub interpolation: Interpolation,
|
pub interpolation: Interpolation,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Clone for VariableCurve {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
VariableCurve {
|
||||||
|
keyframe_timestamps: self.keyframe_timestamps.clone(),
|
||||||
|
keyframes: Keyframes::clone_value(&*self.keyframes),
|
||||||
|
interpolation: self.interpolation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl VariableCurve {
|
impl VariableCurve {
|
||||||
|
/// Creates a new curve from timestamps, keyframes, and interpolation type.
|
||||||
|
///
|
||||||
|
/// The two arrays must have the same length.
|
||||||
|
pub fn new<K>(
|
||||||
|
keyframe_timestamps: Vec<f32>,
|
||||||
|
keyframes: impl Into<K>,
|
||||||
|
interpolation: Interpolation,
|
||||||
|
) -> VariableCurve
|
||||||
|
where
|
||||||
|
K: Keyframes,
|
||||||
|
{
|
||||||
|
VariableCurve {
|
||||||
|
keyframe_timestamps,
|
||||||
|
keyframes: Box::new(keyframes.into()),
|
||||||
|
interpolation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new curve from timestamps and keyframes with no interpolation.
|
||||||
|
///
|
||||||
|
/// The two arrays must have the same length.
|
||||||
|
pub fn step<K>(
|
||||||
|
keyframe_timestamps: impl Into<Vec<f32>>,
|
||||||
|
keyframes: impl Into<K>,
|
||||||
|
) -> VariableCurve
|
||||||
|
where
|
||||||
|
K: Keyframes,
|
||||||
|
{
|
||||||
|
VariableCurve::new(keyframe_timestamps.into(), keyframes, Interpolation::Step)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new curve from timestamps and keyframes with linear
|
||||||
|
/// interpolation.
|
||||||
|
///
|
||||||
|
/// The two arrays must have the same length.
|
||||||
|
pub fn linear<K>(
|
||||||
|
keyframe_timestamps: impl Into<Vec<f32>>,
|
||||||
|
keyframes: impl Into<K>,
|
||||||
|
) -> VariableCurve
|
||||||
|
where
|
||||||
|
K: Keyframes,
|
||||||
|
{
|
||||||
|
VariableCurve::new(keyframe_timestamps.into(), keyframes, Interpolation::Linear)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new curve from timestamps and keyframes with no interpolation.
|
||||||
|
///
|
||||||
|
/// The two arrays must have the same length.
|
||||||
|
pub fn cubic_spline<K>(
|
||||||
|
keyframe_timestamps: impl Into<Vec<f32>>,
|
||||||
|
keyframes: impl Into<K>,
|
||||||
|
) -> VariableCurve
|
||||||
|
where
|
||||||
|
K: Keyframes,
|
||||||
|
{
|
||||||
|
VariableCurve::new(
|
||||||
|
keyframe_timestamps.into(),
|
||||||
|
keyframes,
|
||||||
|
Interpolation::CubicSpline,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Find the index of the keyframe at or before the current time.
|
/// Find the index of the keyframe at or before the current time.
|
||||||
///
|
///
|
||||||
/// Returns [`None`] if the curve is finished or not yet started.
|
/// Returns [`None`] if the curve is finished or not yet started.
|
||||||
|
@ -183,8 +232,223 @@ impl VariableCurve {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We have to implement `PartialReflect` manually because of the embedded
|
||||||
|
// `Box<dyn Keyframes>`, which can't be automatically derived yet.
|
||||||
|
impl PartialReflect for VariableCurve {
|
||||||
|
#[inline]
|
||||||
|
fn get_represented_type_info(&self) -> Option<&'static TypeInfo> {
|
||||||
|
Some(<Self as Typed>::type_info())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn into_partial_reflect(self: Box<Self>) -> Box<dyn PartialReflect> {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn as_partial_reflect(&self) -> &dyn PartialReflect {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_into_reflect(self: Box<Self>) -> Result<Box<dyn Reflect>, Box<dyn PartialReflect>> {
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_as_reflect(&self) -> Option<&dyn Reflect> {
|
||||||
|
Some(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> {
|
||||||
|
Some(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> {
|
||||||
|
if let ReflectRef::Struct(struct_value) = value.reflect_ref() {
|
||||||
|
for (i, value) in struct_value.iter_fields().enumerate() {
|
||||||
|
let name = struct_value.name_at(i).unwrap();
|
||||||
|
if let Some(v) = self.field_mut(name) {
|
||||||
|
v.try_apply(value)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(ApplyError::MismatchedKinds {
|
||||||
|
from_kind: value.reflect_kind(),
|
||||||
|
to_kind: ReflectKind::Struct,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reflect_ref(&self) -> ReflectRef {
|
||||||
|
ReflectRef::Struct(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reflect_mut(&mut self) -> ReflectMut {
|
||||||
|
ReflectMut::Struct(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reflect_owned(self: Box<Self>) -> ReflectOwned {
|
||||||
|
ReflectOwned::Struct(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clone_value(&self) -> Box<dyn PartialReflect> {
|
||||||
|
Box::new((*self).clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have to implement `Reflect` manually because of the embedded `Box<dyn
|
||||||
|
// Keyframes>`, which can't be automatically derived yet.
|
||||||
|
impl Reflect for VariableCurve {
|
||||||
|
#[inline]
|
||||||
|
fn into_any(self: Box<Self>) -> Box<dyn Any> {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn into_reflect(self: Box<Self>) -> Box<dyn Reflect> {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn as_reflect(&self) -> &dyn Reflect {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn as_reflect_mut(&mut self) -> &mut dyn Reflect {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn set(&mut self, value: Box<dyn Reflect>) -> Result<(), Box<dyn Reflect>> {
|
||||||
|
*self = value.take()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have to implement `Struct` manually because of the embedded `Box<dyn
|
||||||
|
// Keyframes>`, which can't be automatically derived yet.
|
||||||
|
impl Struct for VariableCurve {
|
||||||
|
fn field(&self, name: &str) -> Option<&dyn PartialReflect> {
|
||||||
|
match name {
|
||||||
|
"keyframe_timestamps" => Some(&self.keyframe_timestamps),
|
||||||
|
"keyframes" => Some(self.keyframes.as_partial_reflect()),
|
||||||
|
"interpolation" => Some(&self.interpolation),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field_mut(&mut self, name: &str) -> Option<&mut dyn PartialReflect> {
|
||||||
|
match name {
|
||||||
|
"keyframe_timestamps" => Some(&mut self.keyframe_timestamps),
|
||||||
|
"keyframes" => Some(self.keyframes.as_partial_reflect_mut()),
|
||||||
|
"interpolation" => Some(&mut self.interpolation),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field_at(&self, index: usize) -> Option<&dyn PartialReflect> {
|
||||||
|
match index {
|
||||||
|
0 => Some(&self.keyframe_timestamps),
|
||||||
|
1 => Some(self.keyframes.as_partial_reflect()),
|
||||||
|
2 => Some(&self.interpolation),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field_at_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> {
|
||||||
|
match index {
|
||||||
|
0 => Some(&mut self.keyframe_timestamps),
|
||||||
|
1 => Some(self.keyframes.as_partial_reflect_mut()),
|
||||||
|
2 => Some(&mut self.interpolation),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name_at(&self, index: usize) -> Option<&str> {
|
||||||
|
match index {
|
||||||
|
0 => Some("keyframe_timestamps"),
|
||||||
|
1 => Some("keyframes"),
|
||||||
|
2 => Some("interpolation"),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field_len(&self) -> usize {
|
||||||
|
3
|
||||||
|
}
|
||||||
|
|
||||||
|
fn iter_fields(&self) -> FieldIter {
|
||||||
|
FieldIter::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clone_dynamic(&self) -> DynamicStruct {
|
||||||
|
DynamicStruct::from_iter([
|
||||||
|
(
|
||||||
|
"keyframe_timestamps",
|
||||||
|
Box::new(self.keyframe_timestamps.clone()) as Box<dyn PartialReflect>,
|
||||||
|
),
|
||||||
|
("keyframes", PartialReflect::clone_value(&*self.keyframes)),
|
||||||
|
(
|
||||||
|
"interpolation",
|
||||||
|
Box::new(self.interpolation) as Box<dyn PartialReflect>,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have to implement `FromReflect` manually because of the embedded `Box<dyn
|
||||||
|
// Keyframes>`, which can't be automatically derived yet.
|
||||||
|
impl FromReflect for VariableCurve {
|
||||||
|
fn from_reflect(reflect: &dyn PartialReflect) -> Option<Self> {
|
||||||
|
Some(reflect.try_downcast_ref::<VariableCurve>()?.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have to implement `GetTypeRegistration` manually because of the embedded
|
||||||
|
// `Box<dyn Keyframes>`, which can't be automatically derived yet.
|
||||||
|
impl GetTypeRegistration for VariableCurve {
|
||||||
|
fn get_type_registration() -> TypeRegistration {
|
||||||
|
let mut registration = TypeRegistration::of::<Self>();
|
||||||
|
registration.insert::<ReflectFromPtr>(FromType::<Self>::from_type());
|
||||||
|
registration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have to implement `Typed` manually because of the embedded `Box<dyn
|
||||||
|
// Keyframes>`, which can't be automatically derived yet.
|
||||||
|
impl Typed for VariableCurve {
|
||||||
|
fn type_info() -> &'static TypeInfo {
|
||||||
|
static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new();
|
||||||
|
CELL.get_or_set(|| {
|
||||||
|
TypeInfo::Struct(StructInfo::new::<Self>(&[
|
||||||
|
NamedField::new::<Vec<f32>>("keyframe_timestamps"),
|
||||||
|
NamedField::new::<()>("keyframes"),
|
||||||
|
NamedField::new::<Interpolation>("interpolation"),
|
||||||
|
]))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Interpolation method to use between keyframes.
|
/// Interpolation method to use between keyframes.
|
||||||
#[derive(Reflect, Clone, Debug)]
|
#[derive(Reflect, Clone, Copy, Debug)]
|
||||||
pub enum Interpolation {
|
pub enum Interpolation {
|
||||||
/// Linear interpolation between the two closest keyframes.
|
/// Linear interpolation between the two closest keyframes.
|
||||||
Linear,
|
Linear,
|
||||||
|
@ -348,6 +612,29 @@ pub enum RepeatAnimation {
|
||||||
Forever,
|
Forever,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Why Bevy failed to evaluate an animation.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum AnimationEvaluationError {
|
||||||
|
/// The `keyframes` array is too small.
|
||||||
|
///
|
||||||
|
/// For curves with `Interpolation::Step` or `Interpolation::Linear`, the
|
||||||
|
/// `keyframes` array must have at least as many elements as keyframe
|
||||||
|
/// timestamps. For curves with `Interpolation::CubicBezier`, the
|
||||||
|
/// `keyframes` array must have at least 3× the number of elements as
|
||||||
|
/// keyframe timestamps, in order to account for the tangents.
|
||||||
|
KeyframeNotPresent(usize),
|
||||||
|
|
||||||
|
/// The component to be animated isn't present on the animation target.
|
||||||
|
///
|
||||||
|
/// To fix this error, make sure the entity to be animated contains all
|
||||||
|
/// components that have animation curves.
|
||||||
|
ComponentNotPresent(TypeId),
|
||||||
|
|
||||||
|
/// The component to be animated was present, but the property on the
|
||||||
|
/// component wasn't present.
|
||||||
|
PropertyNotPresent(TypeId),
|
||||||
|
}
|
||||||
|
|
||||||
/// An animation that an [`AnimationPlayer`] is currently either playing or was
|
/// An animation that an [`AnimationPlayer`] is currently either playing or was
|
||||||
/// playing, but is presently paused.
|
/// playing, but is presently paused.
|
||||||
///
|
///
|
||||||
|
@ -564,16 +851,6 @@ impl Clone for AnimationPlayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The components that we might need to read or write during animation of each
|
|
||||||
/// animation target.
|
|
||||||
struct AnimationTargetContext<'a> {
|
|
||||||
entity: Entity,
|
|
||||||
target: &'a AnimationTarget,
|
|
||||||
name: Option<&'a Name>,
|
|
||||||
transform: Option<Mut<'a, Transform>>,
|
|
||||||
morph_weights: Option<Mut<'a, MorphWeights>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Information needed during the traversal of the animation graph in
|
/// Information needed during the traversal of the animation graph in
|
||||||
/// [`advance_animations`].
|
/// [`advance_animations`].
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -597,15 +874,6 @@ struct EvaluatedAnimationGraphNode {
|
||||||
mask: AnimationMask,
|
mask: AnimationMask,
|
||||||
}
|
}
|
||||||
|
|
||||||
thread_local! {
|
|
||||||
/// A cached per-thread copy of the graph evaluator.
|
|
||||||
///
|
|
||||||
/// Caching the evaluator lets us save allocation traffic from frame to
|
|
||||||
/// frame.
|
|
||||||
static ANIMATION_GRAPH_EVALUATOR: RefCell<AnimationGraphEvaluator> =
|
|
||||||
RefCell::new(AnimationGraphEvaluator::default());
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AnimationPlayer {
|
impl AnimationPlayer {
|
||||||
/// Start playing an animation, restarting it if necessary.
|
/// Start playing an animation, restarting it if necessary.
|
||||||
pub fn start(&mut self, animation: AnimationNodeIndex) -> &mut ActiveAnimation {
|
pub fn start(&mut self, animation: AnimationNodeIndex) -> &mut ActiveAnimation {
|
||||||
|
@ -826,55 +1094,51 @@ pub fn advance_animations(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A system that modifies animation targets (e.g. bones in a skinned mesh)
|
/// A system that modifies animation targets (e.g. bones in a skinned mesh)
|
||||||
/// according to the currently-playing animation.
|
/// according to the currently-playing animations.
|
||||||
pub fn animate_targets(
|
pub fn animate_targets(
|
||||||
clips: Res<Assets<AnimationClip>>,
|
clips: Res<Assets<AnimationClip>>,
|
||||||
graphs: Res<Assets<AnimationGraph>>,
|
graphs: Res<Assets<AnimationGraph>>,
|
||||||
players: Query<(&AnimationPlayer, &Handle<AnimationGraph>)>,
|
players: Query<(&AnimationPlayer, &Handle<AnimationGraph>)>,
|
||||||
mut targets: Query<(
|
mut targets: Query<(
|
||||||
Entity,
|
Option<&mut Transform>,
|
||||||
&AnimationTarget,
|
EntityMutExcept<(Transform, AnimationPlayer, Handle<AnimationGraph>)>,
|
||||||
Option<&Name>,
|
|
||||||
AnyOf<(&mut Transform, &mut MorphWeights)>,
|
|
||||||
)>,
|
)>,
|
||||||
) {
|
) {
|
||||||
// We use two queries here: one read-only query for animation players and
|
// Evaluate all animation targets in parallel.
|
||||||
// one read-write query for animation targets (e.g. bones). The
|
|
||||||
// `AnimationPlayer` query is read-only shared memory accessible from all
|
|
||||||
// animation targets, which are evaluated in parallel.
|
|
||||||
|
|
||||||
// Iterate over all animation targets in parallel.
|
|
||||||
targets
|
targets
|
||||||
.par_iter_mut()
|
.par_iter_mut()
|
||||||
.for_each(|(id, target, name, (transform, morph_weights))| {
|
.for_each(|(mut transform, mut entity_mut)| {
|
||||||
let Ok((animation_player, animation_graph_handle)) = players.get(target.player) else {
|
let Some(&AnimationTarget {
|
||||||
|
id: target_id,
|
||||||
|
player: player_id,
|
||||||
|
}) = entity_mut.get::<AnimationTarget>()
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let (animation_player, animation_graph_id) =
|
||||||
|
if let Ok((player, graph_handle)) = players.get(player_id) {
|
||||||
|
(player, graph_handle.id())
|
||||||
|
} else {
|
||||||
trace!(
|
trace!(
|
||||||
"Either an animation player {:?} or a graph was missing for the target \
|
"Either an animation player {:?} or a graph was missing for the target \
|
||||||
entity {:?} ({:?}); no animations will play this frame",
|
entity {:?} ({:?}); no animations will play this frame",
|
||||||
target.player,
|
player_id,
|
||||||
id,
|
entity_mut.id(),
|
||||||
name,
|
entity_mut.get::<Name>(),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The graph might not have loaded yet. Safely bail.
|
// The graph might not have loaded yet. Safely bail.
|
||||||
let Some(animation_graph) = graphs.get(animation_graph_handle) else {
|
let Some(animation_graph) = graphs.get(animation_graph_id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut target_context = AnimationTargetContext {
|
|
||||||
entity: id,
|
|
||||||
target,
|
|
||||||
name,
|
|
||||||
transform,
|
|
||||||
morph_weights,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Determine which mask groups this animation target belongs to.
|
// Determine which mask groups this animation target belongs to.
|
||||||
let target_mask = animation_graph
|
let target_mask = animation_graph
|
||||||
.mask_groups
|
.mask_groups
|
||||||
.get(&target.id)
|
.get(&target_id)
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
@ -914,26 +1178,27 @@ pub fn animate_targets(
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(curves) = clip.curves_for_target(target_context.target.id) else {
|
let Some(curves) = clip.curves_for_target(target_id) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let weight = active_animation.computed_weight;
|
let weight = active_animation.computed_weight;
|
||||||
total_weight += weight;
|
total_weight += weight;
|
||||||
|
|
||||||
target_context.apply(curves, weight / total_weight, active_animation.seek_time);
|
let weight = weight / total_weight;
|
||||||
}
|
let seek_time = active_animation.seek_time;
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AnimationTargetContext<'_> {
|
|
||||||
/// Applies a clip to a single animation target according to the
|
|
||||||
/// [`AnimationTargetContext`].
|
|
||||||
fn apply(&mut self, curves: &[VariableCurve], weight: f32, seek_time: f32) {
|
|
||||||
for curve in curves {
|
for curve in curves {
|
||||||
// Some curves have only one keyframe used to set a transform
|
// Some curves have only one keyframe used to set a transform
|
||||||
if curve.keyframe_timestamps.len() == 1 {
|
if curve.keyframe_timestamps.len() == 1 {
|
||||||
self.apply_single_keyframe(curve, weight);
|
if let Err(err) = curve.keyframes.apply_single_keyframe(
|
||||||
|
transform.as_mut().map(|transform| transform.reborrow()),
|
||||||
|
entity_mut.reborrow(),
|
||||||
|
weight,
|
||||||
|
) {
|
||||||
|
warn!("Animation application failed: {:?}", err);
|
||||||
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -943,278 +1208,23 @@ impl AnimationTargetContext<'_> {
|
||||||
let timestamp_start = curve.keyframe_timestamps[step_start];
|
let timestamp_start = curve.keyframe_timestamps[step_start];
|
||||||
let timestamp_end = curve.keyframe_timestamps[step_start + 1];
|
let timestamp_end = curve.keyframe_timestamps[step_start + 1];
|
||||||
// Compute how far we are through the keyframe, normalized to [0, 1]
|
// Compute how far we are through the keyframe, normalized to [0, 1]
|
||||||
let lerp = f32::inverse_lerp(timestamp_start, timestamp_end, seek_time).clamp(0.0, 1.0);
|
let lerp = f32::inverse_lerp(timestamp_start, timestamp_end, seek_time)
|
||||||
|
.clamp(0.0, 1.0);
|
||||||
|
|
||||||
self.apply_tweened_keyframe(
|
if let Err(err) = curve.keyframes.apply_tweened_keyframes(
|
||||||
curve,
|
transform.as_mut().map(|transform| transform.reborrow()),
|
||||||
|
entity_mut.reborrow(),
|
||||||
|
curve.interpolation,
|
||||||
step_start,
|
step_start,
|
||||||
lerp,
|
lerp,
|
||||||
weight,
|
weight,
|
||||||
timestamp_end - timestamp_start,
|
timestamp_end - timestamp_start,
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_single_keyframe(&mut self, curve: &VariableCurve, weight: f32) {
|
|
||||||
match &curve.keyframes {
|
|
||||||
Keyframes::Rotation(keyframes) => {
|
|
||||||
if let Some(ref mut transform) = self.transform {
|
|
||||||
transform.rotation = transform.rotation.slerp(keyframes[0], weight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Keyframes::Translation(keyframes) => {
|
|
||||||
if let Some(ref mut transform) = self.transform {
|
|
||||||
transform.translation = transform.translation.lerp(keyframes[0], weight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Keyframes::Scale(keyframes) => {
|
|
||||||
if let Some(ref mut transform) = self.transform {
|
|
||||||
transform.scale = transform.scale.lerp(keyframes[0], weight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Keyframes::Weights(keyframes) => {
|
|
||||||
let Some(ref mut morphs) = self.morph_weights else {
|
|
||||||
error!(
|
|
||||||
"Tried to animate morphs on {:?} ({:?}), but no `MorphWeights` was found",
|
|
||||||
self.entity, self.name,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let target_count = morphs.weights().len();
|
|
||||||
lerp_morph_weights(
|
|
||||||
morphs.weights_mut(),
|
|
||||||
get_keyframe(target_count, keyframes, 0).iter().copied(),
|
|
||||||
weight,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_tweened_keyframe(
|
|
||||||
&mut self,
|
|
||||||
curve: &VariableCurve,
|
|
||||||
step_start: usize,
|
|
||||||
lerp: f32,
|
|
||||||
weight: f32,
|
|
||||||
duration: f32,
|
|
||||||
) {
|
) {
|
||||||
match (&curve.interpolation, &curve.keyframes) {
|
warn!("Animation application failed: {:?}", err);
|
||||||
(Interpolation::Step, Keyframes::Rotation(keyframes)) => {
|
|
||||||
if let Some(ref mut transform) = self.transform {
|
|
||||||
transform.rotation = transform.rotation.slerp(keyframes[step_start], weight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(Interpolation::Linear, Keyframes::Rotation(keyframes)) => {
|
|
||||||
let Some(ref mut transform) = self.transform else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let rot_start = keyframes[step_start];
|
|
||||||
let rot_end = keyframes[step_start + 1];
|
|
||||||
|
|
||||||
// Rotations are using a spherical linear interpolation
|
|
||||||
let rot = rot_start.slerp(rot_end, lerp);
|
|
||||||
transform.rotation = transform.rotation.slerp(rot, weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
(Interpolation::CubicSpline, Keyframes::Rotation(keyframes)) => {
|
|
||||||
let Some(ref mut transform) = self.transform else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let value_start = keyframes[step_start * 3 + 1];
|
|
||||||
let tangent_out_start = keyframes[step_start * 3 + 2];
|
|
||||||
let tangent_in_end = keyframes[(step_start + 1) * 3];
|
|
||||||
let value_end = keyframes[(step_start + 1) * 3 + 1];
|
|
||||||
let result = cubic_spline_interpolation(
|
|
||||||
value_start,
|
|
||||||
tangent_out_start,
|
|
||||||
tangent_in_end,
|
|
||||||
value_end,
|
|
||||||
lerp,
|
|
||||||
duration,
|
|
||||||
);
|
|
||||||
transform.rotation = transform.rotation.slerp(result.normalize(), weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
(Interpolation::Step, Keyframes::Translation(keyframes)) => {
|
|
||||||
if let Some(ref mut transform) = self.transform {
|
|
||||||
transform.translation =
|
|
||||||
transform.translation.lerp(keyframes[step_start], weight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(Interpolation::Linear, Keyframes::Translation(keyframes)) => {
|
|
||||||
let Some(ref mut transform) = self.transform else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let translation_start = keyframes[step_start];
|
|
||||||
let translation_end = keyframes[step_start + 1];
|
|
||||||
let result = translation_start.lerp(translation_end, lerp);
|
|
||||||
transform.translation = transform.translation.lerp(result, weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
(Interpolation::CubicSpline, Keyframes::Translation(keyframes)) => {
|
|
||||||
let Some(ref mut transform) = self.transform else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let value_start = keyframes[step_start * 3 + 1];
|
|
||||||
let tangent_out_start = keyframes[step_start * 3 + 2];
|
|
||||||
let tangent_in_end = keyframes[(step_start + 1) * 3];
|
|
||||||
let value_end = keyframes[(step_start + 1) * 3 + 1];
|
|
||||||
let result = cubic_spline_interpolation(
|
|
||||||
value_start,
|
|
||||||
tangent_out_start,
|
|
||||||
tangent_in_end,
|
|
||||||
value_end,
|
|
||||||
lerp,
|
|
||||||
duration,
|
|
||||||
);
|
|
||||||
transform.translation = transform.translation.lerp(result, weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
(Interpolation::Step, Keyframes::Scale(keyframes)) => {
|
|
||||||
if let Some(ref mut transform) = self.transform {
|
|
||||||
transform.scale = transform.scale.lerp(keyframes[step_start], weight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(Interpolation::Linear, Keyframes::Scale(keyframes)) => {
|
|
||||||
let Some(ref mut transform) = self.transform else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let scale_start = keyframes[step_start];
|
|
||||||
let scale_end = keyframes[step_start + 1];
|
|
||||||
let result = scale_start.lerp(scale_end, lerp);
|
|
||||||
transform.scale = transform.scale.lerp(result, weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
(Interpolation::CubicSpline, Keyframes::Scale(keyframes)) => {
|
|
||||||
let Some(ref mut transform) = self.transform else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let value_start = keyframes[step_start * 3 + 1];
|
|
||||||
let tangent_out_start = keyframes[step_start * 3 + 2];
|
|
||||||
let tangent_in_end = keyframes[(step_start + 1) * 3];
|
|
||||||
let value_end = keyframes[(step_start + 1) * 3 + 1];
|
|
||||||
let result = cubic_spline_interpolation(
|
|
||||||
value_start,
|
|
||||||
tangent_out_start,
|
|
||||||
tangent_in_end,
|
|
||||||
value_end,
|
|
||||||
lerp,
|
|
||||||
duration,
|
|
||||||
);
|
|
||||||
transform.scale = transform.scale.lerp(result, weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
(Interpolation::Step, Keyframes::Weights(keyframes)) => {
|
|
||||||
let Some(ref mut morphs) = self.morph_weights else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let target_count = morphs.weights().len();
|
|
||||||
let morph_start = get_keyframe(target_count, keyframes, step_start);
|
|
||||||
lerp_morph_weights(morphs.weights_mut(), morph_start.iter().copied(), weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
(Interpolation::Linear, Keyframes::Weights(keyframes)) => {
|
|
||||||
let Some(ref mut morphs) = self.morph_weights else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let target_count = morphs.weights().len();
|
|
||||||
let morph_start = get_keyframe(target_count, keyframes, step_start);
|
|
||||||
let morph_end = get_keyframe(target_count, keyframes, step_start + 1);
|
|
||||||
let result = morph_start
|
|
||||||
.iter()
|
|
||||||
.zip(morph_end)
|
|
||||||
.map(|(a, b)| a.lerp(*b, lerp));
|
|
||||||
lerp_morph_weights(morphs.weights_mut(), result, weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
(Interpolation::CubicSpline, Keyframes::Weights(keyframes)) => {
|
|
||||||
let Some(ref mut morphs) = self.morph_weights else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let target_count = morphs.weights().len();
|
|
||||||
let morph_start = get_keyframe(target_count, keyframes, step_start * 3 + 1);
|
|
||||||
let tangents_out_start = get_keyframe(target_count, keyframes, step_start * 3 + 2);
|
|
||||||
let tangents_in_end = get_keyframe(target_count, keyframes, (step_start + 1) * 3);
|
|
||||||
let morph_end = get_keyframe(target_count, keyframes, (step_start + 1) * 3 + 1);
|
|
||||||
let result = morph_start
|
|
||||||
.iter()
|
|
||||||
.zip(tangents_out_start)
|
|
||||||
.zip(tangents_in_end)
|
|
||||||
.zip(morph_end)
|
|
||||||
.map(
|
|
||||||
|(((&value_start, &tangent_out_start), &tangent_in_end), &value_end)| {
|
|
||||||
cubic_spline_interpolation(
|
|
||||||
value_start,
|
|
||||||
tangent_out_start,
|
|
||||||
tangent_in_end,
|
|
||||||
value_end,
|
|
||||||
lerp,
|
|
||||||
duration,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
);
|
|
||||||
lerp_morph_weights(morphs.weights_mut(), result, weight);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
/// Update `weights` based on weights in `keyframe` with a linear interpolation
|
|
||||||
/// on `key_lerp`.
|
|
||||||
fn lerp_morph_weights(weights: &mut [f32], keyframe: impl Iterator<Item = f32>, key_lerp: f32) {
|
|
||||||
let zipped = weights.iter_mut().zip(keyframe);
|
|
||||||
for (morph_weight, keyframe) in zipped {
|
|
||||||
*morph_weight = morph_weight.lerp(keyframe, key_lerp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract a keyframe from a list of keyframes by index.
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
///
|
|
||||||
/// When `key_index * target_count` is larger than `keyframes`
|
|
||||||
///
|
|
||||||
/// This happens when `keyframes` is not formatted as described in
|
|
||||||
/// [`Keyframes::Weights`]. A possible cause is [`AnimationClip`] not being
|
|
||||||
/// meant to be used for the [`MorphWeights`] of the entity it's being applied to.
|
|
||||||
fn get_keyframe(target_count: usize, keyframes: &[f32], key_index: usize) -> &[f32] {
|
|
||||||
let start = target_count * key_index;
|
|
||||||
let end = target_count * (key_index + 1);
|
|
||||||
&keyframes[start..end]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper function for cubic spline interpolation.
|
|
||||||
fn cubic_spline_interpolation<T>(
|
|
||||||
value_start: T,
|
|
||||||
tangent_out_start: T,
|
|
||||||
tangent_in_end: T,
|
|
||||||
value_end: T,
|
|
||||||
lerp: f32,
|
|
||||||
step_duration: f32,
|
|
||||||
) -> T
|
|
||||||
where
|
|
||||||
T: Mul<f32, Output = T> + Add<Output = T>,
|
|
||||||
{
|
|
||||||
value_start * (2.0 * lerp.cubed() - 3.0 * lerp.squared() + 1.0)
|
|
||||||
+ tangent_out_start * (step_duration) * (lerp.cubed() - 2.0 * lerp.squared() + lerp)
|
|
||||||
+ value_end * (-2.0 * lerp.cubed() + 3.0 * lerp.squared())
|
|
||||||
+ tangent_in_end * step_duration * (lerp.cubed() - lerp.squared())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds animation support to an app
|
/// Adds animation support to an app
|
||||||
|
@ -1237,11 +1247,20 @@ impl Plugin for AnimationPlugin {
|
||||||
(
|
(
|
||||||
advance_transitions,
|
advance_transitions,
|
||||||
advance_animations,
|
advance_animations,
|
||||||
animate_targets.after(bevy_render::mesh::morph::inherit_weights),
|
// TODO: `animate_targets` can animate anything, so
|
||||||
|
// ambiguity testing currently considers it ambiguous with
|
||||||
|
// every other system in `PostUpdate`. We may want to move
|
||||||
|
// it to its own system set after `Update` but before
|
||||||
|
// `PostUpdate`. For now, we just disable ambiguity testing
|
||||||
|
// for this system.
|
||||||
|
animate_targets
|
||||||
|
.after(bevy_render::mesh::morph::inherit_weights)
|
||||||
|
.ambiguous_with_all(),
|
||||||
expire_completed_transitions,
|
expire_completed_transitions,
|
||||||
)
|
)
|
||||||
.chain()
|
.chain()
|
||||||
.before(TransformSystem::TransformPropagate),
|
.before(TransformSystem::TransformPropagate)
|
||||||
|
.before(UiSystem::Prepare),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1296,10 +1315,11 @@ impl AnimationGraphEvaluator {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::VariableCurve;
|
use crate::{prelude::TranslationKeyframes, VariableCurve};
|
||||||
use bevy_math::Vec3;
|
use bevy_math::Vec3;
|
||||||
|
|
||||||
fn test_variable_curve() -> VariableCurve {
|
// Returns the curve and the keyframe count.
|
||||||
|
fn test_variable_curve() -> (VariableCurve, usize) {
|
||||||
let keyframe_timestamps = vec![1.0, 2.0, 3.0, 4.0];
|
let keyframe_timestamps = vec![1.0, 2.0, 3.0, 4.0];
|
||||||
let keyframes = vec![
|
let keyframes = vec![
|
||||||
Vec3::ONE * 0.0,
|
Vec3::ONE * 0.0,
|
||||||
|
@ -1309,13 +1329,14 @@ mod tests {
|
||||||
];
|
];
|
||||||
let interpolation = crate::Interpolation::Linear;
|
let interpolation = crate::Interpolation::Linear;
|
||||||
|
|
||||||
let variable_curve = VariableCurve {
|
assert_eq!(keyframe_timestamps.len(), keyframes.len());
|
||||||
keyframe_timestamps,
|
let keyframe_count = keyframes.len();
|
||||||
keyframes: crate::Keyframes::Translation(keyframes),
|
|
||||||
interpolation,
|
|
||||||
};
|
|
||||||
|
|
||||||
assert!(variable_curve.keyframe_timestamps.len() == variable_curve.keyframes.len());
|
let variable_curve = VariableCurve::new::<TranslationKeyframes>(
|
||||||
|
keyframe_timestamps,
|
||||||
|
keyframes,
|
||||||
|
interpolation,
|
||||||
|
);
|
||||||
|
|
||||||
// f32 doesn't impl Ord so we can't easily sort it
|
// f32 doesn't impl Ord so we can't easily sort it
|
||||||
let mut maybe_last_timestamp = None;
|
let mut maybe_last_timestamp = None;
|
||||||
|
@ -1328,12 +1349,12 @@ mod tests {
|
||||||
maybe_last_timestamp = Some(current_timestamp);
|
maybe_last_timestamp = Some(current_timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
variable_curve
|
(variable_curve, keyframe_count)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn find_current_keyframe_is_in_bounds() {
|
fn find_current_keyframe_is_in_bounds() {
|
||||||
let curve = test_variable_curve();
|
let curve = test_variable_curve().0;
|
||||||
let min_time = *curve.keyframe_timestamps.first().unwrap();
|
let min_time = *curve.keyframe_timestamps.first().unwrap();
|
||||||
// We will always get none at times at or past the second last keyframe
|
// We will always get none at times at or past the second last keyframe
|
||||||
let second_last_keyframe = curve.keyframe_timestamps.len() - 2;
|
let second_last_keyframe = curve.keyframe_timestamps.len() - 2;
|
||||||
|
@ -1364,7 +1385,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn find_current_keyframe_returns_none_on_unstarted_animations() {
|
fn find_current_keyframe_returns_none_on_unstarted_animations() {
|
||||||
let curve = test_variable_curve();
|
let curve = test_variable_curve().0;
|
||||||
let min_time = *curve.keyframe_timestamps.first().unwrap();
|
let min_time = *curve.keyframe_timestamps.first().unwrap();
|
||||||
let seek_time = 0.0;
|
let seek_time = 0.0;
|
||||||
assert!(seek_time < min_time);
|
assert!(seek_time < min_time);
|
||||||
|
@ -1378,7 +1399,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn find_current_keyframe_returns_none_on_finished_animation() {
|
fn find_current_keyframe_returns_none_on_finished_animation() {
|
||||||
let curve = test_variable_curve();
|
let curve = test_variable_curve().0;
|
||||||
let max_time = *curve.keyframe_timestamps.last().unwrap();
|
let max_time = *curve.keyframe_timestamps.last().unwrap();
|
||||||
|
|
||||||
assert!(max_time < f32::INFINITY);
|
assert!(max_time < f32::INFINITY);
|
||||||
|
@ -1391,7 +1412,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn second_last_keyframe_is_found_correctly() {
|
fn second_last_keyframe_is_found_correctly() {
|
||||||
let curve = test_variable_curve();
|
let curve = test_variable_curve().0;
|
||||||
|
|
||||||
// Exact time match
|
// Exact time match
|
||||||
let second_last_keyframe = curve.keyframe_timestamps.len() - 2;
|
let second_last_keyframe = curve.keyframe_timestamps.len() - 2;
|
||||||
|
@ -1410,8 +1431,8 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn exact_keyframe_matches_are_found_correctly() {
|
fn exact_keyframe_matches_are_found_correctly() {
|
||||||
let curve = test_variable_curve();
|
let (curve, keyframe_count) = test_variable_curve();
|
||||||
let second_last_keyframe = curve.keyframes.len() - 2;
|
let second_last_keyframe = keyframe_count - 2;
|
||||||
|
|
||||||
for i in 0..=second_last_keyframe {
|
for i in 0..=second_last_keyframe {
|
||||||
let seek_time = curve.keyframe_timestamps[i];
|
let seek_time = curve.keyframe_timestamps[i];
|
||||||
|
@ -1423,9 +1444,8 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn exact_and_inexact_keyframes_correspond() {
|
fn exact_and_inexact_keyframes_correspond() {
|
||||||
let curve = test_variable_curve();
|
let (curve, keyframe_count) = test_variable_curve();
|
||||||
|
let second_last_keyframe = keyframe_count - 2;
|
||||||
let second_last_keyframe = curve.keyframes.len() - 2;
|
|
||||||
|
|
||||||
for i in 0..=second_last_keyframe {
|
for i in 0..=second_last_keyframe {
|
||||||
let seek_time = curve.keyframe_timestamps[i];
|
let seek_time = curve.keyframe_timestamps[i];
|
||||||
|
|
|
@ -1905,6 +1905,7 @@ impl<'w> FilteredEntityRef<'w> {
|
||||||
/// - If `access` takes read access to a component no mutable reference to that
|
/// - If `access` takes read access to a component no mutable reference to that
|
||||||
/// component can exist at the same time as the returned [`FilteredEntityMut`]
|
/// component can exist at the same time as the returned [`FilteredEntityMut`]
|
||||||
/// - If `access` takes any access for a component `entity` must have that component.
|
/// - If `access` takes any access for a component `entity` must have that component.
|
||||||
|
#[inline]
|
||||||
pub(crate) unsafe fn new(entity: UnsafeEntityCell<'w>, access: Access<ComponentId>) -> Self {
|
pub(crate) unsafe fn new(entity: UnsafeEntityCell<'w>, access: Access<ComponentId>) -> Self {
|
||||||
Self { entity, access }
|
Self { entity, access }
|
||||||
}
|
}
|
||||||
|
@ -2043,6 +2044,7 @@ impl<'w> FilteredEntityRef<'w> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'w> From<FilteredEntityMut<'w>> for FilteredEntityRef<'w> {
|
impl<'w> From<FilteredEntityMut<'w>> for FilteredEntityRef<'w> {
|
||||||
|
#[inline]
|
||||||
fn from(entity_mut: FilteredEntityMut<'w>) -> Self {
|
fn from(entity_mut: FilteredEntityMut<'w>) -> Self {
|
||||||
// SAFETY:
|
// SAFETY:
|
||||||
// - `FilteredEntityMut` guarantees exclusive access to all components in the new `FilteredEntityRef`.
|
// - `FilteredEntityMut` guarantees exclusive access to all components in the new `FilteredEntityRef`.
|
||||||
|
@ -2051,6 +2053,7 @@ impl<'w> From<FilteredEntityMut<'w>> for FilteredEntityRef<'w> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<&'a FilteredEntityMut<'_>> for FilteredEntityRef<'a> {
|
impl<'a> From<&'a FilteredEntityMut<'_>> for FilteredEntityRef<'a> {
|
||||||
|
#[inline]
|
||||||
fn from(entity_mut: &'a FilteredEntityMut<'_>) -> Self {
|
fn from(entity_mut: &'a FilteredEntityMut<'_>) -> Self {
|
||||||
// SAFETY:
|
// SAFETY:
|
||||||
// - `FilteredEntityMut` guarantees exclusive access to all components in the new `FilteredEntityRef`.
|
// - `FilteredEntityMut` guarantees exclusive access to all components in the new `FilteredEntityRef`.
|
||||||
|
@ -2144,6 +2147,7 @@ impl<'w> FilteredEntityMut<'w> {
|
||||||
/// - If `access` takes write access to a component, no reference to that component
|
/// - If `access` takes write access to a component, no reference to that component
|
||||||
/// may exist at the same time as the returned [`FilteredEntityMut`]
|
/// may exist at the same time as the returned [`FilteredEntityMut`]
|
||||||
/// - If `access` takes any access for a component `entity` must have that component.
|
/// - If `access` takes any access for a component `entity` must have that component.
|
||||||
|
#[inline]
|
||||||
pub(crate) unsafe fn new(entity: UnsafeEntityCell<'w>, access: Access<ComponentId>) -> Self {
|
pub(crate) unsafe fn new(entity: UnsafeEntityCell<'w>, access: Access<ComponentId>) -> Self {
|
||||||
Self { entity, access }
|
Self { entity, access }
|
||||||
}
|
}
|
||||||
|
@ -2156,6 +2160,7 @@ impl<'w> FilteredEntityMut<'w> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets read-only access to all of the entity's components.
|
/// Gets read-only access to all of the entity's components.
|
||||||
|
#[inline]
|
||||||
pub fn as_readonly(&self) -> FilteredEntityRef<'_> {
|
pub fn as_readonly(&self) -> FilteredEntityRef<'_> {
|
||||||
FilteredEntityRef::from(self)
|
FilteredEntityRef::from(self)
|
||||||
}
|
}
|
||||||
|
@ -2397,6 +2402,13 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the [ID](Entity) of the current entity.
|
||||||
|
#[inline]
|
||||||
|
#[must_use = "Omit the .id() call if you do not need to store the `Entity` identifier."]
|
||||||
|
pub fn id(&self) -> Entity {
|
||||||
|
self.entity.id()
|
||||||
|
}
|
||||||
|
|
||||||
/// Gets access to the component of type `C` for the current entity. Returns
|
/// Gets access to the component of type `C` for the current entity. Returns
|
||||||
/// `None` if the component doesn't have a component of that type or if the
|
/// `None` if the component doesn't have a component of that type or if the
|
||||||
/// type is one of the excluded components.
|
/// type is one of the excluded components.
|
||||||
|
@ -2478,6 +2490,13 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the [ID](Entity) of the current entity.
|
||||||
|
#[inline]
|
||||||
|
#[must_use = "Omit the .id() call if you do not need to store the `Entity` identifier."]
|
||||||
|
pub fn id(&self) -> Entity {
|
||||||
|
self.entity.id()
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns a new instance with a shorter lifetime.
|
/// Returns a new instance with a shorter lifetime.
|
||||||
///
|
///
|
||||||
/// This is useful if you have `&mut EntityMutExcept`, but you need
|
/// This is useful if you have `&mut EntityMutExcept`, but you need
|
||||||
|
|
|
@ -3,6 +3,9 @@ use crate::{
|
||||||
GltfMeshExtras, GltfNode, GltfSceneExtras, GltfSkin,
|
GltfMeshExtras, GltfNode, GltfSceneExtras, GltfSkin,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use bevy_animation::prelude::{
|
||||||
|
Keyframes, MorphWeightsKeyframes, RotationKeyframes, ScaleKeyframes, TranslationKeyframes,
|
||||||
|
};
|
||||||
#[cfg(feature = "bevy_animation")]
|
#[cfg(feature = "bevy_animation")]
|
||||||
use bevy_animation::{AnimationTarget, AnimationTargetId};
|
use bevy_animation::{AnimationTarget, AnimationTargetId};
|
||||||
use bevy_asset::{
|
use bevy_asset::{
|
||||||
|
@ -263,7 +266,7 @@ async fn load_gltf<'a, 'b, 'c>(
|
||||||
|
|
||||||
#[cfg(feature = "bevy_animation")]
|
#[cfg(feature = "bevy_animation")]
|
||||||
let (animations, named_animations, animation_roots) = {
|
let (animations, named_animations, animation_roots) = {
|
||||||
use bevy_animation::{Interpolation, Keyframes};
|
use bevy_animation::Interpolation;
|
||||||
use gltf::animation::util::ReadOutputs;
|
use gltf::animation::util::ReadOutputs;
|
||||||
let mut animations = vec![];
|
let mut animations = vec![];
|
||||||
let mut named_animations = HashMap::default();
|
let mut named_animations = HashMap::default();
|
||||||
|
@ -294,16 +297,23 @@ async fn load_gltf<'a, 'b, 'c>(
|
||||||
let keyframes = if let Some(outputs) = reader.read_outputs() {
|
let keyframes = if let Some(outputs) = reader.read_outputs() {
|
||||||
match outputs {
|
match outputs {
|
||||||
ReadOutputs::Translations(tr) => {
|
ReadOutputs::Translations(tr) => {
|
||||||
Keyframes::Translation(tr.map(Vec3::from).collect())
|
Box::new(TranslationKeyframes(tr.map(Vec3::from).collect()))
|
||||||
|
as Box<dyn Keyframes>
|
||||||
}
|
}
|
||||||
ReadOutputs::Rotations(rots) => Keyframes::Rotation(
|
ReadOutputs::Rotations(rots) => Box::new(RotationKeyframes(
|
||||||
rots.into_f32().map(bevy_math::Quat::from_array).collect(),
|
rots.into_f32().map(bevy_math::Quat::from_array).collect(),
|
||||||
),
|
))
|
||||||
|
as Box<dyn Keyframes>,
|
||||||
ReadOutputs::Scales(scale) => {
|
ReadOutputs::Scales(scale) => {
|
||||||
Keyframes::Scale(scale.map(Vec3::from).collect())
|
Box::new(ScaleKeyframes(scale.map(Vec3::from).collect()))
|
||||||
|
as Box<dyn Keyframes>
|
||||||
}
|
}
|
||||||
ReadOutputs::MorphTargetWeights(weights) => {
|
ReadOutputs::MorphTargetWeights(weights) => {
|
||||||
Keyframes::Weights(weights.into_f32().collect())
|
let weights: Vec<_> = weights.into_f32().collect();
|
||||||
|
Box::new(MorphWeightsKeyframes {
|
||||||
|
morph_target_count: weights.len() / keyframe_timestamps.len(),
|
||||||
|
weights,
|
||||||
|
}) as Box<dyn Keyframes>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -144,6 +144,7 @@ pub(crate) fn impl_typed(
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
impl #impl_generics #bevy_reflect_path::Typed for #type_path #ty_generics #where_reflect_clause {
|
impl #impl_generics #bevy_reflect_path::Typed for #type_path #ty_generics #where_reflect_clause {
|
||||||
|
#[inline]
|
||||||
fn type_info() -> &'static #bevy_reflect_path::TypeInfo {
|
fn type_info() -> &'static #bevy_reflect_path::TypeInfo {
|
||||||
#type_info_cell
|
#type_info_cell
|
||||||
}
|
}
|
||||||
|
|
|
@ -452,6 +452,7 @@ macro_rules! impl_reflect_for_veclike {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: FromReflect + MaybeTyped + TypePath + GetTypeRegistration> PartialReflect for $ty {
|
impl<T: FromReflect + MaybeTyped + TypePath + GetTypeRegistration> PartialReflect for $ty {
|
||||||
|
#[inline]
|
||||||
fn get_represented_type_info(&self) -> Option<&'static TypeInfo> {
|
fn get_represented_type_info(&self) -> Option<&'static TypeInfo> {
|
||||||
Some(<Self as Typed>::type_info())
|
Some(<Self as Typed>::type_info())
|
||||||
}
|
}
|
||||||
|
@ -460,10 +461,12 @@ macro_rules! impl_reflect_for_veclike {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn as_partial_reflect(&self) -> &dyn PartialReflect {
|
fn as_partial_reflect(&self) -> &dyn PartialReflect {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect {
|
fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
|
@ -229,6 +229,7 @@ impl TypeInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The [`TypeId`] of the underlying type.
|
/// The [`TypeId`] of the underlying type.
|
||||||
|
#[inline]
|
||||||
pub fn type_id(&self) -> TypeId {
|
pub fn type_id(&self) -> TypeId {
|
||||||
self.ty().id()
|
self.ty().id()
|
||||||
}
|
}
|
||||||
|
@ -381,6 +382,7 @@ impl Type {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the [`TypeId`] of the type.
|
/// Returns the [`TypeId`] of the type.
|
||||||
|
#[inline]
|
||||||
pub fn id(&self) -> TypeId {
|
pub fn id(&self) -> TypeId {
|
||||||
self.type_id
|
self.type_id
|
||||||
}
|
}
|
||||||
|
|
|
@ -279,6 +279,7 @@ impl TypeRegistry {
|
||||||
///
|
///
|
||||||
/// If the specified type has not been registered, returns `None`.
|
/// If the specified type has not been registered, returns `None`.
|
||||||
///
|
///
|
||||||
|
#[inline]
|
||||||
pub fn get(&self, type_id: TypeId) -> Option<&TypeRegistration> {
|
pub fn get(&self, type_id: TypeId) -> Option<&TypeRegistration> {
|
||||||
self.registrations.get(&type_id)
|
self.registrations.get(&type_id)
|
||||||
}
|
}
|
||||||
|
|
|
@ -187,6 +187,7 @@ Example | Description
|
||||||
--- | ---
|
--- | ---
|
||||||
[Animated Fox](../examples/animation/animated_fox.rs) | Plays an animation from a skinned glTF
|
[Animated Fox](../examples/animation/animated_fox.rs) | Plays an animation from a skinned glTF
|
||||||
[Animated Transform](../examples/animation/animated_transform.rs) | Create and play an animation defined by code that operates on the `Transform` component
|
[Animated Transform](../examples/animation/animated_transform.rs) | Create and play an animation defined by code that operates on the `Transform` component
|
||||||
|
[Animated UI](../examples/animation/animated_ui.rs) | Shows how to use animation clips to animate UI properties
|
||||||
[Animation Graph](../examples/animation/animation_graph.rs) | Blends multiple animations together with a graph
|
[Animation Graph](../examples/animation/animation_graph.rs) | Blends multiple animations together with a graph
|
||||||
[Animation Masks](../examples/animation/animation_masks.rs) | Demonstrates animation masks
|
[Animation Masks](../examples/animation/animation_masks.rs) | Demonstrates animation masks
|
||||||
[Color animation](../examples/animation/color_animation.rs) | Demonstrates how to animate colors using mixing and splines in different color spaces
|
[Color animation](../examples/animation/color_animation.rs) | Demonstrates how to animate colors using mixing and splines in different color spaces
|
||||||
|
|
|
@ -51,9 +51,9 @@ fn setup(
|
||||||
let planet_animation_target_id = AnimationTargetId::from_name(&planet);
|
let planet_animation_target_id = AnimationTargetId::from_name(&planet);
|
||||||
animation.add_curve_to_target(
|
animation.add_curve_to_target(
|
||||||
planet_animation_target_id,
|
planet_animation_target_id,
|
||||||
VariableCurve {
|
VariableCurve::linear::<TranslationKeyframes>(
|
||||||
keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0, 4.0],
|
[0.0, 1.0, 2.0, 3.0, 4.0],
|
||||||
keyframes: Keyframes::Translation(vec![
|
[
|
||||||
Vec3::new(1.0, 0.0, 1.0),
|
Vec3::new(1.0, 0.0, 1.0),
|
||||||
Vec3::new(-1.0, 0.0, 1.0),
|
Vec3::new(-1.0, 0.0, 1.0),
|
||||||
Vec3::new(-1.0, 0.0, -1.0),
|
Vec3::new(-1.0, 0.0, -1.0),
|
||||||
|
@ -61,9 +61,8 @@ fn setup(
|
||||||
// in case seamless looping is wanted, the last keyframe should
|
// in case seamless looping is wanted, the last keyframe should
|
||||||
// be the same as the first one
|
// be the same as the first one
|
||||||
Vec3::new(1.0, 0.0, 1.0),
|
Vec3::new(1.0, 0.0, 1.0),
|
||||||
]),
|
],
|
||||||
interpolation: Interpolation::Linear,
|
),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
// Or it can modify the rotation of the transform.
|
// Or it can modify the rotation of the transform.
|
||||||
// To find the entity to modify, the hierarchy will be traversed looking for
|
// To find the entity to modify, the hierarchy will be traversed looking for
|
||||||
|
@ -72,17 +71,16 @@ fn setup(
|
||||||
AnimationTargetId::from_names([planet.clone(), orbit_controller.clone()].iter());
|
AnimationTargetId::from_names([planet.clone(), orbit_controller.clone()].iter());
|
||||||
animation.add_curve_to_target(
|
animation.add_curve_to_target(
|
||||||
orbit_controller_animation_target_id,
|
orbit_controller_animation_target_id,
|
||||||
VariableCurve {
|
VariableCurve::linear::<RotationKeyframes>(
|
||||||
keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0, 4.0],
|
[0.0, 1.0, 2.0, 3.0, 4.0],
|
||||||
keyframes: Keyframes::Rotation(vec![
|
[
|
||||||
Quat::IDENTITY,
|
Quat::IDENTITY,
|
||||||
Quat::from_axis_angle(Vec3::Y, PI / 2.),
|
Quat::from_axis_angle(Vec3::Y, PI / 2.),
|
||||||
Quat::from_axis_angle(Vec3::Y, PI / 2. * 2.),
|
Quat::from_axis_angle(Vec3::Y, PI / 2. * 2.),
|
||||||
Quat::from_axis_angle(Vec3::Y, PI / 2. * 3.),
|
Quat::from_axis_angle(Vec3::Y, PI / 2. * 3.),
|
||||||
Quat::IDENTITY,
|
Quat::IDENTITY,
|
||||||
]),
|
],
|
||||||
interpolation: Interpolation::Linear,
|
),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
// If a curve in an animation is shorter than the other, it will not repeat
|
// If a curve in an animation is shorter than the other, it will not repeat
|
||||||
// until all other curves are finished. In that case, another animation should
|
// until all other curves are finished. In that case, another animation should
|
||||||
|
@ -92,9 +90,9 @@ fn setup(
|
||||||
);
|
);
|
||||||
animation.add_curve_to_target(
|
animation.add_curve_to_target(
|
||||||
satellite_animation_target_id,
|
satellite_animation_target_id,
|
||||||
VariableCurve {
|
VariableCurve::linear::<ScaleKeyframes>(
|
||||||
keyframe_timestamps: vec![0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0],
|
[0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0],
|
||||||
keyframes: Keyframes::Scale(vec![
|
[
|
||||||
Vec3::splat(0.8),
|
Vec3::splat(0.8),
|
||||||
Vec3::splat(1.2),
|
Vec3::splat(1.2),
|
||||||
Vec3::splat(0.8),
|
Vec3::splat(0.8),
|
||||||
|
@ -104,26 +102,24 @@ fn setup(
|
||||||
Vec3::splat(0.8),
|
Vec3::splat(0.8),
|
||||||
Vec3::splat(1.2),
|
Vec3::splat(1.2),
|
||||||
Vec3::splat(0.8),
|
Vec3::splat(0.8),
|
||||||
]),
|
],
|
||||||
interpolation: Interpolation::Linear,
|
),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
// There can be more than one curve targeting the same entity path
|
// There can be more than one curve targeting the same entity path
|
||||||
animation.add_curve_to_target(
|
animation.add_curve_to_target(
|
||||||
AnimationTargetId::from_names(
|
AnimationTargetId::from_names(
|
||||||
[planet.clone(), orbit_controller.clone(), satellite.clone()].iter(),
|
[planet.clone(), orbit_controller.clone(), satellite.clone()].iter(),
|
||||||
),
|
),
|
||||||
VariableCurve {
|
VariableCurve::linear::<RotationKeyframes>(
|
||||||
keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0, 4.0],
|
[0.0, 1.0, 2.0, 3.0, 4.0],
|
||||||
keyframes: Keyframes::Rotation(vec![
|
[
|
||||||
Quat::IDENTITY,
|
Quat::IDENTITY,
|
||||||
Quat::from_axis_angle(Vec3::Y, PI / 2.),
|
Quat::from_axis_angle(Vec3::Y, PI / 2.),
|
||||||
Quat::from_axis_angle(Vec3::Y, PI / 2. * 2.),
|
Quat::from_axis_angle(Vec3::Y, PI / 2. * 2.),
|
||||||
Quat::from_axis_angle(Vec3::Y, PI / 2. * 3.),
|
Quat::from_axis_angle(Vec3::Y, PI / 2. * 3.),
|
||||||
Quat::IDENTITY,
|
Quat::IDENTITY,
|
||||||
]),
|
],
|
||||||
interpolation: Interpolation::Linear,
|
),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create the animation graph
|
// Create the animation graph
|
||||||
|
|
181
examples/animation/animated_ui.rs
Normal file
181
examples/animation/animated_ui.rs
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
//! Shows how to use animation clips to animate UI properties.
|
||||||
|
|
||||||
|
use bevy::{
|
||||||
|
animation::{AnimationTarget, AnimationTargetId},
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
|
||||||
|
// A type that represents the font size of the first text section.
|
||||||
|
//
|
||||||
|
// We implement `AnimatableProperty` on this.
|
||||||
|
#[derive(Reflect)]
|
||||||
|
struct FontSizeProperty;
|
||||||
|
|
||||||
|
// A type that represents the color of the first text section.
|
||||||
|
//
|
||||||
|
// We implement `AnimatableProperty` on this.
|
||||||
|
#[derive(Reflect)]
|
||||||
|
struct TextColorProperty;
|
||||||
|
|
||||||
|
// 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<AnimationGraph>,
|
||||||
|
// The index of the node within that graph.
|
||||||
|
node_index: AnimationNodeIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The entry point.
|
||||||
|
fn main() {
|
||||||
|
App::new()
|
||||||
|
.add_plugins(DefaultPlugins)
|
||||||
|
// Note that we don't need any systems other than the setup system,
|
||||||
|
// because Bevy automatically updates animations every frame.
|
||||||
|
.add_systems(Startup, setup)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnimatableProperty for FontSizeProperty {
|
||||||
|
type Component = Text;
|
||||||
|
|
||||||
|
type Property = f32;
|
||||||
|
|
||||||
|
fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property> {
|
||||||
|
Some(&mut component.sections.get_mut(0)?.style.font_size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnimatableProperty for TextColorProperty {
|
||||||
|
type Component = Text;
|
||||||
|
|
||||||
|
type Property = Srgba;
|
||||||
|
|
||||||
|
fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property> {
|
||||||
|
match component.sections.get_mut(0)?.style.color {
|
||||||
|
Color::Srgba(ref mut color) => Some(color),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnimationInfo {
|
||||||
|
// Programmatically creates the UI animation.
|
||||||
|
fn create(
|
||||||
|
animation_graphs: &mut Assets<AnimationGraph>,
|
||||||
|
animation_clips: &mut Assets<AnimationClip>,
|
||||||
|
) -> AnimationInfo {
|
||||||
|
// Create an ID that identifies the text node we're going to animate.
|
||||||
|
let animation_target_name = Name::new("Text");
|
||||||
|
let animation_target_id = AnimationTargetId::from_name(&animation_target_name);
|
||||||
|
|
||||||
|
// Allocate an animation clip.
|
||||||
|
let mut animation_clip = AnimationClip::default();
|
||||||
|
|
||||||
|
// Create a curve that animates font size.
|
||||||
|
//
|
||||||
|
// `VariableCurve::linear` is just a convenience constructor; it's also
|
||||||
|
// possible to initialize the structure manually.
|
||||||
|
animation_clip.add_curve_to_target(
|
||||||
|
animation_target_id,
|
||||||
|
VariableCurve::linear::<AnimatablePropertyKeyframes<FontSizeProperty>>(
|
||||||
|
[0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0],
|
||||||
|
[24.0, 80.0, 24.0, 80.0, 24.0, 80.0, 24.0],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a curve that animates font color. Note that this should have
|
||||||
|
// the same time duration as the previous curve.
|
||||||
|
animation_clip.add_curve_to_target(
|
||||||
|
animation_target_id,
|
||||||
|
VariableCurve::linear::<AnimatablePropertyKeyframes<TextColorProperty>>(
|
||||||
|
[0.0, 1.0, 2.0, 3.0],
|
||||||
|
[Srgba::RED, Srgba::GREEN, Srgba::BLUE, Srgba::RED],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save our animation clip as an asset.
|
||||||
|
let animation_clip_handle = animation_clips.add(animation_clip);
|
||||||
|
|
||||||
|
// Create an animation graph with that clip.
|
||||||
|
let (animation_graph, animation_node_index) =
|
||||||
|
AnimationGraph::from_clip(animation_clip_handle);
|
||||||
|
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_index: animation_node_index,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates all the entities in the scene.
|
||||||
|
fn setup(
|
||||||
|
mut commands: Commands,
|
||||||
|
asset_server: Res<AssetServer>,
|
||||||
|
mut animation_graphs: ResMut<Assets<AnimationGraph>>,
|
||||||
|
mut animation_clips: ResMut<Assets<AnimationClip>>,
|
||||||
|
) {
|
||||||
|
// Create the animation.
|
||||||
|
let AnimationInfo {
|
||||||
|
target_name: animation_target_name,
|
||||||
|
target_id: animation_target_id,
|
||||||
|
graph: animation_graph,
|
||||||
|
node_index: animation_node_index,
|
||||||
|
} = AnimationInfo::create(&mut animation_graphs, &mut animation_clips);
|
||||||
|
|
||||||
|
// Build an animation player that automatically plays the UI animation.
|
||||||
|
let mut animation_player = AnimationPlayer::default();
|
||||||
|
animation_player.play(animation_node_index).repeat();
|
||||||
|
|
||||||
|
// Add a camera.
|
||||||
|
commands.spawn(Camera2dBundle::default());
|
||||||
|
|
||||||
|
// Build the UI. We have a parent node that covers the whole screen and
|
||||||
|
// contains the `AnimationPlayer`, as well as a child node that contains the
|
||||||
|
// text to be animated.
|
||||||
|
commands
|
||||||
|
.spawn(NodeBundle {
|
||||||
|
// Cover the whole screen, and center contents.
|
||||||
|
style: Style {
|
||||||
|
position_type: PositionType::Absolute,
|
||||||
|
top: Val::Px(0.0),
|
||||||
|
left: Val::Px(0.0),
|
||||||
|
right: Val::Px(0.0),
|
||||||
|
bottom: Val::Px(0.0),
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
..default()
|
||||||
|
})
|
||||||
|
.insert(animation_player)
|
||||||
|
.insert(animation_graph)
|
||||||
|
.with_children(|builder| {
|
||||||
|
// Build the text node.
|
||||||
|
let player = builder.parent_entity();
|
||||||
|
builder
|
||||||
|
.spawn(
|
||||||
|
TextBundle::from_section(
|
||||||
|
"Bevy",
|
||||||
|
TextStyle {
|
||||||
|
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||||
|
font_size: 24.0,
|
||||||
|
color: Color::Srgba(Srgba::RED),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_text_justify(JustifyText::Center),
|
||||||
|
)
|
||||||
|
// Mark as an animation target.
|
||||||
|
.insert(AnimationTarget {
|
||||||
|
id: animation_target_id,
|
||||||
|
player,
|
||||||
|
})
|
||||||
|
.insert(animation_target_name);
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in a new issue