From 3c645c69665122e83188b4377caf82e7ca3b6081 Mon Sep 17 00:00:00 2001 From: James Liu Date: Fri, 2 Feb 2024 13:19:37 -0800 Subject: [PATCH] Animatable trait for interpolation and blending (#4482) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 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` and `Color`. This also does not fully integrate with the existing `AnimationPlayer` yet, just setting up the trait. --------- Co-authored-by: Kirillov Kirill Co-authored-by: François Co-authored-by: irate Co-authored-by: Alice Cecile Co-authored-by: Alice Cecile --- crates/bevy_animation/src/animatable.rs | 162 ++++++++++++++++++++++++ crates/bevy_animation/src/lib.rs | 7 +- crates/bevy_animation/src/util.rs | 10 ++ 3 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 crates/bevy_animation/src/animatable.rs create mode 100644 crates/bevy_animation/src/util.rs diff --git a/crates/bevy_animation/src/animatable.rs b/crates/bevy_animation/src/animatable.rs new file mode 100644 index 00000000000000..24b90316faa35d --- /dev/null +++ b/crates/bevy_animation/src/animatable.rs @@ -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 { + /// 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>) -> 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>) -> 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>) -> 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>) -> 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>) -> 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: + #[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>) -> Self { + let mut value = Self::IDENTITY; + for input in inputs { + value = Self::interpolate(&value, &input.value, input.weight); + } + value + } +} diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 05e6921e0afcd0..43d66fd2d81bbb 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -2,6 +2,9 @@ #![warn(missing_docs)] +mod animatable; +mod util; + use std::ops::{Add, Deref, Mul}; use std::time::Duration; @@ -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, }; } diff --git a/crates/bevy_animation/src/util.rs b/crates/bevy_animation/src/util.rs new file mode 100644 index 00000000000000..67aaf8116e365c --- /dev/null +++ b/crates/bevy_animation/src/util.rs @@ -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(a: T, b: T, t: f32) -> T { + if t < 1.0 { + a + } else { + b + } +}