diff --git a/crates/bevy_animation/src/animatable.rs b/crates/bevy_animation/src/animatable.rs deleted file mode 100644 index a345c5fce4f0a..0000000000000 --- a/crates/bevy_animation/src/animatable.rs +++ /dev/null @@ -1,272 +0,0 @@ -//! Traits and type for interpolating between values. - -use crate::util; -use bevy_color::{Laba, LinearRgba, Oklaba, Srgba, Xyza}; -use bevy_math::*; -use bevy_reflect::Reflect; -use bevy_transform::prelude::Transform; - -/// 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 an 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; -} - -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 - } - } - }; -} - -macro_rules! impl_color_animatable { - ($ty: ident) => { - impl Animatable for $ty { - #[inline] - fn interpolate(a: &Self, b: &Self, t: f32) -> Self { - let value = *a * (1. - t) + *b * t; - value - } - - #[inline] - fn blend(inputs: impl Iterator>) -> Self { - let mut value = Default::default(); - for input in inputs { - if input.additive { - value += 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); - -impl_color_animatable!(LinearRgba); -impl_color_animatable!(Laba); -impl_color_animatable!(Oklaba); -impl_color_animatable!(Srgba); -impl_color_animatable!(Xyza); - -// 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_key(|x| FloatOrd(x.weight)) - .is_some_and(|input| input.value) - } -} - -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 = - Quat::slerp(Quat::IDENTITY, input.value.rotation, input.weight) * rotation; - } 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 a slerp to smoothly interpolate between quaternions. - #[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 BlendInput { - weight, - value: incoming_value, - additive, - } in inputs - { - if additive { - value = Self::slerp(Self::IDENTITY, incoming_value, weight) * value; - } else { - value = Self::interpolate(&value, &incoming_value, weight); - } - } - value - } -} - -/// 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`. -pub fn interpolate_with_cubic_bezier(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) -} diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index be68c9357658f..101ef8dcaa4de 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -22,7 +22,7 @@ //! //! For instance, let's imagine that we want to use the `Vec3` output //! from our curve to animate the [translation component of a `Transform`]. For this, there is -//! the adaptor [`AnimatableCurve`], which wraps any [`Curve`] and [`AnimatableProperty`] and turns it into an +//! the adaptor [`PropertyCurve`], which wraps any [`Curve`] and [`AnimatableProperty`] and turns it into an //! [`AnimationCurve`] that will use the given curve to animate the entity's property: //! //! # use bevy_math::curve::{Curve, Interval, FunctionCurve}; @@ -33,7 +33,7 @@ //! # Interval::UNIT, //! # |t| vec3(t.cos(), 0.0, 0.0) //! # ); -//! let wobble_animation = AnimatableCurve::new(animated_field!(Transform::translation), wobble_curve); +//! let wobble_animation = PropertyCurve::new(animated_field!(Transform::translation), wobble_curve); //! //! And finally, this [`AnimationCurve`] needs to be added to an [`AnimationClip`] in order to //! actually animate something. This is what that looks like: @@ -47,7 +47,7 @@ //! # Interval::UNIT, //! # |t| { vec3(t.cos(), 0.0, 0.0) }, //! # ); -//! # let wobble_animation = AnimatableCurve::new(animated_field!(Transform::translation), wobble_curve); +//! # let wobble_animation = PropertyCurve::new(animated_field!(Transform::translation), wobble_curve); //! # let animation_target_id = AnimationTargetId::from(&Name::new("Test")); //! let mut animation_clip = AnimationClip::default(); //! animation_clip.add_curve_to_target( @@ -63,7 +63,7 @@ //! //! ## Animated Fields //! -//! The [`animated_field`] macro (which returns an [`AnimatedField`]), in combination with [`AnimatableCurve`] +//! The [`animated_field`] macro (which returns an [`AnimatedField`]), in combination with [`PropertyCurve`] //! is the easiest way to make an animation curve (see the example above). //! //! This will select a field on a component and pass it to a [`Curve`] with a type that matches the field. @@ -71,7 +71,7 @@ //! ## Animatable Properties //! //! Animation of arbitrary aspects of entities can be accomplished using [`AnimatableProperty`] in -//! conjunction with [`AnimatableCurve`]. See the documentation [there] for details. +//! conjunction with [`PropertyCurve`]. See the documentation [there] for details. //! //! ## Custom [`AnimationCurve`] and [`AnimationCurveEvaluator`] //! @@ -91,14 +91,17 @@ use core::{ use crate::{ graph::AnimationNodeIndex, - prelude::{Animatable, BlendInput}, + prelude::{Blendable, Blender}, AnimationEntityMut, AnimationEvaluationError, }; use bevy_ecs::component::{Component, Mutable}; -use bevy_math::curve::{ - cores::{UnevenCore, UnevenCoreError}, - iterable::IterableCurve, - Curve, Interval, +use bevy_math::{ + curve::{ + cores::{UnevenCore, UnevenCoreError}, + iterable::IterableCurve, + Curve, Interval, + }, + Interpolate, }; use bevy_mesh::morph::MorphWeights; use bevy_platform_support::hash::Hashed; @@ -111,6 +114,12 @@ use downcast_rs::{impl_downcast, Downcast}; /// as long as it can be obtained by mutable reference. This makes it more /// flexible than [`animated_field`]. /// +/// Promperties that [`Blend`] are easiest to use, because the default +/// implementation of [`AnimationCurve`] for [`PropertyCurve`] is blending-based. +/// To animating a non-blendable property (like a boolean or enum), you must add +/// a new `AnimationCurve` implementation to `PropertyCurve`. +/// +/// [`Blend`]: crate::blend::Blend /// [`animated_field`]: crate::animated_field /// /// Here, `AnimatableProperty` is used to animate a value inside an `Option`, @@ -149,11 +158,10 @@ use downcast_rs::{impl_downcast, Downcast}; /// } /// } /// -/// -/// You can then create an [`AnimatableCurve`] to animate this property like so: +/// You can then create a [`PropertyCurve`] to animate this property like so: /// /// # use bevy_animation::{VariableCurve, AnimationEntityMut, AnimationEvaluationError, animation_curves::EvaluatorId}; -/// # use bevy_animation::prelude::{AnimatableProperty, AnimatableKeyframeCurve, AnimatableCurve}; +/// # use bevy_animation::prelude::{AnimatableProperty, KeyframeCurve, PropertyCurve}; /// # use bevy_ecs::{name::Name, component::Component}; /// # use std::any::TypeId; /// # #[derive(Component)] @@ -180,16 +188,16 @@ use downcast_rs::{impl_downcast, Downcast}; /// # EvaluatorId::Type(TypeId::of::()) /// # } /// # } -/// AnimatableCurve::new( +/// PropertyCurve::new( /// PowerLevelProperty, -/// AnimatableKeyframeCurve::new([ +/// KeyframeCurve::new([ /// (0.0, 0.0), /// (1.0, 9001.0), /// ]).expect("Failed to create power level curve") /// ); pub trait AnimatableProperty: Send + Sync + 'static { /// The animated property type. - type Property: Animatable; + type Property: Send + Sync + 'static; /// Retrieves the property from the given `entity`. fn get_mut<'a>( @@ -207,29 +215,29 @@ pub trait AnimatableProperty: Send + Sync + 'static { /// /// The best way to create an instance of this type is via the [`animated_field`] macro. /// -/// `C` is the component being animated, `A` is the type of the [`Animatable`] field on the component, and `F` is an accessor +/// `C` is the component being animated, `B` is the type of the [`Blendable`] field on the component, and `F` is an accessor /// function that accepts a reference to `C` and retrieves the field `A`. /// /// [`animated_field`]: crate::animated_field #[derive(Clone)] -pub struct AnimatedField &mut A> { +pub struct AnimatedField &mut B> { func: F, /// A pre-hashed (component-type-id, reflected-field-index) pair, uniquely identifying a component field evaluator_id: Hashed<(TypeId, usize)>, - marker: PhantomData<(C, A)>, + marker: PhantomData<(C, B)>, } -impl AnimatableProperty for AnimatedField +impl AnimatableProperty for AnimatedField where C: Component, - A: Animatable + Clone + Sync + Debug, - F: Fn(&mut C) -> &mut A + Send + Sync + 'static, + B: Blendable + Clone + Send + Sync + Debug + 'static, + F: Fn(&mut C) -> &mut B + Send + Sync + 'static, { - type Property = A; + type Property = B; fn get_mut<'a>( &self, entity: &'a mut AnimationEntityMut, - ) -> Result<&'a mut A, AnimationEvaluationError> { + ) -> Result<&'a mut B, AnimationEvaluationError> { let c = entity .get_mut::() .ok_or_else(|| AnimationEvaluationError::ComponentNotPresent(TypeId::of::()))?; @@ -285,7 +293,7 @@ impl AnimationCompatibleCurve for C where C: Curve + Debug + Clone + /// [property type]: AnimatableProperty::Property #[derive(Reflect, FromReflect)] #[reflect(from_reflect = false)] -pub struct AnimatableCurve { +pub struct PropertyCurve { /// The property selector, which defines what component to access and how to access /// a property on that component. pub property: P, @@ -296,22 +304,22 @@ pub struct AnimatableCurve { pub curve: C, } -/// An [`AnimatableCurveEvaluator`] for [`AnimatableProperty`] instances. +/// An [`AnimationCurveEvaluator`] for [`AnimatableProperty`] instances that support blending. /// /// You shouldn't ordinarily need to instantiate one of these manually. Bevy -/// will automatically do so when you use an [`AnimatableCurve`] instance. +/// will automatically do so when you use a [`PropertyCurve`] instance. #[derive(Reflect)] -pub struct AnimatableCurveEvaluator { - evaluator: BasicAnimationCurveEvaluator, - property: Box>, +pub struct PropertyCurveEvaluator { + evaluator: BlendStackEvaluator, + property: Box>, } -impl AnimatableCurve +impl PropertyCurve where P: AnimatableProperty, C: AnimationCompatibleCurve, { - /// Create an [`AnimatableCurve`] (and thus an [`AnimationCurve`]) from a curve + /// Create a [`PropertyCurve`] (and thus an [`AnimationCurve`]) from a curve /// valued in an [animatable property]. /// /// [animatable property]: AnimatableProperty::Property @@ -320,7 +328,7 @@ where } } -impl Clone for AnimatableCurve +impl Clone for PropertyCurve where C: Clone, P: Clone, @@ -333,20 +341,24 @@ where } } -impl Debug for AnimatableCurve +impl Debug for PropertyCurve where C: Debug, { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.debug_struct("AnimatableCurve") + f.debug_struct("PropertyCurve") .field("curve", &self.curve) .finish() } } -impl AnimationCurve for AnimatableCurve +// We can treat any `PropertyCurve` over a `Blendable` property as an `AnimationCurve`, using the +// `BlendStackEvaluator`. You can still create a `PropertyCurve` for something that dosn't implement +// `Blend`/`Blendable` but you will have to implement `AnimationCurve` for `PropertyCurve` manually. +impl AnimationCurve for PropertyCurve where - P: AnimatableProperty + Clone, + P: AnimatableProperty + Clone, + B: Blendable + Send + Sync + 'static, C: AnimationCompatibleCurve + Clone, { fn clone_value(&self) -> Box { @@ -362,8 +374,8 @@ where } fn create_evaluator(&self) -> Box { - Box::new(AnimatableCurveEvaluator:: { - evaluator: BasicAnimationCurveEvaluator::default(), + Box::new(PropertyCurveEvaluator:: { + evaluator: BlendStackEvaluator::default(), property: Box::new(self.property.clone()), }) } @@ -376,22 +388,22 @@ where graph_node: AnimationNodeIndex, ) -> Result<(), AnimationEvaluationError> { let curve_evaluator = curve_evaluator - .downcast_mut::>() + .downcast_mut::>() .unwrap(); let value = self.curve.sample_clamped(t); - curve_evaluator - .evaluator - .stack - .push(BasicAnimationCurveEvaluatorStackElement { - value, - weight, - graph_node, - }); + curve_evaluator.evaluator.stack.push(BlendStackElement { + value, + weight, + graph_node, + }); Ok(()) } } -impl AnimationCurveEvaluator for AnimatableCurveEvaluator { +impl AnimationCurveEvaluator for PropertyCurveEvaluator +where + B: Blendable + Send + Sync + 'static, +{ fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { self.evaluator.combine(graph_node, /*additive=*/ false) } @@ -417,7 +429,7 @@ impl AnimationCurveEvaluator for AnimatableCurveEvaluator { .evaluator .stack .pop() - .ok_or_else(inconsistent::>)? + .ok_or_else(inconsistent::>)? .value; Ok(()) } @@ -563,7 +575,7 @@ impl WeightsCurveEvaluator { if additive { *dest += src * weight_to_blend; } else { - *dest = f32::interpolate(dest, &src, weight_to_blend / *current_weight); + *dest = f32::interp(dest, &src, weight_to_blend / *current_weight); } } } @@ -627,39 +639,39 @@ impl AnimationCurveEvaluator for WeightsCurveEvaluator { } #[derive(Reflect)] -struct BasicAnimationCurveEvaluator +struct BlendStackEvaluator where - A: Animatable, + B: Blendable, { - stack: Vec>, - blend_register: Option<(A, f32)>, + stack: Vec>, + blend_register: Option>, } #[derive(Reflect)] -struct BasicAnimationCurveEvaluatorStackElement +struct BlendStackElement where - A: Animatable, + B: Blendable, { - value: A, + value: B, weight: f32, graph_node: AnimationNodeIndex, } -impl Default for BasicAnimationCurveEvaluator +impl Default for BlendStackEvaluator where - A: Animatable, + B: Blendable, { fn default() -> Self { - BasicAnimationCurveEvaluator { + BlendStackEvaluator { stack: vec![], blend_register: None, } } } -impl BasicAnimationCurveEvaluator +impl BlendStackEvaluator where - A: Animatable, + B: Blendable, { fn combine( &mut self, @@ -673,74 +685,32 @@ where return Ok(()); } - let BasicAnimationCurveEvaluatorStackElement { + let BlendStackElement { value: value_to_blend, weight: weight_to_blend, graph_node: _, } = self.stack.pop().unwrap(); - match self.blend_register.take() { - None => { - self.initialize_blend_register(value_to_blend, weight_to_blend, additive); - } - Some((mut current_value, mut current_weight)) => { - current_weight += weight_to_blend; + let blender = match (self.blend_register.take(), additive) { + (None, true) => value_to_blend.blend_additive(weight_to_blend), + (None, false) => value_to_blend.blend_interp(weight_to_blend), + (Some(blender), true) => blender.blend_additive(value_to_blend, weight_to_blend), + (Some(blender), false) => blender.blend_interp(value_to_blend, weight_to_blend), + }; - if additive { - current_value = A::blend( - [ - BlendInput { - weight: 1.0, - value: current_value, - additive: true, - }, - BlendInput { - weight: weight_to_blend, - value: value_to_blend, - additive: true, - }, - ] - .into_iter(), - ); - } else { - current_value = A::interpolate( - ¤t_value, - &value_to_blend, - weight_to_blend / current_weight, - ); - } - - self.blend_register = Some((current_value, current_weight)); - } - } + self.blend_register = Some(blender); Ok(()) } - fn initialize_blend_register(&mut self, value: A, weight: f32, additive: bool) { - if additive { - let scaled_value = A::blend( - [BlendInput { - weight, - value, - additive: true, - }] - .into_iter(), - ); - self.blend_register = Some((scaled_value, weight)); - } else { - self.blend_register = Some((value, weight)); - } - } - fn push_blend_register( &mut self, weight: f32, graph_node: AnimationNodeIndex, ) -> Result<(), AnimationEvaluationError> { - if let Some((value, _)) = self.blend_register.take() { - self.stack.push(BasicAnimationCurveEvaluatorStackElement { - value, + if let Some(blender) = self.blend_register.take() { + self.stack.push(BlendStackElement { + value: blender.finish(), weight, graph_node, }); @@ -753,9 +723,9 @@ where /// to entities by the animation system. /// /// Typically, this will not need to be implemented manually, since it is -/// automatically implemented by [`AnimatableCurve`] and other curves used by +/// automatically implemented by [`PropertyCurve`] and other curves used by /// the animation system (e.g. those that animate parts of transforms or morph -/// weights). However, this can be implemented manually when `AnimatableCurve` +/// weights). However, this can be implemented manually when `PropertyCurve` /// is not sufficiently expressive. /// /// In many respects, this behaves like a type-erased form of [`Curve`], where @@ -826,24 +796,25 @@ pub enum EvaluatorId<'a> { /// 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 [`AnimatableCurveEvaluator`] +/// You can implement this trait when the generic [`PropertyCurveEvaluator`] /// isn't sufficiently-expressive for your needs. For example, [`MorphWeights`] -/// implements this trait instead of using [`AnimatableCurveEvaluator`] because +/// implements this trait instead of using [`PropertyCurveEvaluator`] 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. +/// with [`Blend`] as that works on fixed-size values only. /// /// If you implement this trait, you should also implement [`AnimationCurve`] on /// your curve type, as that trait allows creating instances of this one. /// -/// Implementations of [`AnimatableCurveEvaluator`] should maintain a *stack* of +/// Implementations of [`AnimationCurveEvaluator`] should maintain a *stack* of /// (value, weight, node index) triples, as well as a *blend register*, which is -/// either a (value, weight) pair or empty. *Value* here refers to an instance +/// either an active [`Blender`] or empty. *Value* here refers to an instance /// of the value being animated: for example, [`Vec3`] in the case of -/// translation keyframes. The stack stores intermediate values generated while +/// translation keyframes. The stack stores intermediate values generated while /// evaluating the [`crate::graph::AnimationGraph`], while the blend register /// stores the result of a blend operation. /// /// [`Vec3`]: bevy_math::Vec3 +/// [`Blend`]: crate::blend::Blend pub trait AnimationCurveEvaluator: Downcast + Send + Sync + 'static { /// Blends the top element of the stack with the blend register. /// @@ -913,20 +884,20 @@ pub trait AnimationCurveEvaluator: Downcast + Send + Sync + 'static { impl_downcast!(AnimationCurveEvaluator); -/// A [curve] defined by keyframes with values in an [animatable] type. +/// A [curve] defined by keyframes with values that can be [interpolated between]. /// -/// The keyframes are interpolated using the type's [`Animatable::interpolate`] implementation. +/// The keyframes are interpolated using the type's [`Interpolate::interp`] implementation. /// /// [curve]: Curve -/// [animatable]: Animatable +/// [interpolated between]: Interpolate #[derive(Debug, Clone, Reflect)] -pub struct AnimatableKeyframeCurve { +pub struct KeyframeCurve { core: UnevenCore, } -impl Curve for AnimatableKeyframeCurve +impl Curve for KeyframeCurve where - T: Animatable + Clone, + T: Interpolate + Clone, { #[inline] fn domain(&self) -> Interval { @@ -936,7 +907,7 @@ where #[inline] fn sample_clamped(&self, t: f32) -> T { // `UnevenCore::sample_with` is implicitly clamped. - self.core.sample_with(t, ::interpolate) + self.core.sample_with(t, T::interp) } #[inline] @@ -945,13 +916,13 @@ where } } -impl AnimatableKeyframeCurve +impl KeyframeCurve where - T: Animatable, + T: Interpolate, { - /// Create a new [`AnimatableKeyframeCurve`] from the given `keyframes`. The values of this + /// Create a new [`KeyframeCurve`] from the given `keyframes`. The values of this /// curve are interpolated from the keyframes using the output type's implementation of - /// [`Animatable::interpolate`]. + /// [`Interpolate::interp`]. /// /// There must be at least two samples in order for this method to succeed. pub fn new(keyframes: impl IntoIterator) -> Result { diff --git a/crates/bevy_animation/src/blend.rs b/crates/bevy_animation/src/blend.rs new file mode 100644 index 0000000000000..95f36e6d37488 --- /dev/null +++ b/crates/bevy_animation/src/blend.rs @@ -0,0 +1,189 @@ +//! This module contains traits for additive blending, for use in animation. + +use bevy_color::{ + ColorToComponents, Gray, Hsla, Hwba, Laba, Lcha, LinearRgba, Oklaba, Oklcha, Srgba, Xyza, +}; +use bevy_math::*; +use bevy_transform::components::Transform; + +/// A type with a natural method of addative blending. +/// +/// Any type with a well-behaved method of composition which also implements the [`Interpolate`] trait +/// can support addative blending. Specifically, the type must: +/// +/// 1. Have a natural method of composition. For vectors this is addition, for quaternions or rotations +/// composition is usually expressed as multiplication. We'll use `comp(a, b)` to denote this operation. +/// +/// 2. The operation must be associative. That is, `comp(comp(a, b), c)` must be equivalent to +/// `comp(a, comp(b, c))`. The result does not need to be data-identical, but it should be equivalent +/// under some reasonable notion of equivalence. +/// +/// 2. Have an `IDENTITY` value such that composition with the identity is equivalent in some way to the +/// original value. +/// +/// 3. The value of `T::blend(a, b, w)` should be equivalent to `comp(a, interp(IDENTITY, b, w))`. This implies +/// that `T::blend(a, b, 0)` is equivalent to `a` and `T::blend(a, b, 1)` is equivalent to `comp(a, b)`. +/// +/// Some of you will have noticed that these rules encodes the axioms for a Monoid; In fact this trait +/// represents something similar to a Lie Group. +pub trait Blend: Interpolate { + /// The default value of the blend, which has no effect when blended with other values. + const IDENTITY: Self; + + /// Addatively blends another value on top of this one. + fn blend(self, other: Self, blend_weight: f32) -> Self; +} + +/// The state for an ongoing blend operation. On types implementing `Blend`, you can start a new +/// blend operation using [`Blendable::blend_additive`] or [`Blendable::blend_interp`]. +pub struct Blender +where + T: Blend, +{ + /// The value being blended. + value: T, + /// The cumulative weight of the blend operation so far. Every new blend operation + /// increases this weight. + weight: f32, +} + +impl Blender +where + T: Blend, +{ + /// Addatively blends the value into the blender. + pub fn blend_additive(self, value: T, weight: f32) -> Self { + Blender { + value: T::blend(self.value, value, weight), + weight: self.weight + weight, + } + } + + /// Interpolatively blends the value into the blender. + pub fn blend_interp(self, value: T, weight: f32) -> Self { + let cumulative_weight = self.weight + weight; + Blender { + value: T::interp(&self.value, &value, weight / cumulative_weight), + weight: cumulative_weight, + } + } + + /// Finishes the blend and returns the blended value. + pub fn finish(self) -> T { + self.value + } +} + +/// This is an extension trait to `Blend` with methods to easily blend and interpolate +/// between multiple values. +pub trait Blendable: Blend { + /// Addatively blends the value into a new blender. + fn blend_additive(self, weight: f32) -> Blender { + Blender { + value: Self::blend(Self::IDENTITY, self, weight), + weight, + } + } + + /// Interpolateively blends the value into a new blender. + fn blend_interp(self, weight: f32) -> Blender { + Blender { + value: self, + weight, + } + } +} + +impl Blendable for T where T: Blend {} + +macro_rules! impl_blendable_vectorspace { + ($ty: ident) => { + impl Blend for $ty { + const IDENTITY: Self = <$ty as VectorSpace>::ZERO; + + #[inline] + fn blend(self, other: Self, blend_weight: f32) -> Self { + self + other * blend_weight + } + } + }; +} + +impl_blendable_vectorspace!(f32); +impl_blendable_vectorspace!(Vec2); +impl_blendable_vectorspace!(Vec3); +impl_blendable_vectorspace!(Vec3A); +impl_blendable_vectorspace!(Vec4); + +macro_rules! impl_blendable_color { + ($ty: ident) => { + impl Blend for $ty { + const IDENTITY: Self = $ty::BLACK; + + #[inline] + fn blend(self, other: Self, blend_weight: f32) -> Self { + $ty::from_vec4(self.to_vec4() + other.to_vec4() * blend_weight) + } + } + }; +} + +impl_blendable_color!(Srgba); +impl_blendable_color!(LinearRgba); +impl_blendable_color!(Hsla); +impl_blendable_color!(Hwba); +impl_blendable_color!(Laba); +impl_blendable_color!(Lcha); +impl_blendable_color!(Oklaba); +impl_blendable_color!(Oklcha); +impl_blendable_color!(Xyza); + +macro_rules! impl_blendable_group_action { + ($ty: ident) => { + impl Blend for $ty { + const IDENTITY: Self = $ty::IDENTITY; + + #[inline] + fn blend(self, other: Self, blend_weight: f32) -> Self { + $ty::interp(&Self::IDENTITY, &other, blend_weight) * self + } + } + }; +} + +impl_blendable_group_action!(Rot2); +impl_blendable_group_action!(Quat); + +impl Blend for Transform { + const IDENTITY: Self = Transform::IDENTITY; + + fn blend(self, other: Self, blend_weight: f32) -> Self { + Transform { + translation: Vec3::blend(self.translation, other.translation, blend_weight), + scale: Vec3::blend(self.scale, other.scale, blend_weight), + rotation: Quat::blend(self.rotation, other.rotation, blend_weight), + } + } +} + +impl Blend for Isometry2d { + const IDENTITY: Self = Isometry2d::IDENTITY; + + fn blend(self, other: Self, blend_weight: f32) -> Self { + Isometry2d { + rotation: Rot2::blend(self.rotation, other.rotation, blend_weight), + translation: Vec2::blend(self.translation, other.translation, blend_weight), + } + } +} + +impl Blend for Isometry3d { + const IDENTITY: Self = Isometry3d::IDENTITY; + + fn blend(self, other: Self, blend_weight: f32) -> Self { + Isometry3d { + rotation: Quat::blend(self.rotation, other.rotation, blend_weight), + translation: Vec3A::blend(self.translation, other.translation, blend_weight), + } + } +} diff --git a/crates/bevy_animation/src/gltf_curves.rs b/crates/bevy_animation/src/gltf_curves.rs index 688011a32cf71..4ec090556eb16 100644 --- a/crates/bevy_animation/src/gltf_curves.rs +++ b/crates/bevy_animation/src/gltf_curves.rs @@ -194,7 +194,7 @@ where | InterpolationDatum::RightTail(v) => Either::Left(v.iter().copied()), InterpolationDatum::Between(u, v, s) => { - let interpolated = u.iter().zip(v.iter()).map(move |(x, y)| x.lerp(*y, s)); + let interpolated = u.iter().zip(v.iter()).map(move |(x, y)| x.interp(y, s)); Either::Right(interpolated) } } diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index cfb7f33e04ccf..fb8e81b29018d 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -9,12 +9,11 @@ extern crate alloc; -pub mod animatable; pub mod animation_curves; +pub mod blend; pub mod gltf_curves; pub mod graph; pub mod transition; -mod util; use core::{ any::TypeId, @@ -52,8 +51,8 @@ use uuid::Uuid; pub mod prelude { #[doc(hidden)] pub use crate::{ - animatable::*, animation_curves::*, graph::*, transition::*, AnimationClip, - AnimationPlayer, AnimationPlugin, VariableCurve, + animation_curves::*, blend::*, graph::*, transition::*, AnimationClip, AnimationPlayer, + AnimationPlugin, VariableCurve, }; } diff --git a/crates/bevy_animation/src/util.rs b/crates/bevy_animation/src/util.rs deleted file mode 100644 index 67aaf8116e365..0000000000000 --- a/crates/bevy_animation/src/util.rs +++ /dev/null @@ -1,10 +0,0 @@ -/// 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 - } -} diff --git a/crates/bevy_color/src/color.rs b/crates/bevy_color/src/color.rs index 832394449bc4f..2a8959b752ab8 100644 --- a/crates/bevy_color/src/color.rs +++ b/crates/bevy_color/src/color.rs @@ -1,7 +1,8 @@ use crate::{ color_difference::EuclideanDistance, Alpha, Hsla, Hsva, Hue, Hwba, Laba, Lcha, LinearRgba, - Luminance, Mix, Oklaba, Oklcha, Saturation, Srgba, StandardColor, Xyza, + Luminance, Oklaba, Oklcha, Saturation, Srgba, StandardColor, Xyza, }; +use bevy_math::{curve::InterpolateCurve, Interpolate, InterpolateStable}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::prelude::*; use derive_more::derive::From; @@ -17,7 +18,7 @@ use derive_more::derive::From; /// /// # Operations /// -/// [`Color`] supports all the standard color operations, such as [mixing](Mix), +/// [`Color`] supports all the standard color operations, such as [interpolating](bevy_math::Interpolate), /// [luminance](Luminance) and [hue](Hue) adjustment, /// and [diffing](EuclideanDistance). These operations delegate to the concrete color space contained /// by [`Color`], but will convert to [`Oklch`](Oklcha) for operations which aren't supported in the @@ -476,6 +477,16 @@ impl Default for Color { } } +impl Interpolate for Color { + #[inline] + fn interp(&self, other: &Self, param: f32) -> Self { + Oklaba::interp(&(*self).into(), &(*other).into(), param).into() + } +} + +impl InterpolateStable for Color {} +impl InterpolateCurve for Color {} + impl Alpha for Color { fn with_alpha(&self, alpha: f32) -> Self { let mut new = *self; @@ -852,27 +863,6 @@ impl Saturation for Color { } } -impl Mix for Color { - fn mix(&self, other: &Self, factor: f32) -> Self { - let mut new = *self; - - match &mut new { - Color::Srgba(x) => *x = x.mix(&(*other).into(), factor), - Color::LinearRgba(x) => *x = x.mix(&(*other).into(), factor), - Color::Hsla(x) => *x = x.mix(&(*other).into(), factor), - Color::Hsva(x) => *x = x.mix(&(*other).into(), factor), - Color::Hwba(x) => *x = x.mix(&(*other).into(), factor), - Color::Laba(x) => *x = x.mix(&(*other).into(), factor), - Color::Lcha(x) => *x = x.mix(&(*other).into(), factor), - Color::Oklaba(x) => *x = x.mix(&(*other).into(), factor), - Color::Oklcha(x) => *x = x.mix(&(*other).into(), factor), - Color::Xyza(x) => *x = x.mix(&(*other).into(), factor), - } - - new - } -} - impl EuclideanDistance for Color { fn distance_squared(&self, other: &Self) -> f32 { match self { diff --git a/crates/bevy_color/src/color_gradient.rs b/crates/bevy_color/src/color_gradient.rs index b087205bb6b49..d4d3e1cd4269c 100644 --- a/crates/bevy_color/src/color_gradient.rs +++ b/crates/bevy_color/src/color_gradient.rs @@ -1,8 +1,10 @@ -use crate::Mix; use alloc::vec::Vec; -use bevy_math::curve::{ - cores::{EvenCore, EvenCoreError}, - Curve, Interval, +use bevy_math::{ + curve::{ + cores::{EvenCore, EvenCoreError}, + Curve, Interval, + }, + Interpolate, }; /// A curve whose samples are defined by a collection of colors. @@ -15,21 +17,18 @@ pub struct ColorCurve { impl ColorCurve where - T: Mix + Clone, + T: Clone + Interpolate, { - /// Create a new [`ColorCurve`] from a collection of [mixable] types. The domain of this curve + /// Create a new [`ColorCurve`] from a collection of colors. The domain of this curve /// will always be `[0.0, len - 1]` where `len` is the amount of mixable objects in the /// collection. /// - /// This fails if there's not at least two mixable things in the collection. - /// - /// [mixable]: `Mix` + /// This fails if less than two colors are provided. /// /// # Example /// /// ``` /// # use bevy_color::palettes::basic::*; - /// # use bevy_color::Mix; /// # use bevy_color::Srgba; /// # use bevy_color::ColorCurve; /// # use bevy_math::curve::Interval; @@ -53,7 +52,7 @@ where impl Curve for ColorCurve where - T: Mix + Clone, + T: Clone + Interpolate, { #[inline] fn domain(&self) -> Interval { @@ -63,7 +62,7 @@ where #[inline] fn sample_clamped(&self, t: f32) -> T { // `EvenCore::sample_with` clamps the input implicitly. - self.core.sample_with(t, T::mix) + self.core.sample_with(t, T::interp) } #[inline] @@ -88,7 +87,7 @@ mod tests { assert_eq!(curve.domain(), Interval::new(0.0, 2.0).unwrap()); - let brighter_curve = curve.map(|c: Srgba| c.mix(&basic::WHITE, 0.5)); + let brighter_curve = curve.map(|c: Srgba| c.interp(&basic::WHITE, 0.5)); [ (-0.1, None), diff --git a/crates/bevy_color/src/color_ops.rs b/crates/bevy_color/src/color_ops.rs index 60a535d9fe7da..6e891f929f7fa 100644 --- a/crates/bevy_color/src/color_ops.rs +++ b/crates/bevy_color/src/color_ops.rs @@ -1,4 +1,4 @@ -use bevy_math::{ops, Vec3, Vec4}; +use bevy_math::{ops, Interpolate, Vec3, Vec4}; /// Methods for changing the luminance of a color. Note that these methods are not /// guaranteed to produce consistent results across color spaces, @@ -29,21 +29,8 @@ pub trait Luminance: Sized { fn lighter(&self, amount: f32) -> Self; } -/// Linear interpolation of two colors within a given color space. -pub trait Mix: Sized { - /// Linearly interpolate between this and another color, by factor. - /// Factor should be between 0.0 and 1.0. - fn mix(&self, other: &Self, factor: f32) -> Self; - - /// Linearly interpolate between this and another color, by factor, storing the result - /// in this color. Factor should be between 0.0 and 1.0. - fn mix_assign(&mut self, other: Self, factor: f32) { - *self = self.mix(&other, factor); - } -} - /// Trait for returning a grayscale color of a provided lightness. -pub trait Gray: Mix + Sized { +pub trait Gray: Interpolate { /// A pure black color. const BLACK: Self; /// A pure white color. @@ -51,7 +38,7 @@ pub trait Gray: Mix + Sized { /// Returns a grey color with the provided lightness from (0.0 - 1.0). 0 is black, 1 is white. fn gray(lightness: f32) -> Self { - Self::BLACK.mix(&Self::WHITE, lightness) + Self::BLACK.interp(&Self::WHITE, lightness) } } diff --git a/crates/bevy_color/src/color_range.rs b/crates/bevy_color/src/color_range.rs index 48afa5418225d..bcadeefa55f58 100644 --- a/crates/bevy_color/src/color_range.rs +++ b/crates/bevy_color/src/color_range.rs @@ -1,21 +1,21 @@ use core::ops::Range; -use crate::Mix; +use bevy_math::Interpolate; /// Represents a range of colors that can be linearly interpolated, defined by a start and /// end point which must be in the same color space. It works for any color type that -/// implements [`Mix`]. +/// implements [`bevy_math::Interpolate`] (which is all of them). /// /// This is useful for defining gradients or animated color transitions. -pub trait ColorRange { +pub trait ColorRange { /// Get the color value at the given interpolation factor, which should be between 0.0 (start) /// and 1.0 (end). fn at(&self, factor: f32) -> T; } -impl ColorRange for Range { +impl ColorRange for Range { fn at(&self, factor: f32) -> T { - self.start.mix(&self.end, factor.clamp(0.0, 1.0)) + self.start.interp(&self.end, factor.clamp(0.0, 1.0)) } } diff --git a/crates/bevy_color/src/hsla.rs b/crates/bevy_color/src/hsla.rs index b29fce72ac9dc..0db4f4b1bf40d 100644 --- a/crates/bevy_color/src/hsla.rs +++ b/crates/bevy_color/src/hsla.rs @@ -1,8 +1,8 @@ use crate::{ - Alpha, ColorToComponents, Gray, Hsva, Hue, Hwba, Lcha, LinearRgba, Luminance, Mix, Saturation, + Alpha, ColorToComponents, Gray, Hsva, Hue, Hwba, Lcha, LinearRgba, Luminance, Saturation, Srgba, StandardColor, Xyza, }; -use bevy_math::{Vec3, Vec4}; +use bevy_math::{Interpolate, InterpolateStable, Vec3, Vec4}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::prelude::*; @@ -111,9 +111,9 @@ impl Default for Hsla { } } -impl Mix for Hsla { +impl Interpolate for Hsla { #[inline] - fn mix(&self, other: &Self, factor: f32) -> Self { + fn interp(&self, other: &Self, factor: f32) -> Self { let n_factor = 1.0 - factor; Self { hue: crate::color_ops::lerp_hue(self.hue, other.hue, factor), @@ -124,6 +124,9 @@ impl Mix for Hsla { } } +impl InterpolateStable for Hsla {} +impl InterpolateCurve for Hsla {} + impl Gray for Hsla { const BLACK: Self = Self::new(0., 0., 0., 1.); const WHITE: Self = Self::new(0., 0., 1., 1.); @@ -415,25 +418,25 @@ mod tests { } #[test] - fn test_mix_wrap() { + fn test_interp_wrap() { let hsla0 = Hsla::new(10., 0.5, 0.5, 1.0); let hsla1 = Hsla::new(20., 0.5, 0.5, 1.0); let hsla2 = Hsla::new(350., 0.5, 0.5, 1.0); - assert_approx_eq!(hsla0.mix(&hsla1, 0.25).hue, 12.5, 0.001); - assert_approx_eq!(hsla0.mix(&hsla1, 0.5).hue, 15., 0.001); - assert_approx_eq!(hsla0.mix(&hsla1, 0.75).hue, 17.5, 0.001); + assert_approx_eq!(hsla0.interp(&hsla1, 0.25).hue, 12.5, 0.001); + assert_approx_eq!(hsla0.interp(&hsla1, 0.5).hue, 15., 0.001); + assert_approx_eq!(hsla0.interp(&hsla1, 0.75).hue, 17.5, 0.001); - assert_approx_eq!(hsla1.mix(&hsla0, 0.25).hue, 17.5, 0.001); - assert_approx_eq!(hsla1.mix(&hsla0, 0.5).hue, 15., 0.001); - assert_approx_eq!(hsla1.mix(&hsla0, 0.75).hue, 12.5, 0.001); + assert_approx_eq!(hsla1.interp(&hsla0, 0.25).hue, 17.5, 0.001); + assert_approx_eq!(hsla1.interp(&hsla0, 0.5).hue, 15., 0.001); + assert_approx_eq!(hsla1.interp(&hsla0, 0.75).hue, 12.5, 0.001); - assert_approx_eq!(hsla0.mix(&hsla2, 0.25).hue, 5., 0.001); - assert_approx_eq!(hsla0.mix(&hsla2, 0.5).hue, 0., 0.001); - assert_approx_eq!(hsla0.mix(&hsla2, 0.75).hue, 355., 0.001); + assert_approx_eq!(hsla0.interp(&hsla2, 0.25).hue, 5., 0.001); + assert_approx_eq!(hsla0.interp(&hsla2, 0.5).hue, 0., 0.001); + assert_approx_eq!(hsla0.interp(&hsla2, 0.75).hue, 355., 0.001); - assert_approx_eq!(hsla2.mix(&hsla0, 0.25).hue, 355., 0.001); - assert_approx_eq!(hsla2.mix(&hsla0, 0.5).hue, 0., 0.001); - assert_approx_eq!(hsla2.mix(&hsla0, 0.75).hue, 5., 0.001); + assert_approx_eq!(hsla2.interp(&hsla0, 0.25).hue, 355., 0.001); + assert_approx_eq!(hsla2.interp(&hsla0, 0.5).hue, 0., 0.001); + assert_approx_eq!(hsla2.interp(&hsla0, 0.75).hue, 5., 0.001); } #[test] diff --git a/crates/bevy_color/src/hsva.rs b/crates/bevy_color/src/hsva.rs index 9e94eb24f672e..4d3d2461b5b88 100644 --- a/crates/bevy_color/src/hsva.rs +++ b/crates/bevy_color/src/hsva.rs @@ -1,8 +1,8 @@ use crate::{ - Alpha, ColorToComponents, Gray, Hue, Hwba, Lcha, LinearRgba, Mix, Saturation, Srgba, - StandardColor, Xyza, + Alpha, ColorToComponents, Gray, Hue, Hwba, Lcha, LinearRgba, Saturation, Srgba, StandardColor, + Xyza, }; -use bevy_math::{Vec3, Vec4}; +use bevy_math::{curve::InterpolateCurve, Interpolate, InterpolateStable, Vec3, Vec4}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::prelude::*; @@ -82,9 +82,9 @@ impl Default for Hsva { } } -impl Mix for Hsva { +impl Interpolate for Hsva { #[inline] - fn mix(&self, other: &Self, factor: f32) -> Self { + fn interp(&self, other: &Self, factor: f32) -> Self { let n_factor = 1.0 - factor; Self { hue: crate::color_ops::lerp_hue(self.hue, other.hue, factor), @@ -95,6 +95,9 @@ impl Mix for Hsva { } } +impl InterpolateStable for Hsva {} +impl InterpolateCurve for Hsva {} + impl Gray for Hsva { const BLACK: Self = Self::new(0., 0., 0., 1.); const WHITE: Self = Self::new(0., 0., 1., 1.); diff --git a/crates/bevy_color/src/hwba.rs b/crates/bevy_color/src/hwba.rs index 36d328658d575..9b4b2829288b0 100644 --- a/crates/bevy_color/src/hwba.rs +++ b/crates/bevy_color/src/hwba.rs @@ -2,10 +2,8 @@ //! in [_HWB - A More Intuitive Hue-Based Color Model_] by _Smith et al_. //! //! [_HWB - A More Intuitive Hue-Based Color Model_]: https://web.archive.org/web/20240226005220/http://alvyray.com/Papers/CG/HWB_JGTv208.pdf -use crate::{ - Alpha, ColorToComponents, Gray, Hue, Lcha, LinearRgba, Mix, Srgba, StandardColor, Xyza, -}; -use bevy_math::{ops, Vec3, Vec4}; +use crate::{Alpha, ColorToComponents, Gray, Hue, Lcha, LinearRgba, Srgba, StandardColor, Xyza}; +use bevy_math::{curve::InterpolateCurve, ops, Interpolate, InterpolateStable, Vec3, Vec4}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::prelude::*; @@ -85,9 +83,9 @@ impl Default for Hwba { } } -impl Mix for Hwba { +impl Interpolate for Hwba { #[inline] - fn mix(&self, other: &Self, factor: f32) -> Self { + fn interp(&self, other: &Self, factor: f32) -> Self { let n_factor = 1.0 - factor; Self { hue: crate::color_ops::lerp_hue(self.hue, other.hue, factor), @@ -98,6 +96,9 @@ impl Mix for Hwba { } } +impl InterpolateStable for Hwba {} +impl InterpolateCurve for Hwba {} + impl Gray for Hwba { const BLACK: Self = Self::new(0., 0., 1., 1.); const WHITE: Self = Self::new(0., 1., 0., 1.); diff --git a/crates/bevy_color/src/laba.rs b/crates/bevy_color/src/laba.rs index 010b3df249678..5b0fb34d43789 100644 --- a/crates/bevy_color/src/laba.rs +++ b/crates/bevy_color/src/laba.rs @@ -1,8 +1,8 @@ use crate::{ - impl_componentwise_vector_space, Alpha, ColorToComponents, Gray, Hsla, Hsva, Hwba, LinearRgba, - Luminance, Mix, Oklaba, Srgba, StandardColor, Xyza, + Alpha, ColorToComponents, Gray, Hsla, Hsva, Hwba, LinearRgba, Luminance, Oklaba, Srgba, + StandardColor, Xyza, }; -use bevy_math::{ops, Vec3, Vec4}; +use bevy_math::{curve::InterpolateCurve, ops, Interpolate, InterpolateStable, Vec3, Vec4}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::prelude::*; @@ -35,8 +35,6 @@ pub struct Laba { impl StandardColor for Laba {} -impl_componentwise_vector_space!(Laba, [lightness, a, b, alpha]); - impl Laba { /// Construct a new [`Laba`] color from components. /// @@ -93,9 +91,9 @@ impl Default for Laba { } } -impl Mix for Laba { +impl Interpolate for Laba { #[inline] - fn mix(&self, other: &Self, factor: f32) -> Self { + fn interp(&self, other: &Self, factor: f32) -> Self { let n_factor = 1.0 - factor; Self { lightness: self.lightness * n_factor + other.lightness * factor, @@ -106,6 +104,9 @@ impl Mix for Laba { } } +impl InterpolateStable for Laba {} +impl InterpolateCurve for Laba {} + impl Gray for Laba { const BLACK: Self = Self::new(0., 0., 0., 1.); const WHITE: Self = Self::new(1., 0., 0., 1.); diff --git a/crates/bevy_color/src/lcha.rs b/crates/bevy_color/src/lcha.rs index e5f5ecab32ea4..7d42b64f259f1 100644 --- a/crates/bevy_color/src/lcha.rs +++ b/crates/bevy_color/src/lcha.rs @@ -1,8 +1,7 @@ use crate::{ - Alpha, ColorToComponents, Gray, Hue, Laba, LinearRgba, Luminance, Mix, Srgba, StandardColor, - Xyza, + Alpha, ColorToComponents, Gray, Hue, Laba, LinearRgba, Luminance, Srgba, StandardColor, Xyza, }; -use bevy_math::{ops, Vec3, Vec4}; +use bevy_math::{curve::InterpolateCurve, ops, Interpolate, InterpolateStable, Vec3, Vec4}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::prelude::*; @@ -115,9 +114,9 @@ impl Default for Lcha { } } -impl Mix for Lcha { +impl Interpolate for Lcha { #[inline] - fn mix(&self, other: &Self, factor: f32) -> Self { + fn interp(&self, other: &Self, factor: f32) -> Self { let n_factor = 1.0 - factor; Self { lightness: self.lightness * n_factor + other.lightness * factor, @@ -128,6 +127,9 @@ impl Mix for Lcha { } } +impl InterpolateStable for Lcha {} +impl InterpolateCurve for Lcha {} + impl Gray for Lcha { const BLACK: Self = Self::new(0.0, 0.0, 0.0000136603785, 1.0); const WHITE: Self = Self::new(1.0, 0.0, 0.0000136603785, 1.0); diff --git a/crates/bevy_color/src/lib.rs b/crates/bevy_color/src/lib.rs index e1ee1fbe38cd0..90493ef87437b 100644 --- a/crates/bevy_color/src/lib.rs +++ b/crates/bevy_color/src/lib.rs @@ -76,7 +76,7 @@ //! allowing you to use them with splines. //! //! Please note that most often adding or subtracting colors is not what you may want. -//! Please have a look at other operations like blending, lightening or mixing colors using e.g. [`Mix`] or [`Luminance`] instead. +//! Please have a look at other operations like blending, lightening or mixing colors using e.g. [`bevy_math::Interpolate`] or [`Luminance`] instead. //! //! # Example //! @@ -173,99 +173,3 @@ where Self: Alpha, { } - -macro_rules! impl_componentwise_vector_space { - ($ty: ident, [$($element: ident),+]) => { - impl core::ops::Add for $ty { - type Output = Self; - - fn add(self, rhs: Self) -> Self::Output { - Self::Output { - $($element: self.$element + rhs.$element,)+ - } - } - } - - impl core::ops::AddAssign for $ty { - fn add_assign(&mut self, rhs: Self) { - *self = *self + rhs; - } - } - - impl core::ops::Neg for $ty { - type Output = Self; - - fn neg(self) -> Self::Output { - Self::Output { - $($element: -self.$element,)+ - } - } - } - - impl core::ops::Sub for $ty { - type Output = Self; - - fn sub(self, rhs: Self) -> Self::Output { - Self::Output { - $($element: self.$element - rhs.$element,)+ - } - } - } - - impl core::ops::SubAssign for $ty { - fn sub_assign(&mut self, rhs: Self) { - *self = *self - rhs; - } - } - - impl core::ops::Mul for $ty { - type Output = Self; - - fn mul(self, rhs: f32) -> Self::Output { - Self::Output { - $($element: self.$element * rhs,)+ - } - } - } - - impl core::ops::Mul<$ty> for f32 { - type Output = $ty; - - fn mul(self, rhs: $ty) -> Self::Output { - Self::Output { - $($element: self * rhs.$element,)+ - } - } - } - - impl core::ops::MulAssign for $ty { - fn mul_assign(&mut self, rhs: f32) { - *self = *self * rhs; - } - } - - impl core::ops::Div for $ty { - type Output = Self; - - fn div(self, rhs: f32) -> Self::Output { - Self::Output { - $($element: self.$element / rhs,)+ - } - } - } - - impl core::ops::DivAssign for $ty { - fn div_assign(&mut self, rhs: f32) { - *self = *self / rhs; - } - } - - impl bevy_math::VectorSpace for $ty { - const ZERO: Self = Self { - $($element: 0.0,)+ - }; - } - }; -} - -pub(crate) use impl_componentwise_vector_space; diff --git a/crates/bevy_color/src/linear_rgba.rs b/crates/bevy_color/src/linear_rgba.rs index d00d765aaccd0..d55ca2001962d 100644 --- a/crates/bevy_color/src/linear_rgba.rs +++ b/crates/bevy_color/src/linear_rgba.rs @@ -1,8 +1,8 @@ use crate::{ - color_difference::EuclideanDistance, impl_componentwise_vector_space, Alpha, ColorToComponents, - ColorToPacked, Gray, Luminance, Mix, StandardColor, + color_difference::EuclideanDistance, Alpha, ColorToComponents, ColorToPacked, Gray, Luminance, + StandardColor, }; -use bevy_math::{ops, Vec3, Vec4}; +use bevy_math::{curve::InterpolateCurve, ops, Interpolate, InterpolateStable, Vec3, Vec4}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::prelude::*; use bytemuck::{Pod, Zeroable}; @@ -37,8 +37,6 @@ pub struct LinearRgba { impl StandardColor for LinearRgba {} -impl_componentwise_vector_space!(LinearRgba, [red, green, blue, alpha]); - impl LinearRgba { /// A fully black color with full alpha. pub const BLACK: Self = Self { @@ -147,10 +145,10 @@ impl LinearRgba { let target_luminance = (luminance + amount).clamp(0.0, 1.0); if target_luminance < luminance { let adjustment = (luminance - target_luminance) / luminance; - self.mix_assign(Self::new(0.0, 0.0, 0.0, self.alpha), adjustment); + self.interp_assign(&Self::new(0.0, 0.0, 0.0, self.alpha), adjustment); } else if target_luminance > luminance { let adjustment = (target_luminance - luminance) / (1. - luminance); - self.mix_assign(Self::new(1.0, 1.0, 1.0, self.alpha), adjustment); + self.interp_assign(&Self::new(1.0, 1.0, 1.0, self.alpha), adjustment); } } @@ -161,6 +159,16 @@ impl LinearRgba { pub fn as_u32(&self) -> u32 { u32::from_le_bytes(self.to_u8_array()) } + + /// Scales the color components (not alpha) by the given factor. + pub fn scale_by(self, factor: f32) -> Self { + Self { + red: self.red * factor, + green: self.green * factor, + blue: self.green * factor, + alpha: self.alpha, + } + } } impl Default for LinearRgba { @@ -204,9 +212,9 @@ impl Luminance for LinearRgba { } } -impl Mix for LinearRgba { +impl Interpolate for LinearRgba { #[inline] - fn mix(&self, other: &Self, factor: f32) -> Self { + fn interp(&self, other: &Self, factor: f32) -> Self { let n_factor = 1.0 - factor; Self { red: self.red * n_factor + other.red * factor, @@ -217,6 +225,9 @@ impl Mix for LinearRgba { } } +impl InterpolateStable for LinearRgba {} +impl InterpolateCurve for LinearRgba {} + impl Gray for LinearRgba { const BLACK: Self = Self::BLACK; const WHITE: Self = Self::WHITE; diff --git a/crates/bevy_color/src/oklaba.rs b/crates/bevy_color/src/oklaba.rs index 0203ca6a695e8..7c71261ab91db 100644 --- a/crates/bevy_color/src/oklaba.rs +++ b/crates/bevy_color/src/oklaba.rs @@ -1,8 +1,10 @@ use crate::{ - color_difference::EuclideanDistance, impl_componentwise_vector_space, Alpha, ColorToComponents, - Gray, Hsla, Hsva, Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza, + color_difference::EuclideanDistance, Alpha, ColorToComponents, Gray, Hsla, Hsva, Hwba, Lcha, + LinearRgba, Luminance, Srgba, StandardColor, Xyza, +}; +use bevy_math::{ + curve::InterpolateCurve, ops, FloatPow, Interpolate, InterpolateStable, Vec3, Vec4, }; -use bevy_math::{ops, FloatPow, Vec3, Vec4}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::prelude::*; @@ -35,8 +37,6 @@ pub struct Oklaba { impl StandardColor for Oklaba {} -impl_componentwise_vector_space!(Oklaba, [lightness, a, b, alpha]); - impl Oklaba { /// Construct a new [`Oklaba`] color from components. /// @@ -93,9 +93,9 @@ impl Default for Oklaba { } } -impl Mix for Oklaba { +impl Interpolate for Oklaba { #[inline] - fn mix(&self, other: &Self, factor: f32) -> Self { + fn interp(&self, other: &Self, factor: f32) -> Self { let n_factor = 1.0 - factor; Self { lightness: self.lightness * n_factor + other.lightness * factor, @@ -106,6 +106,9 @@ impl Mix for Oklaba { } } +impl InterpolateStable for Oklaba {} +impl InterpolateCurve for Oklaba {} + impl Gray for Oklaba { const BLACK: Self = Self::new(0., 0., 0., 1.); const WHITE: Self = Self::new(1.0, 0.0, 0.000000059604645, 1.0); diff --git a/crates/bevy_color/src/oklcha.rs b/crates/bevy_color/src/oklcha.rs index 91ffe422c75c3..ac797aee794d7 100644 --- a/crates/bevy_color/src/oklcha.rs +++ b/crates/bevy_color/src/oklcha.rs @@ -1,8 +1,10 @@ use crate::{ color_difference::EuclideanDistance, Alpha, ColorToComponents, Gray, Hsla, Hsva, Hue, Hwba, - Laba, Lcha, LinearRgba, Luminance, Mix, Oklaba, Srgba, StandardColor, Xyza, + Laba, Lcha, LinearRgba, Luminance, Oklaba, Srgba, StandardColor, Xyza, +}; +use bevy_math::{ + curve::InterpolateCurve, ops, FloatPow, Interpolate, InterpolateStable, Vec3, Vec4, }; -use bevy_math::{ops, FloatPow, Vec3, Vec4}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::prelude::*; @@ -111,9 +113,9 @@ impl Default for Oklcha { } } -impl Mix for Oklcha { +impl Interpolate for Oklcha { #[inline] - fn mix(&self, other: &Self, factor: f32) -> Self { + fn interp(&self, other: &Self, factor: f32) -> Self { let n_factor = 1.0 - factor; Self { lightness: self.lightness * n_factor + other.lightness * factor, @@ -124,6 +126,9 @@ impl Mix for Oklcha { } } +impl InterpolateStable for Oklcha {} +impl InterpolateCurve for Oklcha {} + impl Gray for Oklcha { const BLACK: Self = Self::new(0., 0., 0., 1.); const WHITE: Self = Self::new(1.0, 0.000000059604645, 90.0, 1.0); diff --git a/crates/bevy_color/src/srgba.rs b/crates/bevy_color/src/srgba.rs index ead2adf03928f..3cc4e155b534d 100644 --- a/crates/bevy_color/src/srgba.rs +++ b/crates/bevy_color/src/srgba.rs @@ -1,10 +1,10 @@ use crate::{ - color_difference::EuclideanDistance, impl_componentwise_vector_space, Alpha, ColorToComponents, - ColorToPacked, Gray, LinearRgba, Luminance, Mix, StandardColor, Xyza, + color_difference::EuclideanDistance, Alpha, ColorToComponents, ColorToPacked, Gray, LinearRgba, + Luminance, StandardColor, Xyza, }; #[cfg(feature = "alloc")] use alloc::{format, string::String}; -use bevy_math::{ops, Vec3, Vec4}; +use bevy_math::{curve::InterpolateCurve, ops, Interpolate, InterpolateStable, Vec3, Vec4}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::prelude::*; use thiserror::Error; @@ -38,8 +38,6 @@ pub struct Srgba { impl StandardColor for Srgba {} -impl_componentwise_vector_space!(Srgba, [red, green, blue, alpha]); - impl Srgba { // The standard VGA colors, with alpha set to 1.0. // https://en.wikipedia.org/wiki/Web_colors#Basic_colors @@ -271,9 +269,9 @@ impl Luminance for Srgba { } } -impl Mix for Srgba { +impl Interpolate for Srgba { #[inline] - fn mix(&self, other: &Self, factor: f32) -> Self { + fn interp(&self, other: &Self, factor: f32) -> Self { let n_factor = 1.0 - factor; Self { red: self.red * n_factor + other.red * factor, @@ -311,9 +309,12 @@ impl EuclideanDistance for Srgba { } } +impl InterpolateStable for Srgba {} +impl InterpolateCurve for Srgba {} + impl Gray for Srgba { - const BLACK: Self = Self::BLACK; - const WHITE: Self = Self::WHITE; + const BLACK: Srgba = Srgba::new(0.0, 0.0, 0.0, 1.0); + const WHITE: Srgba = Srgba::new(1.0, 1.0, 1.0, 1.0); } impl ColorToComponents for Srgba { diff --git a/crates/bevy_color/src/xyza.rs b/crates/bevy_color/src/xyza.rs index c48a868416323..bea6d397c406f 100644 --- a/crates/bevy_color/src/xyza.rs +++ b/crates/bevy_color/src/xyza.rs @@ -1,8 +1,5 @@ -use crate::{ - impl_componentwise_vector_space, Alpha, ColorToComponents, Gray, LinearRgba, Luminance, Mix, - StandardColor, -}; -use bevy_math::{Vec3, Vec4}; +use crate::{Alpha, ColorToComponents, Gray, LinearRgba, Luminance, StandardColor}; +use bevy_math::{curve::InterpolateCurve, Interpolate, InterpolateStable, Vec3, Vec4}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::prelude::*; @@ -35,8 +32,6 @@ pub struct Xyza { impl StandardColor for Xyza {} -impl_componentwise_vector_space!(Xyza, [x, y, z, alpha]); - impl Xyza { /// Construct a new [`Xyza`] color from components. /// @@ -136,9 +131,9 @@ impl Luminance for Xyza { } } -impl Mix for Xyza { +impl Interpolate for Xyza { #[inline] - fn mix(&self, other: &Self, factor: f32) -> Self { + fn interp(&self, other: &Self, factor: f32) -> Self { let n_factor = 1.0 - factor; Self { x: self.x * n_factor + other.x * factor, @@ -149,6 +144,9 @@ impl Mix for Xyza { } } +impl InterpolateStable for Xyza {} +impl InterpolateCurve for Xyza {} + impl Gray for Xyza { const BLACK: Self = Self::new(0., 0., 0., 1.); const WHITE: Self = Self::new(0.95047, 1.0, 1.08883, 1.0); diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index 882626af881da..26c04ed457e20 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -12,7 +12,7 @@ use bevy_asset::{ io::Reader, AssetLoadError, AssetLoader, Handle, LoadContext, ReadAssetBytesError, RenderAssetUsages, }; -use bevy_color::{Color, LinearRgba}; +use bevy_color::{Color, ColorToComponents, LinearRgba}; use bevy_core_pipeline::prelude::Camera3d; use bevy_ecs::{ entity::{Entity, EntityHashMap}, @@ -293,7 +293,7 @@ async fn load_gltf<'a, 'b, 'c>( let translation_property = animated_field!(Transform::translation); let translations: Vec = tr.map(Vec3::from).collect(); if keyframe_timestamps.len() == 1 { - Some(VariableCurve::new(AnimatableCurve::new( + Some(VariableCurve::new(PropertyCurve::new( translation_property, ConstantCurve::new(Interval::EVERYWHERE, translations[0]), ))) @@ -305,7 +305,7 @@ async fn load_gltf<'a, 'b, 'c>( ) .ok() .map(|curve| { - VariableCurve::new(AnimatableCurve::new( + VariableCurve::new(PropertyCurve::new( translation_property, curve, )) @@ -317,7 +317,7 @@ async fn load_gltf<'a, 'b, 'c>( ) .ok() .map(|curve| { - VariableCurve::new(AnimatableCurve::new( + VariableCurve::new(PropertyCurve::new( translation_property, curve, )) @@ -327,7 +327,7 @@ async fn load_gltf<'a, 'b, 'c>( CubicKeyframeCurve::new(keyframe_timestamps, translations) .ok() .map(|curve| { - VariableCurve::new(AnimatableCurve::new( + VariableCurve::new(PropertyCurve::new( translation_property, curve, )) @@ -341,7 +341,7 @@ async fn load_gltf<'a, 'b, 'c>( let rotations: Vec = rots.into_f32().map(Quat::from_array).collect(); if keyframe_timestamps.len() == 1 { - Some(VariableCurve::new(AnimatableCurve::new( + Some(VariableCurve::new(PropertyCurve::new( rotation_property, ConstantCurve::new(Interval::EVERYWHERE, rotations[0]), ))) @@ -353,7 +353,7 @@ async fn load_gltf<'a, 'b, 'c>( ) .ok() .map(|curve| { - VariableCurve::new(AnimatableCurve::new( + VariableCurve::new(PropertyCurve::new( rotation_property, curve, )) @@ -365,7 +365,7 @@ async fn load_gltf<'a, 'b, 'c>( ) .ok() .map(|curve| { - VariableCurve::new(AnimatableCurve::new( + VariableCurve::new(PropertyCurve::new( rotation_property, curve, )) @@ -378,7 +378,7 @@ async fn load_gltf<'a, 'b, 'c>( ) .ok() .map(|curve| { - VariableCurve::new(AnimatableCurve::new( + VariableCurve::new(PropertyCurve::new( rotation_property, curve, )) @@ -391,7 +391,7 @@ async fn load_gltf<'a, 'b, 'c>( let scale_property = animated_field!(Transform::scale); let scales: Vec = scale.map(Vec3::from).collect(); if keyframe_timestamps.len() == 1 { - Some(VariableCurve::new(AnimatableCurve::new( + Some(VariableCurve::new(PropertyCurve::new( scale_property, ConstantCurve::new(Interval::EVERYWHERE, scales[0]), ))) @@ -403,7 +403,7 @@ async fn load_gltf<'a, 'b, 'c>( ) .ok() .map(|curve| { - VariableCurve::new(AnimatableCurve::new( + VariableCurve::new(PropertyCurve::new( scale_property, curve, )) @@ -415,7 +415,7 @@ async fn load_gltf<'a, 'b, 'c>( ) .ok() .map(|curve| { - VariableCurve::new(AnimatableCurve::new( + VariableCurve::new(PropertyCurve::new( scale_property, curve, )) @@ -425,7 +425,7 @@ async fn load_gltf<'a, 'b, 'c>( CubicKeyframeCurve::new(keyframe_timestamps, scales) .ok() .map(|curve| { - VariableCurve::new(AnimatableCurve::new( + VariableCurve::new(PropertyCurve::new( scale_property, curve, )) @@ -1165,8 +1165,8 @@ fn load_material( SpecularExtension::parse(load_context, document, material).unwrap_or_default(); // We need to operate in the Linear color space and be willing to exceed 1.0 in our channels - let base_emissive = LinearRgba::rgb(emissive[0], emissive[1], emissive[2]); - let emissive = base_emissive * material.emissive_strength().unwrap_or(1.0); + let emissive_strength = material.emissive_strength().unwrap_or(1.0); + let emissive = LinearRgba::from_vec3(Vec3::from_array(emissive) * emissive_strength); StandardMaterial { base_color: Color::linear_rgba(color[0], color[1], color[2], color[3]), diff --git a/crates/bevy_math/src/common_traits.rs b/crates/bevy_math/src/common_traits.rs index a9a8ef910a86e..9794e526d9451 100644 --- a/crates/bevy_math/src/common_traits.rs +++ b/crates/bevy_math/src/common_traits.rs @@ -1,12 +1,137 @@ //! This module contains abstract mathematical traits shared by types used in `bevy_math`. -use crate::{ops, Dir2, Dir3, Dir3A, Quat, Rot2, Vec2, Vec3, Vec3A, Vec4}; +use crate::{ops, Dir2, Dir3, Dir3A, Isometry2d, Isometry3d, Quat, Rot2, Vec2, Vec3, Vec3A, Vec4}; use core::{ fmt::Debug, ops::{Add, Div, Mul, Neg, Sub}, }; use variadics_please::all_tuples_enumerated; +/// A type with a natural method of smooth interpolation. This is intended to be the "nicest" or form of +/// interpolation available for a given type, and defines the interpolation method used by the animation +/// system. It may not necessarily be the fastest form of interpolation available. +/// +/// Interpolation is a fairly fluid concept, so to make things a little more predictable we require the +/// following rules to hold: +/// +/// 1. The notion of interpolation should follow naturally from the semantics of the type, so +/// that inferring the interpolation mode from the type alone is sensible. +/// +/// 2. The path traced by interpolating between two points should be continuous and smooth, +/// as far as the limits of floating-point arithmetic permit. This trait should not be +/// implemented for types that don't have an apparent notion of continuity (like `bool`). +/// +/// 3. Interpolation should recover something equivalent to the starting value at `t = 0.0` +/// and likewise with the ending value at `t = 1.0`. They do not have to be data-identical, but +/// they should be semantically identical. For example, [`Quat::slerp`] doesn't always yield its +/// second rotation input exactly at `t = 1.0`, but it always returns an equivalent rotation +/// (this trait is implemented for `Quat` using `slerp`). +/// +/// 4. Interpolation should be the same forward and backwards. That is, `interp(a, b, t)` should +/// be equivalent to `interp(b, a, t - 1)`. +/// +/// 5. Interpolating from a value to itself `interp(a, a, t)` should always return values equivalent +/// to the original value `a`. +/// +/// +/// We make no guarantees about the behavior of `interp` for values outside of the interval `[0, 1]`. +/// Other sub-traits (such as [`InterpolateStable`] or [`VectorSpace`]) may add additional guarantees, +/// such as linearity. +pub trait Interpolate: Sized { + /// Smoothly interpolates between two values. There are often many ways to interpolate for a given + /// type, but this method always represents a sane default interpolation method. + /// + /// Other sub-traits may add stronger properties to this method: + /// - For types that implement `VectorSpace`, this is linear interpolation. + /// - For types that implement `InterpolateStable`, the interpolation is stable under resampling. + fn interp(&self, other: &Self, param: f32) -> Self; + + /// Performs interpolation in place. See the documentation on the [`interp`] method for more info. + /// + /// [`interp`]: Interpolate::interp + #[inline] + fn interp_assign(&mut self, other: &Self, param: f32) { + *self = self.interp(other, param); + } +} + +macro_rules! impl_interpolate_tuple { + ($(#[$meta:meta])* $(($n:tt, $T:ident)),*) => { + $(#[$meta])* + impl<$($T: Interpolate),*> Interpolate for ($($T,)*) { + #[inline] + fn interp(&self, other: &Self, param: f32) -> Self { + ( + $( + <$T as Interpolate>::interp(&self.$n, &other.$n, param), + )* + ) + } + } + }; +} + +all_tuples_enumerated!( + #[doc(fake_variadic)] + impl_interpolate_tuple, + 1, + 11, + T +); + +impl Interpolate for Rot2 { + #[inline] + fn interp(&self, other: &Self, param: f32) -> Self { + self.slerp(*other, param) + } +} + +impl Interpolate for Quat { + #[inline] + fn interp(&self, other: &Self, param: f32) -> Self { + self.slerp(*other, param) + } +} + +impl Interpolate for Dir2 { + #[inline] + fn interp(&self, other: &Self, param: f32) -> Self { + self.slerp(*other, param) + } +} + +impl Interpolate for Dir3 { + #[inline] + fn interp(&self, other: &Self, param: f32) -> Self { + self.slerp(*other, param) + } +} + +impl Interpolate for Dir3A { + #[inline] + fn interp(&self, other: &Self, param: f32) -> Self { + self.slerp(*other, param) + } +} + +impl Interpolate for Isometry2d { + fn interp(&self, other: &Self, param: f32) -> Self { + Isometry2d { + rotation: self.rotation.interp(&other.rotation, param), + translation: self.translation.interp(&other.translation, param), + } + } +} + +impl Interpolate for Isometry3d { + fn interp(&self, other: &Self, param: f32) -> Self { + Isometry3d { + rotation: self.rotation.interp(&other.rotation, param), + translation: self.translation.interp(&other.translation, param), + } + } +} + /// A type that supports the mathematical operations of a real vector space, irrespective of dimension. /// In particular, this means that the implementing type supports: /// - Scalar multiplication and division on the right by elements of `f32` @@ -26,8 +151,11 @@ use variadics_please::all_tuples_enumerated; /// /// Note that, because implementing types use floating point arithmetic, they are not required to actually /// implement `PartialEq` or `Eq`. +/// +/// Also note that all vector spaces implement [`Interpolate`] with linear interpolation via a blanket-impl. pub trait VectorSpace: - Mul + Interpolate + + Mul + Div + Add + Sub @@ -39,16 +167,18 @@ pub trait VectorSpace: { /// The zero vector, which is the identity of addition for the vector space type. const ZERO: Self; +} - /// Perform vector space linear interpolation between this element and another, based - /// on the parameter `t`. When `t` is `0`, `self` is recovered. When `t` is `1`, `rhs` - /// is recovered. - /// - /// Note that the value of `t` is not clamped by this function, so extrapolating outside - /// of the interval `[0,1]` is allowed. +// Equip all vector spaces with linear interpolation. This will conflict with other implementations of +// interpolation for vector spaces; that's intentional, linear interpolation is the only sane default +// for a vector-space. +impl Interpolate for V +where + V: VectorSpace, +{ #[inline] - fn lerp(self, rhs: Self, t: f32) -> Self { - self * (1. - t) + rhs * t + fn interp(&self, other: &Self, param: f32) -> Self { + *self * (1. - param) + *other * param } } @@ -156,7 +286,7 @@ where } /// A type that supports the operations of a normed vector space; i.e. a norm operation in addition -/// to those of [`VectorSpace`]. Specifically, the implementor must guarantee that the following +/// to those of [`VectorSpace`]. Specifically, the implementer must guarantee that the following /// relationships hold, within the limitations of floating point arithmetic: /// - (Nonnegativity) For all `v: Self`, `v.norm() >= 0.0`. /// - (Positive definiteness) For all `v: Self`, `v.norm() == 0.0` implies `v == Self::ZERO`. @@ -250,30 +380,20 @@ impl NormedVectorSpace for f32 { } } -/// A type with a natural interpolation that provides strong subdivision guarantees. +/// This trait extends [`Interpolate`] with strong subdivision guarantees. /// -/// Although the only required method is `interpolate_stable`, many things are expected of it: +/// The interpolation (`Interpolate::interp`) must be *subdivision-stable*: for any interpolation curve +/// between two (unnamed) values and any parameter-value pairs `(t0, p)` and `(t1, q)`, the +/// interpolation curve between `p` and `q` must be the *linear* reparameterization of the original +/// interpolation curve restricted to the interval `[t0, t1]`. /// -/// 1. The notion of interpolation should follow naturally from the semantics of the type, so -/// that inferring the interpolation mode from the type alone is sensible. -/// -/// 2. The interpolation recovers something equivalent to the starting value at `t = 0.0` -/// and likewise with the ending value at `t = 1.0`. They do not have to be data-identical, but -/// they should be semantically identical. For example, [`Quat::slerp`] doesn't always yield its -/// second rotation input exactly at `t = 1.0`, but it always returns an equivalent rotation. -/// -/// 3. Importantly, the interpolation must be *subdivision-stable*: for any interpolation curve -/// between two (unnamed) values and any parameter-value pairs `(t0, p)` and `(t1, q)`, the -/// interpolation curve between `p` and `q` must be the *linear* reparameterization of the original -/// interpolation curve restricted to the interval `[t0, t1]`. -/// -/// The last of these conditions is very strong and indicates something like constant speed. It -/// is called "subdivision stability" because it guarantees that breaking up the interpolation -/// into segments and joining them back together has no effect. +/// This condition is very strong, and indicates something like constant speed. It is called +/// "subdivision stability" because it guarantees that breaking up the interpolation into segments and +/// joining them back together has no effect. /// /// Here is a diagram depicting it: /// ```text -/// top curve = u.interpolate_stable(v, t) +/// top curve = T::interp(u, v, t) /// /// t0 => p t1 => q /// |-------------|---------|-------------| @@ -287,35 +407,16 @@ impl NormedVectorSpace for f32 { /// |-------------------------------------| /// 0 => p 1 => q /// -/// bottom curve = p.interpolate_stable(q, s) +/// bottom curve = T::interp(p, q, s) /// ``` /// /// Note that some common forms of interpolation do not satisfy this criterion. For example, /// [`Quat::lerp`] and [`Rot2::nlerp`] are not subdivision-stable. /// -/// Furthermore, this is not to be used as a general trait for abstract interpolation. -/// Consumers rely on the strong guarantees in order for behavior based on this trait to be -/// well-behaved. -/// /// [`Quat::slerp`]: crate::Quat::slerp /// [`Quat::lerp`]: crate::Quat::lerp /// [`Rot2::nlerp`]: crate::Rot2::nlerp -pub trait StableInterpolate: Clone { - /// Interpolate between this value and the `other` given value using the parameter `t`. At - /// `t = 0.0`, a value equivalent to `self` is recovered, while `t = 1.0` recovers a value - /// equivalent to `other`, with intermediate values interpolating between the two. - /// See the [trait-level documentation] for details. - /// - /// [trait-level documentation]: StableInterpolate - fn interpolate_stable(&self, other: &Self, t: f32) -> Self; - - /// A version of [`interpolate_stable`] that assigns the result to `self` for convenience. - /// - /// [`interpolate_stable`]: StableInterpolate::interpolate_stable - fn interpolate_stable_assign(&mut self, other: &Self, t: f32) { - *self = self.interpolate_stable(other, t); - } - +pub trait InterpolateStable: Interpolate { /// Smoothly nudge this value towards the `target` at a given decay rate. The `decay_rate` /// parameter controls how fast the distance between `self` and `target` decays relative to /// the units of `delta`; the intended usage is for `decay_rate` to generally remain fixed, @@ -332,7 +433,7 @@ pub trait StableInterpolate: Clone { /// /// # Example /// ``` - /// # use bevy_math::{Vec3, StableInterpolate}; + /// # use bevy_math::{Vec3, InterpolateStable}; /// # let delta_time: f32 = 1.0 / 60.0; /// let mut object_position: Vec3 = Vec3::ZERO; /// let target_position: Vec3 = Vec3::new(2.0, 3.0, 5.0); @@ -341,71 +442,35 @@ pub trait StableInterpolate: Clone { /// // Calling this repeatedly will move `object_position` towards `target_position`: /// object_position.smooth_nudge(&target_position, decay_rate, delta_time); /// ``` + #[inline] fn smooth_nudge(&mut self, target: &Self, decay_rate: f32, delta: f32) { - self.interpolate_stable_assign(target, 1.0 - ops::exp(-decay_rate * delta)); + self.interp_assign(target, 1.0 - ops::exp(-decay_rate * delta)); } } // Conservatively, we presently only apply this for normed vector spaces, where the notion // of being constant-speed is literally true. The technical axioms are satisfied for any // VectorSpace type, but the "natural from the semantics" part is less clear in general. -impl StableInterpolate for V -where - V: NormedVectorSpace, -{ - #[inline] - fn interpolate_stable(&self, other: &Self, t: f32) -> Self { - self.lerp(*other, t) - } -} +impl InterpolateStable for V where V: NormedVectorSpace {} -impl StableInterpolate for Rot2 { - #[inline] - fn interpolate_stable(&self, other: &Self, t: f32) -> Self { - self.slerp(*other, t) - } -} +impl InterpolateStable for Rot2 {} -impl StableInterpolate for Quat { - #[inline] - fn interpolate_stable(&self, other: &Self, t: f32) -> Self { - self.slerp(*other, t) - } -} +impl InterpolateStable for Quat {} -impl StableInterpolate for Dir2 { - #[inline] - fn interpolate_stable(&self, other: &Self, t: f32) -> Self { - self.slerp(*other, t) - } -} +impl InterpolateStable for Dir2 {} -impl StableInterpolate for Dir3 { - #[inline] - fn interpolate_stable(&self, other: &Self, t: f32) -> Self { - self.slerp(*other, t) - } -} +impl InterpolateStable for Dir3 {} -impl StableInterpolate for Dir3A { - #[inline] - fn interpolate_stable(&self, other: &Self, t: f32) -> Self { - self.slerp(*other, t) - } -} +impl InterpolateStable for Dir3A {} + +impl InterpolateStable for Isometry2d {} + +impl InterpolateStable for Isometry3d {} macro_rules! impl_stable_interpolate_tuple { ($(#[$meta:meta])* $(($n:tt, $T:ident)),*) => { $(#[$meta])* - impl<$($T: StableInterpolate),*> StableInterpolate for ($($T,)*) { - fn interpolate_stable(&self, other: &Self, t: f32) -> Self { - ( - $( - <$T as StableInterpolate>::interpolate_stable(&self.$n, &other.$n, t), - )* - ) - } - } + impl<$($T: InterpolateStable),*> InterpolateStable for ($($T,)*) {} }; } diff --git a/crates/bevy_math/src/curve/easing.rs b/crates/bevy_math/src/curve/easing.rs index c0b452e001b86..6085ca6ee8340 100644 --- a/crates/bevy_math/src/curve/easing.rs +++ b/crates/bevy_math/src/curve/easing.rs @@ -5,7 +5,7 @@ use crate::{ curve::{Curve, CurveExt, FunctionCurve, Interval}, - Dir2, Dir3, Dir3A, Isometry2d, Isometry3d, Quat, Rot2, VectorSpace, + Dir2, Dir3, Dir3A, InterpolateStable, Isometry2d, Isometry3d, NormedVectorSpace, Quat, Rot2, }; #[cfg(feature = "bevy_reflect")] @@ -13,39 +13,38 @@ use bevy_reflect::std_traits::ReflectDefault; use variadics_please::all_tuples_enumerated; -// TODO: Think about merging `Ease` with `StableInterpolate` - -/// A type whose values can be eased between. +/// Provides a curve version of [`StableInterpolate`] which extends beyond the curve +/// segment connecting the two values. The purpose of this trait is to make it easy to +/// work with easing curves, which often extrapolate values before the starting point or +/// after the end point. +/// +/// This trait requires: +/// +/// 1. The curve must follow all the rules of [`Interpolate`] and [`InterpolateStable`]. +/// +/// 2. The curve must extend smoothly beyond the start and end points, so that it can be +/// used for extrapolation. /// -/// This requires the construction of an interpolation curve that actually extends -/// beyond the curve segment that connects two values, because an easing curve may -/// extrapolate before the starting value and after the ending value. This is -/// especially common in easing functions that mimic elastic or springlike behavior. -pub trait Ease: Sized { - /// Given `start` and `end` values, produce a curve with [unlimited domain] - /// that: - /// - takes a value equivalent to `start` at `t = 0` - /// - takes a value equivalent to `end` at `t = 1` - /// - has constant speed everywhere, including outside of `[0, 1]` +/// [`Interpolate`]: crate::Interpolate +/// +pub trait InterpolateCurve: InterpolateStable { + /// Given `start` and `end` values, produce a smooth curve with [unlimited domain] + /// that moves between the start and end points over the interval `[0, 1]` and has + /// constant velocity. /// /// [unlimited domain]: Interval::EVERYWHERE - fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve; -} - -impl Ease for V { - fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve { - FunctionCurve::new(Interval::EVERYWHERE, move |t| V::lerp(start, end, t)) + fn interp_curve(start: Self, end: Self) -> impl Curve { + FunctionCurve::new(Interval::EVERYWHERE, move |t| Self::interp(&start, &end, t)) } } -impl Ease for Rot2 { - fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve { - FunctionCurve::new(Interval::EVERYWHERE, move |t| Rot2::slerp(start, end, t)) - } -} +// Implement ease for all normed vector spaces. +impl InterpolateCurve for V {} -impl Ease for Quat { - fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve { +impl InterpolateCurve for Rot2 {} + +impl InterpolateCurve for Quat { + fn interp_curve(start: Self, end: Self) -> impl Curve { let dot = start.dot(end); let end_adjusted = if dot < 0.0 { -end } else { end }; let difference = end_adjusted * start.inverse(); @@ -56,58 +55,46 @@ impl Ease for Quat { } } -impl Ease for Dir2 { - fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve { - FunctionCurve::new(Interval::EVERYWHERE, move |t| Dir2::slerp(start, end, t)) - } -} +impl InterpolateCurve for Dir2 {} -impl Ease for Dir3 { - fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve { +impl InterpolateCurve for Dir3 { + fn interp_curve(start: Self, end: Self) -> impl Curve { let difference_quat = Quat::from_rotation_arc(start.as_vec3(), end.as_vec3()); - Quat::interpolating_curve_unbounded(Quat::IDENTITY, difference_quat).map(move |q| q * start) + Quat::interp_curve(Quat::IDENTITY, difference_quat).map(move |q| q * start) } } -impl Ease for Dir3A { - fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve { +impl InterpolateCurve for Dir3A { + fn interp_curve(start: Self, end: Self) -> impl Curve { let difference_quat = Quat::from_rotation_arc(start.as_vec3a().into(), end.as_vec3a().into()); - Quat::interpolating_curve_unbounded(Quat::IDENTITY, difference_quat).map(move |q| q * start) + Quat::interp_curve(Quat::IDENTITY, difference_quat).map(move |q| q * start) } } -impl Ease for Isometry3d { - fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve { +impl InterpolateCurve for Isometry3d { + fn interp_curve(start: Self, end: Self) -> impl Curve { FunctionCurve::new(Interval::EVERYWHERE, move |t| { // we can use sample_unchecked here, since both interpolating_curve_unbounded impls // used are defined on the whole domain Isometry3d { - rotation: Quat::interpolating_curve_unbounded(start.rotation, end.rotation) + rotation: Quat::interp_curve(start.rotation, end.rotation).sample_unchecked(t), + translation: crate::Vec3A::interp_curve(start.translation, end.translation) .sample_unchecked(t), - translation: crate::Vec3A::interpolating_curve_unbounded( - start.translation, - end.translation, - ) - .sample_unchecked(t), } }) } } -impl Ease for Isometry2d { - fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve { +impl InterpolateCurve for Isometry2d { + fn interp_curve(start: Self, end: Self) -> impl Curve { FunctionCurve::new(Interval::EVERYWHERE, move |t| { // we can use sample_unchecked here, since both interpolating_curve_unbounded impls // used are defined on the whole domain Isometry2d { - rotation: Rot2::interpolating_curve_unbounded(start.rotation, end.rotation) + rotation: Rot2::interp_curve(start.rotation, end.rotation).sample_unchecked(t), + translation: crate::Vec2::interp_curve(start.translation, end.translation) .sample_unchecked(t), - translation: crate::Vec2::interpolating_curve_unbounded( - start.translation, - end.translation, - ) - .sample_unchecked(t), } }) } @@ -116,12 +103,12 @@ impl Ease for Isometry2d { macro_rules! impl_ease_tuple { ($(#[$meta:meta])* $(($n:tt, $T:ident)),*) => { $(#[$meta])* - impl<$($T: Ease),*> Ease for ($($T,)*) { - fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve { + impl<$($T: InterpolateCurve),*> InterpolateCurve for ($($T,)*) { + fn interp_curve(start: Self, end: Self) -> impl Curve { let curve_tuple = ( $( - <$T as Ease>::interpolating_curve_unbounded(start.$n, end.$n), + <$T as InterpolateCurve>::interp_curve(start.$n, end.$n), )* ); @@ -188,7 +175,7 @@ all_tuples_enumerated!( /// assert_eq!(c.sample_clamped(2.0), 4.0); /// ``` /// -/// `EasingCurve` can be used with any type that implements the [`Ease`] trait. +/// `EasingCurve` can be used with any type that implements the [`InterpolateCurve`] trait. /// This includes many math types, like vectors and rotations. /// /// ``` @@ -257,7 +244,7 @@ impl EasingCurve { impl Curve for EasingCurve where - T: Ease + Clone, + T: InterpolateCurve + Clone, { #[inline] fn domain(&self) -> Interval { @@ -267,8 +254,7 @@ where #[inline] fn sample_unchecked(&self, t: f32) -> T { let remapped_t = self.ease_fn.eval(t); - T::interpolating_curve_unbounded(self.start.clone(), self.end.clone()) - .sample_unchecked(remapped_t) + T::interp_curve(self.start.clone(), self.end.clone()).sample_unchecked(remapped_t) } } @@ -1025,7 +1011,7 @@ mod tests { let quat_start = Quat::from_axis_angle(Vec3::Z, 0.0); let quat_end = Quat::from_axis_angle(Vec3::Z, 90.0_f32.to_radians()); - let quat_curve = Quat::interpolating_curve_unbounded(quat_start, quat_end); + let quat_curve = Quat::interp_curve(quat_start, quat_end); assert_abs_diff_eq!( quat_curve.sample(0.0).unwrap(), @@ -1060,7 +1046,7 @@ mod tests { let iso_2d_start = Isometry2d::new(Vec2::ZERO, Rot2::degrees(0.0)); let iso_2d_end = Isometry2d::new(Vec2::ONE, Rot2::degrees(angle)); - let iso_2d_curve = Isometry2d::interpolating_curve_unbounded(iso_2d_start, iso_2d_end); + let iso_2d_curve = Isometry2d::interp_curve(iso_2d_start, iso_2d_end); [-1.0, 0.0, 0.5, 1.0, 2.0].into_iter().for_each(|t| { assert_abs_diff_eq!( @@ -1076,7 +1062,7 @@ mod tests { let iso_3d_start = Isometry3d::new(Vec3A::ZERO, Quat::from_axis_angle(Vec3::Z, 0.0)); let iso_3d_end = Isometry3d::new(Vec3A::ONE, Quat::from_axis_angle(Vec3::Z, angle)); - let iso_3d_curve = Isometry3d::interpolating_curve_unbounded(iso_3d_start, iso_3d_end); + let iso_3d_curve = Isometry3d::interp_curve(iso_3d_start, iso_3d_end); [-1.0, 0.0, 0.5, 1.0, 2.0].into_iter().for_each(|t| { assert_abs_diff_eq!( diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 94e7b0151e226..400e5d5890a49 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -312,7 +312,7 @@ use interval::InvalidIntervalError; use thiserror::Error; #[cfg(feature = "alloc")] -use {crate::StableInterpolate, itertools::Itertools}; +use {crate::InterpolateStable, itertools::Itertools}; /// A trait for a type that can represent values of type `T` parametrized over a fixed interval. /// @@ -771,7 +771,7 @@ impl CurveExt for C where C: Curve {} /// For more information, see the [module-level documentation]. /// /// [curves]: Curve -/// [stable interpolation]: crate::StableInterpolate +/// [stable interpolation]: crate::InterpolateStable /// [module-level documentation]: self #[cfg(feature = "alloc")] pub trait CurveResampleExt: Curve { @@ -826,10 +826,10 @@ pub trait CurveResampleExt: Curve { /// /// If `segments` is zero or if this curve has unbounded domain, a [`ResamplingError`] is returned. /// - /// [automatic interpolation]: crate::common_traits::StableInterpolate + /// [automatic interpolation]: crate::common_traits::InterpolateStable fn resample_auto(&self, segments: usize) -> Result, ResamplingError> where - T: StableInterpolate, + T: InterpolateStable, { let samples = self.samples(segments + 1)?.collect_vec(); Ok(SampleAutoCurve { @@ -901,13 +901,13 @@ pub trait CurveResampleExt: Curve { /// If `sample_times` doesn't contain at least two distinct times after filtering, a /// [`ResamplingError`] is returned. /// - /// [automatic interpolation]: crate::common_traits::StableInterpolate + /// [automatic interpolation]: crate::common_traits::InterpolateStable fn resample_uneven_auto( &self, sample_times: impl IntoIterator, ) -> Result, ResamplingError> where - T: StableInterpolate, + T: InterpolateStable, { let domain = self.domain(); let mut times = sample_times diff --git a/crates/bevy_math/src/curve/sample_curves.rs b/crates/bevy_math/src/curve/sample_curves.rs index f0fa928abba57..88f12b880424b 100644 --- a/crates/bevy_math/src/curve/sample_curves.rs +++ b/crates/bevy_math/src/curve/sample_curves.rs @@ -1,9 +1,10 @@ //! Sample-interpolated curves constructed using the [`Curve`] API. +use crate::{Interpolate, InterpolateStable}; + use super::cores::{EvenCore, EvenCoreError, UnevenCore, UnevenCoreError}; use super::{Curve, Interval}; -use crate::StableInterpolate; #[cfg(feature = "bevy_reflect")] use alloc::format; use core::any::type_name; @@ -133,7 +134,7 @@ impl SampleCurve { /// A curve that is defined by neighbor interpolation over a set of evenly-spaced samples, /// interpolated automatically using [a particularly well-behaved interpolation]. /// -/// [a particularly well-behaved interpolation]: StableInterpolate +/// [a particularly well-behaved interpolation]: InterpolateStable #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] @@ -143,7 +144,7 @@ pub struct SampleAutoCurve { impl Curve for SampleAutoCurve where - T: StableInterpolate, + T: InterpolateStable + Clone, { #[inline] fn domain(&self) -> Interval { @@ -153,8 +154,7 @@ where #[inline] fn sample_clamped(&self, t: f32) -> T { // `EvenCore::sample_with` is implicitly clamped. - self.core - .sample_with(t, ::interpolate_stable) + self.core.sample_with(t, ::interp) } #[inline] @@ -305,7 +305,7 @@ impl UnevenSampleCurve { /// A curve that is defined by interpolation over unevenly spaced samples, /// interpolated automatically using [a particularly well-behaved interpolation]. /// -/// [a particularly well-behaved interpolation]: StableInterpolate +/// [a particularly well-behaved interpolation]: InterpolateStable #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] @@ -315,7 +315,7 @@ pub struct UnevenSampleAutoCurve { impl Curve for UnevenSampleAutoCurve where - T: StableInterpolate, + T: InterpolateStable + Clone, { #[inline] fn domain(&self) -> Interval { @@ -325,8 +325,7 @@ where #[inline] fn sample_clamped(&self, t: f32) -> T { // `UnevenCore::sample_with` is implicitly clamped. - self.core - .sample_with(t, ::interpolate_stable) + self.core.sample_with(t, ::interp) } #[inline] @@ -369,17 +368,17 @@ mod tests { //! - function items //! - 'static closures //! - function pointers - use super::{SampleCurve, UnevenSampleCurve}; - use crate::{curve::Interval, VectorSpace}; + use super::{Interpolate, SampleCurve, UnevenSampleCurve}; + use crate::curve::Interval; use alloc::boxed::Box; use bevy_reflect::Reflect; #[test] fn reflect_sample_curve() { fn foo(x: &f32, y: &f32, t: f32) -> f32 { - x.lerp(*y, t) + x.interp(y, t) } - let bar = |x: &f32, y: &f32, t: f32| x.lerp(*y, t); + let bar = |x: &f32, y: &f32, t: f32| x.interp(y, t); let baz: fn(&f32, &f32, f32) -> f32 = bar; let samples = [0.0, 1.0, 2.0]; @@ -392,9 +391,9 @@ mod tests { #[test] fn reflect_uneven_sample_curve() { fn foo(x: &f32, y: &f32, t: f32) -> f32 { - x.lerp(*y, t) + x.interp(y, t) } - let bar = |x: &f32, y: &f32, t: f32| x.lerp(*y, t); + let bar = |x: &f32, y: &f32, t: f32| x.interp(y, t); let baz: fn(&f32, &f32, f32) -> f32 = bar; let keyframes = [(0.0, 1.0), (1.0, 0.0), (2.0, -1.0)]; diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index 20d458db72d23..8b66f12b356d3 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -77,9 +77,9 @@ pub mod prelude { ivec2, ivec3, ivec4, mat2, mat3, mat3a, mat4, ops, primitives::*, quat, uvec2, uvec3, uvec4, vec2, vec3, vec3a, vec4, BVec2, BVec3, BVec3A, BVec4, BVec4A, - EulerRot, FloatExt, IRect, IVec2, IVec3, IVec4, Isometry2d, Isometry3d, Mat2, Mat3, Mat3A, - Mat4, Quat, Ray2d, Ray3d, Rect, Rot2, StableInterpolate, URect, UVec2, UVec3, UVec4, Vec2, - Vec2Swizzles, Vec3, Vec3A, Vec3Swizzles, Vec4, Vec4Swizzles, + EulerRot, FloatExt, IRect, IVec2, IVec3, IVec4, Interpolate, InterpolateStable, Isometry2d, + Isometry3d, Mat2, Mat3, Mat3A, Mat4, Quat, Ray2d, Ray3d, Rect, Rot2, URect, UVec2, UVec3, + UVec4, Vec2, Vec2Swizzles, Vec3, Vec3A, Vec3Swizzles, Vec4, Vec4Swizzles, }; #[doc(hidden)] diff --git a/crates/bevy_math/src/sampling/shape_sampling.rs b/crates/bevy_math/src/sampling/shape_sampling.rs index 3be0ead1da98e..627423520ed6b 100644 --- a/crates/bevy_math/src/sampling/shape_sampling.rs +++ b/crates/bevy_math/src/sampling/shape_sampling.rs @@ -317,9 +317,9 @@ fn sample_triangle_boundary( if let Ok(dist) = WeightedIndex::new([ab.norm(), ac.norm(), bc.norm()]) { match dist.sample(rng) { - 0 => a.lerp(b, t), - 1 => a.lerp(c, t), - 2 => b.lerp(c, t), + 0 => a.interp(&b, t), + 1 => a.interp(&c, t), + 2 => b.interp(&c, t), _ => unreachable!(), } } else { diff --git a/crates/bevy_transform/src/components/transform.rs b/crates/bevy_transform/src/components/transform.rs index 0ea9e1930f366..79de502bdb1aa 100644 --- a/crates/bevy_transform/src/components/transform.rs +++ b/crates/bevy_transform/src/components/transform.rs @@ -1,5 +1,5 @@ use super::GlobalTransform; -use bevy_math::{Affine3A, Dir3, Isometry3d, Mat3, Mat4, Quat, Vec3}; +use bevy_math::{Affine3A, Dir3, Interpolate, Isometry3d, Mat3, Mat4, Quat, Vec3}; use core::ops::Mul; #[cfg(feature = "bevy-support")] @@ -649,6 +649,16 @@ impl Mul for Transform { } } +impl Interpolate for Transform { + fn interp(&self, other: &Self, param: f32) -> Self { + Transform { + translation: self.translation.interp(&other.translation, param), + rotation: self.rotation.interp(&other.rotation, param), + scale: self.scale.interp(&other.scale, param), + } + } +} + /// An optimization for transform propagation. This ZST marker component uses change detection to /// mark all entities of the hierarchy as "dirty" if any of their descendants have a changed /// `Transform`. If this component is *not* marked `is_changed()`, propagation will halt. diff --git a/examples/3d/mesh_ray_cast.rs b/examples/3d/mesh_ray_cast.rs index 85985f75a900a..4046537bed012 100644 --- a/examples/3d/mesh_ray_cast.rs +++ b/examples/3d/mesh_ray_cast.rs @@ -6,7 +6,7 @@ use std::f32::consts::{FRAC_PI_2, PI}; use bevy::{ color::palettes::css, core_pipeline::{bloom::Bloom, tonemapping::Tonemapping}, - math::vec3, + math::{vec3, Interpolate}, picking::backend::ray::RayMap, prelude::*, }; @@ -60,8 +60,12 @@ fn bounce_ray(mut ray: Ray3d, ray_cast: &mut MeshRayCast, gizmos: &mut Gizmos, c // Draw the point of intersection and add it to the list let brightness = 1.0 + 10.0 * (1.0 - i as f32 / MAX_BOUNCES as f32); - intersections.push((hit.point, Color::BLACK.mix(&color, brightness))); - gizmos.sphere(hit.point, 0.005, Color::BLACK.mix(&color, brightness * 2.0)); + intersections.push((hit.point, Color::BLACK.interp(&color, brightness))); + gizmos.sphere( + hit.point, + 0.005, + Color::BLACK.interp(&color, brightness * 2.0), + ); // Reflect the ray off of the surface ray.direction = Dir3::new(ray.direction.reflect(hit.normal)).unwrap(); diff --git a/examples/3d/transmission.rs b/examples/3d/transmission.rs index e4df69b3d96fa..e5c65f9fa470d 100644 --- a/examples/3d/transmission.rs +++ b/examples/3d/transmission.rs @@ -26,7 +26,7 @@ use bevy::{ bloom::Bloom, core_3d::ScreenSpaceTransmissionQuality, prepass::DepthPrepass, tonemapping::Tonemapping, }, - math::ops, + math::{ops, Interpolate}, pbr::{NotShadowCaster, PointLightShadowMap, TransmittedShadowReceiver}, prelude::*, render::{ @@ -126,8 +126,8 @@ fn setup( )); // Candle Flame - let scaled_white = LinearRgba::from(ANTIQUE_WHITE) * 20.; - let scaled_orange = LinearRgba::from(ORANGE_RED) * 4.; + let scaled_white = LinearRgba::from(ANTIQUE_WHITE).scale_by(20.0); + let scaled_orange = LinearRgba::from(ORANGE_RED).scale_by(4.0); let emissive = LinearRgba { red: scaled_white.red + scaled_orange.red, green: scaled_white.green + scaled_orange.green, @@ -289,7 +289,7 @@ fn setup( Transform::from_xyz(-1.0, 1.7, 0.0), PointLight { color: Color::from( - LinearRgba::from(ANTIQUE_WHITE).mix(&LinearRgba::from(ORANGE_RED), 0.2), + LinearRgba::from(ANTIQUE_WHITE).interp(&LinearRgba::from(ORANGE_RED), 0.2), ), intensity: 4_000.0, radius: 0.2, diff --git a/examples/animation/animated_transform.rs b/examples/animation/animated_transform.rs index decb3d34a69df..99046f1e81212 100644 --- a/examples/animation/animated_transform.rs +++ b/examples/animation/animated_transform.rs @@ -53,7 +53,7 @@ fn setup( let planet_animation_target_id = AnimationTargetId::from_name(&planet); animation.add_curve_to_target( planet_animation_target_id, - AnimatableCurve::new( + PropertyCurve::new( animated_field!(Transform::translation), UnevenSampleAutoCurve::new([0.0, 1.0, 2.0, 3.0, 4.0].into_iter().zip([ Vec3::new(1.0, 0.0, 1.0), @@ -74,7 +74,7 @@ fn setup( AnimationTargetId::from_names([planet.clone(), orbit_controller.clone()].iter()); animation.add_curve_to_target( orbit_controller_animation_target_id, - AnimatableCurve::new( + PropertyCurve::new( animated_field!(Transform::rotation), UnevenSampleAutoCurve::new([0.0, 1.0, 2.0, 3.0, 4.0].into_iter().zip([ Quat::IDENTITY, @@ -94,7 +94,7 @@ fn setup( ); animation.add_curve_to_target( satellite_animation_target_id, - AnimatableCurve::new( + PropertyCurve::new( animated_field!(Transform::scale), UnevenSampleAutoCurve::new( [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0] @@ -119,7 +119,7 @@ fn setup( AnimationTargetId::from_names( [planet.clone(), orbit_controller.clone(), satellite.clone()].iter(), ), - AnimatableCurve::new( + PropertyCurve::new( animated_field!(Transform::rotation), UnevenSampleAutoCurve::new([0.0, 1.0, 2.0, 3.0, 4.0].into_iter().zip([ Quat::IDENTITY, diff --git a/examples/animation/animated_ui.rs b/examples/animation/animated_ui.rs index f31b2ccd5eb4b..ee75730145bc6 100644 --- a/examples/animation/animated_ui.rs +++ b/examples/animation/animated_ui.rs @@ -47,9 +47,9 @@ impl AnimationInfo { // Create a curve that animates font size. animation_clip.add_curve_to_target( animation_target_id, - AnimatableCurve::new( + PropertyCurve::new( animated_field!(TextFont::font_size), - AnimatableKeyframeCurve::new( + KeyframeCurve::new( [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0] .into_iter() .zip([24.0, 80.0, 24.0, 80.0, 24.0, 80.0, 24.0]), @@ -67,9 +67,9 @@ impl AnimationInfo { // that it is in the "srgba" format. animation_clip.add_curve_to_target( animation_target_id, - AnimatableCurve::new( + PropertyCurve::new( TextColorProperty, - AnimatableKeyframeCurve::new([0.0, 1.0, 2.0, 3.0].into_iter().zip([ + KeyframeCurve::new([0.0, 1.0, 2.0, 3.0].into_iter().zip([ Srgba::RED, Srgba::GREEN, Srgba::BLUE, diff --git a/examples/animation/color_animation.rs b/examples/animation/color_animation.rs index 2b72c44bff82e..e3cb378d95e06 100644 --- a/examples/animation/color_animation.rs +++ b/examples/animation/color_animation.rs @@ -1,20 +1,21 @@ //! Demonstrates how to animate colors in different color spaces using mixing and splines. -use bevy::{math::VectorSpace, prelude::*}; +use bevy::{ + color::ColorCurve, + math::{Curve, Interpolate}, + prelude::*, +}; // We define this trait so we can reuse the same code for multiple color types that may be implemented using curves. -trait CurveColor: VectorSpace + Into + Send + Sync + 'static {} -impl + Send + Sync + 'static> CurveColor for T {} +trait ColorSpace: Interpolate + Into + Clone + Send + Sync + 'static {} -// We define this trait so we can reuse the same code for multiple color types that may be implemented using mixing. -trait MixedColor: Mix + Into + Send + Sync + 'static {} -impl + Send + Sync + 'static> MixedColor for T {} +impl + Clone + Send + Sync + 'static> ColorSpace for T {} -#[derive(Debug, Component)] -struct Curve(CubicCurve); +#[derive(Component)] +struct ExampleCurve(ColorCurve); #[derive(Debug, Component)] -struct Mixed([T; 4]); +struct ExampleInterpolate([T; 4]); fn main() { App::new() @@ -68,39 +69,39 @@ fn setup(mut commands: Commands) { spawn_mixed_sprite(&mut commands, -275., colors.map(Oklcha::from)); } -fn spawn_curve_sprite(commands: &mut Commands, y: f32, points: [T; 4]) { +fn spawn_curve_sprite(commands: &mut Commands, y: f32, points: [T; 4]) { commands.spawn(( Sprite::sized(Vec2::new(75., 75.)), Transform::from_xyz(0., y, 0.), - Curve(CubicBezier::new([points]).to_curve().unwrap()), + ExampleCurve(ColorCurve::new(points).unwrap()), )); } -fn spawn_mixed_sprite(commands: &mut Commands, y: f32, colors: [T; 4]) { +fn spawn_mixed_sprite(commands: &mut Commands, y: f32, colors: [T; 4]) { commands.spawn(( Transform::from_xyz(0., y, 0.), Sprite::sized(Vec2::new(75., 75.)), - Mixed(colors), + ExampleInterpolate(colors), )); } -fn animate_curve( +fn animate_curve( time: Res