Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow animation clips to animate arbitrary properties. #15282

Merged
merged 11 commits into from
Sep 23, 2024
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3488,6 +3488,17 @@ description = "Demonstrates percentage-closer soft shadows (PCSS)"
category = "3D Rendering"
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]
inherits = "release"
opt-level = "z"
Expand Down
4 changes: 4 additions & 0 deletions crates/bevy_animation/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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_transform = { path = "../bevy_transform", 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
fixedbitset = "0.5"
Expand Down
163 changes: 157 additions & 6 deletions crates/bevy_animation/src/animatable.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
//! 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_ecs::world::World;
use bevy_math::*;
use bevy_reflect::Reflect;
use bevy_transform::prelude::Transform;
Expand All @@ -28,10 +27,6 @@ pub trait Animatable: Reflect + Sized + Send + Sync + 'static {
///
/// Implementors should return a default value when no inputs are provided here.
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 {
Expand Down Expand Up @@ -192,3 +187,159 @@ impl Animatable for Quat {
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
pcwalton marked this conversation as resolved.
Show resolved Hide resolved

// 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)
}
Loading