-
-
Notifications
You must be signed in to change notification settings - Fork 4k
Unified interpolation #18674
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Unified interpolation #18674
Conversation
/// 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Super minor insignificant question: why not interpolate
? Is it just to keep it concise?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd personally prefer we use the full interpolate
, and then offer a trivial blanket-impl trait to provide a shortcut name like lerp
(which is more standard nomenclature anyway):
pub trait Lerp: Interpolate {
fn lerp(&self, other: &Self, value: f32) -> Self;
}
impl<T: Interpolate> Lerp for T {
fn lerp(&self, other: &Self, value: f32) -> Self {
self.interpolate(other, value)
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know enough about the animation system to do a full review, but IMO, renaming to interpolate
and the Lerp
trait are the only things missing from this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if a blanket impl of Lerp
for all T: Interpolate
would necessarily make sense. For example, Quat::interp
uses Quat::slerp
, not Quat::lerp
, so it would be misleading.
+1 for interpolate
over interp
though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
VectorSpace
is effectively lerp
right now. I don't think a Lerp
trait is necessary: slerp
, lerp
and nlerp
should be reserved for methods implemented directly on types, not traits. If you need generalized linear interpolation use T: VectorSpace
with T::interpolate
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with this in principle; in maths there are clear and well known definitions for things like interpolation, so the more things we can tie back to pure-maths concepts the better IMO. I do have some questions and comments, but I largely approve of this PR as-is. Just commenting now since it's still in draft.
@@ -3,7 +3,7 @@ | |||
#[path = "../helpers/camera_controller.rs"] | |||
mod camera_controller; | |||
|
|||
use bevy::{color::palettes::css::*, prelude::*}; | |||
use bevy::{color::palettes::css::*, math::Interpolate, prelude::*}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be worth putting Interpolate
in bevy_math::prelude
, since it's likely to be commonly used.
let color = mixed.0[start_i as usize].mix(&mixed.0[start_i as usize + 1], local_t); | ||
let color = mixed.0[start_i as usize].interp(&mixed.0[start_i as usize + 1], local_t); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Future PR: Lines like this make me want a RangeInterpolateExt
trait which would let you do something like (a..b).at(t)
. Likewise for slices (which would assume equal weighting to account for more than 2 items in a slice).
pub trait RangeInterpolateExt<T> {
fn at(&self, value: f32) -> T;
}
impl<T: Interpolate> RangeInterpolateExt<T> for Range<T> {
fn at(&self, value: f32) -> T {
self.start.interp(self.end, value)
}
}
assert_eq!((0.0..5.0).at(0.5), 2.5);
Slice support in particular would be really nice, since you could do:
let index = start_i as usize;
let color = mixed.0[index..=index+1].at(local_t);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
actually bevy_color
already has something quite like this. we just need to generalize it.
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([ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, a KeyframeCurve
implies it must support animation, since it is a curve of animation keyframes.
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), | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Future PR: This is begging for a derive macro.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep, so is Blend
... actually all these traits are.
impl Interpolate for Color { | ||
#[inline] | ||
fn interp(&self, other: &Self, param: f32) -> Self { | ||
Oklaba::interp(&(*self).into(), &(*other).into(), param).into() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: This will increase the number of maths operations involved in the-function-formerly-known-as-mix
, since we now always convert to Oklaba
for both colours, instead of only ever converting the second colour into the first's space. I think that's acceptable, but others may not agree.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would say this is probably good behaviour, because Oklab should be the goto "colour interpolating" space based on how it's constructed, and users of the Color
enum aren't supposed to be picky about performance (if they are, they should use specific colour types and watch for where they convert.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The change should be documented though, as it is an observable change in behaviour.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't agree with this change mainly because is not in sync with the rest of the Color
impls, like Hue
, Luminance
, Saturation
, etc.
How all traits are implemented is by keeping the color space if possible, and in case is not, the color is converted to ChosenColorSpace
which is Oklcha
not Oklaba
.
So even if this change stays as it is, at least use ChosenColorSpace
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interpolating in OkLch is not going to be as perceptually linear as OkLab. Personally I think the Color enum should use the space that makes the most sense here -- OkLcha obviously makes the most sense for Luminance
and Hue
(because it's a luma-chroma-hue space) but OkLab makes the most sense for Interpolate
and EuclideanDistance
(because it's a perceptually uniform linear space).
Tbh, looking at them now I have an issue with how these other traits are implemented as well (it's concerning that EuclideanDistance
is not symmetric). But I can leave those changes for another PR.
/// | ||
/// 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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any reason this trait isn't moved to bevy_math
instead? While it's only used on bevy_animation
, it conceptually feels more bevy_math
than bevy_animation
to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was wondering about this too. Open to bike shedding from reviewers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My vote: rename to BlendAdditive
and move to bevy_math
. That leaves room for multiplicative blending in the future.
/// 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like we're rapidly reinventing the information captured in num-traits
. Is there a reason bevy_math
doesn't use it? Especially considering it's already in our dependency graph thanks to numerous other crates (approx
, guillotiere
, image
, wgpu
, rand_distr
, etc.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had the same thought while writing this, but I don't like introducing new dependencies when they are incidental. As future work we should look at utilizing num
, possibly along side a change to add f64
scalar support throughout bevy math.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The lack of f64
support is pretty annoying, and also f16
(and other non-standard float types) would be useful for e.g. GPU integration.
@@ -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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// To animating a non-blendable property (like a boolean or enum), you must add | |
/// To animate a non-blendable property (like a boolean or enum), you must add |
use bevy_math::*; | ||
use bevy_transform::components::Transform; | ||
|
||
/// A type with a natural method of addative blending. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// A type with a natural method of addative blending. | |
/// A type with a natural method of additive blending. |
/// 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: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// can support addative blending. Specifically, the type must: | |
/// can support additive blending. Specifically, the type must: |
/// 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick: My hunch is that this sort of plural readership is not fit for writing documentation.
Maybe something like this would be better?
/// Some of you will have noticed that these rules encodes the axioms for a Monoid; In fact this trait | |
/// Those more familiar with abstract algebra may notice that these rules encodes the axioms for a Monoid; In fact this trait |
const IDENTITY: Self; | ||
|
||
/// Addatively blends another value on top of this one. | ||
fn blend(self, other: Self, blend_weight: f32) -> Self; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type docstring talks about the properties the blended object should have, but what properties should this blend
function itself have and how do they relate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See point (3). We require blend(a, b, w) == comp(a, interp(IDENTITY, b, w))
. Eg, it should be an additive blend.
where | ||
T: Blend, | ||
{ | ||
/// Addatively blends the value into the blender. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// Addatively blends the value into the blender. | |
/// Additively 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), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possible division by zero if the user happens to put in a negative weight (unlikely) or when starting with a 0 weight blend on a 0 weight blender. (I can see this happening in procedural scenarios, blending with a 0 input should probably always blend with 0)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch
/// 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// Addatively blends the value into a new blender. | |
/// Additively blends the value into a new blender. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you believe I put this through two different levels of spell-check and these still got through somehow?
} | ||
} | ||
|
||
/// This is an extension trait to `Blend` with methods to easily blend and interpolate |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need an extension trait?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At one point I had more logic in Blendable
but I guess I refactored that into Blender<T>
at some point. I could probably remove Blendable
now I guess, in favor of Blend
and Blender<T>
.
|
||
#[inline] | ||
fn blend(self, other: Self, blend_weight: f32) -> Self { | ||
$ty::from_vec4(self.to_vec4() + other.to_vec4() * blend_weight) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this supposed to be a convex (or at least an affine) combination?
$ty::from_vec4(self.to_vec4() + other.to_vec4() * blend_weight) | |
$ty::from_vec4(self.to_vec4() * (1.0 - blend_weight) + other.to_vec4() * blend_weight) |
Also I wonder if we want to blend alpha like that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nooo Blend
is for additive blending, which is just a linear combination. if we did it this way, blend
would be equivalent to interpolate
. take a closer look at Blender<T>::blend_additive
, Blender<T>::blend_interp
and impl_blendable_group_action!
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a pretty direct translation of the math as it was done previously (just with the extra complexity of Animatable
cut out).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but it's an interesting idea to alpha-blend here. additive blending is all about layering effects on top of each other, and alpha blending does sort of capture that for colors. @mweatherley @viridia should additive blending for animated colors involve alpha blending?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think there's an obvious right answer here. If I add A to B, does the alpha in A affect the blend factor, or does it affect the result alpha? My first question would be, "what does photoshop / gimp / inkscape / CSS do?"
I would check out what linebender does here in their new color
crate (which was partly based on bevy_color) - they are far more pedantic/strict about color spaces than we are: https://docs.rs/color/0.2.3/src/color/dynamic.rs.html#412-430
Just FYI, it's interesting that linebender's color
crate defines premultiplied-alpha colors as distinct types (using a generic parameter). This was an idea that we discussed early on, and initially I was against it (I thought it would make the color classes too complicated) but now I am not so sure that was the right choice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it seems as though the current implementation just does a linear combination including alpha, without considering premultiplication or alpha blending. This PR at least consistent with the current behavior.
Let's mark color blend-mode support and possible typed alpha state as problems for a future PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another util file down! 🎉
@@ -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), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do find it a little funny that colour mixing is now a bevy_math thing.
I wonder what bevy_color people think about this change.
Also I think it would be nice to add in an alias here for discoverability.
/// [`Color`] supports all the standard color operations, such as [interpolating](bevy_math::Interpolate), | |
/// [`Color`] supports all the standard color operations, such as [mixing/interpolating](bevy_math::Interpolate), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would be fine to add a mix
and mix_assign
alias to Interpolate
to ease migrations. the trouble with mixing is that no one really gets to own it, it has to be shared by what ever crate the type comes from, and math and animation.
@@ -143,7 +144,7 @@ pub struct SampleAutoCurve<T> { | |||
|
|||
impl<T> Curve<T> for SampleAutoCurve<T> | |||
where | |||
T: StableInterpolate, | |||
T: InterpolateStable + Clone, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By using Interpolate
here would make SampleAutoCurve
usable in more scenarios.
Like you could get rid of ColorCurve
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
InterpolateStable
gives us something like uniform velocity, which I think is important to have here by default.
I do want to merge this and ColorCurve
(or get rid of ColorCurve
) and rename the AutoCurve
stuff to be more friendly. But I was thinking about doing it as follow up. I don't think this needs to happen in this PR.
pub trait VectorSpace: | ||
Mul<f32, Output = Self> | ||
Interpolate |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because of the given blank implementation down below, you don't need to make the VectorSpace
to inherit from Interpolate
.
What I mean is that types that depend on T: VectorSpace
can use .interp()
automatically as long as they import the Interpolate
trait.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But you can't use interpolate
in generics of type T: VectorSpace
unless there's a trait bound, can you?
impl Interpolate for Color { | ||
#[inline] | ||
fn interp(&self, other: &Self, param: f32) -> Self { | ||
Oklaba::interp(&(*self).into(), &(*other).into(), param).into() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't agree with this change mainly because is not in sync with the rest of the Color
impls, like Hue
, Luminance
, Saturation
, etc.
How all traits are implemented is by keeping the color space if possible, and in case is not, the color is converted to ChosenColorSpace
which is Oklcha
not Oklaba
.
So even if this change stays as it is, at least use ChosenColorSpace
.
I have a few concerns as someone who's interested in skeletal animation. TLDR is that
The remaining concerns were true of the previous
// 1: Current Bevy? Additive identity = 0.
fn sub(additive, base) { additive - base }
fn weighted_add(base, additive, weight) { base + (additive * weight) }
// 2: Additive identity = 1.
fn sub(additive, base) { additive / base }
fn weighted_add(base, additive, weight) { lerp(base, base * additive, weight) }
// 3: Unreal. Additive identity = 0.
fn sub(additive, base) { (additive / base) - 1.0 }
fn weighted_add(base, additive, weight) { base * (1.0 + (additive * weight)) } |
@greeble-dev thanks so much for this feedback. Re:
|
|
Yep, And yes sigh looks like that does mess with scale again. Scale will have to be |
Here it is (also, yes). |
Objective
Bevy currently has lots of different ways to perform interpolation:
Animatable
,Mix
,StableInterpolate
,Ease
andVectorSpace
to name a few. These traits are implemented on types in a confusing ad-hoc case-by-case basis that often leaves useful functionality on the table.This PR seeks to unify all these methods under a single
Interpolate
super-trait, with a single "sane default" interpolation method calledinterp()
. With this trait, a lot of obscure technical debt inbevy_math
,bevy_color
, andbevy_animation
can be cleared away.Solution
We introduce a new
Interpolate
trait with fairly strong requirements, and then reconcile the existing interpolation methods with it:bevy_color::Mix
is superseded and has been removed.StableInterpolate
adds a uniform velocity invariant, it has been turned into a sub-trait and renamedInterpolateStable
.VectorSpace
supports linear interpolation, it has become a sub-trait with a blanket impl.Ease
is just a curve version of interpolation, it has become a sub-trait and been renamedInterpolateCurve
.Animatable
is technical-debt and was being heavily abused by the animation system, it has been replaced by theBlend
andBlendable
sub-traits (which encapsulate Lie-Group-like additive blending behavior).As I was working on this, I found myself getting very confused by the long and complex type names in the
animation_curves.rs
-- and since many of them referenced the now defunctAnimatable
trait, I thought I would take the opportunity to give them shorter and more clearly distinct names. I'm clearly not the only person who has been having trouble with them -- in three places the docs seem to be linking toAnimatableCurve
when they meant to link toAnimationCurve
.Testing
I ran all the animation and color examples, and every other example which I directly modified.
Migration Guide
Math
VectorSpace::lerp
was replaced byInterpolate::interp
. Theinterp
method is always linear for vector-spaces.StableInterpolate
is nowInterpolateStable
. Theinterpolate_stable
method was replaced byInterpolate::interp
.Ease
is nowInterpolateCurve
andinterpolate_curve_unbounded
is nowinterp_curve
.Color
bevy_color::Mix
trait became thebevy_math::Interpolate
trait, andmix
is nowinterp
.Color
now interpolates inOklab
by default.Laba
,LinearRgba
,Oklaba
,Srgba
andXyza
no longer implementVectorSpace
. Usescaled_by
to scale colors, orto_vec4
andfrom_vec4
for general math.Interpolate
,InterpolateStable
, andInterpolateCurve
.Animation
Blend
trait has been introduced to extendInterpolate
with additive blending.Animatable
trait has been replaced byBlendable
, which has a blanket implementation forBlend
types.AnimatableProperty
can now be defined for properties that do not implementBlendable
(or previously,Animatable
).BasicAnimationCurveEvaluator
is nowBlendStackEvaluator
.BasicAnimationCurveEvaluatorStackElement
is nowBlendStackElement
.AnimatableCurve
is nowPropertyCurve
.AnimatableCurveEvaluator
is nowPropertyCurveEvaluator
.AnimatableKeyframeCurve
is nowKeyframeCurve
.Questions For Reviewers
Animatable
entirely, but it doesn't need to. We can keep it around (but not use it internally) if we know there are downstream consumers. Do you think people will miss it?InterpolateCurve
forQuat
? Why isslerp
not good enough here?