Skip to content

Commit

Permalink
Animatable trait for interpolation and blending (#4482)
Browse files Browse the repository at this point in the history
# Objective
Allow animation of types other than translation, scale, and rotation on
`Transforms`.

## Solution
Add a base trait for all values that can be animated by the animation
system. This provides the basic operations for sampling and blending
animation values for more than just translation, rotation, and scale.

This implements part of bevyengine/rfcs#51, but is missing the
implementations for `Range<T>` and `Color`. This also does not fully
integrate with the existing `AnimationPlayer` yet, just setting up the
trait.

---------

Co-authored-by: Kirillov Kirill <kirusfg@gmail.com>
Co-authored-by: François <mockersf@gmail.com>
Co-authored-by: irate <JustTheCoolDude@gmail.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Alice Cecile <alice.i.cecil@gmail.com>
  • Loading branch information
6 people authored Feb 2, 2024
1 parent c859eac commit 602515d
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 2 deletions.
162 changes: 162 additions & 0 deletions crates/bevy_animation/src/animatable.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
use crate::util;
use bevy_ecs::world::World;
use bevy_math::*;
use bevy_reflect::Reflect;
use bevy_transform::prelude::Transform;
use bevy_utils::FloatOrd;

/// An individual input for [`Animatable::blend`].
pub struct BlendInput<T> {
/// The individual item's weight. This may not be bound to the range `[0.0, 1.0]`.
pub weight: f32,
/// The input value to be blended.
pub value: T,
/// Whether or not to additively blend this input into the final result.
pub additive: bool,
}

/// An animatable value type.
pub trait Animatable: Reflect + Sized + Send + Sync + 'static {
/// Interpolates between `a` and `b` with a interpolation factor of `time`.
///
/// The `time` parameter here may not be clamped to the range `[0.0, 1.0]`.
fn interpolate(a: &Self, b: &Self, time: f32) -> Self;

/// Blends one or more values together.
///
/// 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 {
($ty: ty, $base: ty) => {
impl Animatable for $ty {
#[inline]
fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
let t = <$base>::from(t);
(*a) * (1.0 - t) + (*b) * t
}

#[inline]
fn blend(inputs: impl Iterator<Item = BlendInput<Self>>) -> Self {
let mut value = Default::default();
for input in inputs {
if input.additive {
value += <$base>::from(input.weight) * input.value;
} else {
value = Self::interpolate(&value, &input.value, input.weight);
}
}
value
}
}
};
}

impl_float_animatable!(f32, f32);
impl_float_animatable!(Vec2, f32);
impl_float_animatable!(Vec3A, f32);
impl_float_animatable!(Vec4, f32);

impl_float_animatable!(f64, f64);
impl_float_animatable!(DVec2, f64);
impl_float_animatable!(DVec3, f64);
impl_float_animatable!(DVec4, f64);

// Vec3 is special cased to use Vec3A internally for blending
impl Animatable for Vec3 {
#[inline]
fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
(*a) * (1.0 - t) + (*b) * t
}

#[inline]
fn blend(inputs: impl Iterator<Item = BlendInput<Self>>) -> Self {
let mut value = Vec3A::ZERO;
for input in inputs {
if input.additive {
value += input.weight * Vec3A::from(input.value);
} else {
value = Vec3A::interpolate(&value, &Vec3A::from(input.value), input.weight);
}
}
Self::from(value)
}
}

impl Animatable for bool {
#[inline]
fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
util::step_unclamped(*a, *b, t)
}

#[inline]
fn blend(inputs: impl Iterator<Item = BlendInput<Self>>) -> Self {
inputs
.max_by(|a, b| FloatOrd(a.weight).cmp(&FloatOrd(b.weight)))
.map(|input| input.value)
.unwrap_or(false)
}
}

impl Animatable for Transform {
fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
Self {
translation: Vec3::interpolate(&a.translation, &b.translation, t),
rotation: Quat::interpolate(&a.rotation, &b.rotation, t),
scale: Vec3::interpolate(&a.scale, &b.scale, t),
}
}

fn blend(inputs: impl Iterator<Item = BlendInput<Self>>) -> Self {
let mut translation = Vec3A::ZERO;
let mut scale = Vec3A::ZERO;
let mut rotation = Quat::IDENTITY;

for input in inputs {
if input.additive {
translation += input.weight * Vec3A::from(input.value.translation);
scale += input.weight * Vec3A::from(input.value.scale);
rotation = rotation.slerp(input.value.rotation, input.weight);
} else {
translation = Vec3A::interpolate(
&translation,
&Vec3A::from(input.value.translation),
input.weight,
);
scale = Vec3A::interpolate(&scale, &Vec3A::from(input.value.scale), input.weight);
rotation = Quat::interpolate(&rotation, &input.value.rotation, input.weight);
}
}

Self {
translation: Vec3::from(translation),
rotation,
scale: Vec3::from(scale),
}
}
}

impl Animatable for Quat {
/// Performs an nlerp, because it's cheaper and easier to combine with other animations,
/// reference: <http://number-none.com/product/Understanding%20Slerp,%20Then%20Not%20Using%20It/>
#[inline]
fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
// We want to smoothly interpolate between the two quaternions by default,
// rather than using a quicker but less correct linear interpolation.
a.slerp(*b, t)
}

#[inline]
fn blend(inputs: impl Iterator<Item = BlendInput<Self>>) -> Self {
let mut value = Self::IDENTITY;
for input in inputs {
value = Self::interpolate(&value, &input.value, input.weight);
}
value
}
}
7 changes: 5 additions & 2 deletions crates/bevy_animation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
#![warn(missing_docs)]

mod animatable;
mod util;

use std::ops::{Add, Deref, Mul};
use std::time::Duration;

Expand All @@ -21,8 +24,8 @@ use bevy_utils::{tracing::warn, HashMap};
pub mod prelude {
#[doc(hidden)]
pub use crate::{
AnimationClip, AnimationPlayer, AnimationPlugin, EntityPath, Interpolation, Keyframes,
VariableCurve,
animatable::*, AnimationClip, AnimationPlayer, AnimationPlugin, EntityPath, Interpolation,
Keyframes, VariableCurve,
};
}

Expand Down
10 changes: 10 additions & 0 deletions crates/bevy_animation/src/util.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/// Steps between two different discrete values of any type.
/// Returns `a` if `t < 1.0`, otherwise returns `b`.
#[inline]
pub(crate) fn step_unclamped<T>(a: T, b: T, t: f32) -> T {
if t < 1.0 {
a
} else {
b
}
}

0 comments on commit 602515d

Please sign in to comment.