[math] Add SmoothStep
and SmootherStep
easing functions (#16957)
# Objective Almost all of the `*InOut` easing functions are not actually smooth (`SineInOut` is the one exception). Because they're defined piecewise, they jump from accelerating upwards to accelerating downwards, causing infinite jerk at t=½. ## Solution This PR adds the well-known [smoothstep](https://registry.khronos.org/OpenGL-Refpages/gl4/html/smoothstep.xhtml), as well as its higher-degree version [smootherstep](https://en.wikipedia.org/wiki/Smoothstep#Variations), as easing functions. Mathematically, these are the classic [Hermite interpolation](https://en.wikipedia.org/wiki/Hermite_interpolation) results: - for smoothstep, the cubic with velocity zero at both ends - for smootherstep, the quintic with velocity zero *and acceleration zero* at both ends And because they're simple polynomials, there's no branching and thus they don't have the acceleration jump in the middle. I also added some more information and cross-linking to the documentation for these and some of the other easing functions, to help clarify why one might want to use these over other existing ones. In particular, I suspect that if people are willing to pay for a quintic they might prefer `SmootherStep` to `QuinticInOut`. For consistency with how everything else has triples, I added `Smooth(er)Step{In,Out}` as well, in case people want to run the `In` and `Out` versions separately for some reason. Qualitatively they're not hugely different from `Quadratic{In,Out}` or `Cubic{In,Out}`, though, so could be removed if you'd rather. They're low cost to keep, though, and convenient for testing. ## Testing These are simple polynomials, so their coefficients can be read directly from the Horner's method implementation and compared to the reference materials. The tests from #16910 were updated to also test these 6 new easing functions, ensuring basic behaviour, plus one was updated to better check that the InOut versions of things match their rescaled In and Out versions. Even small changes like ```diff - (((2.5 + (-1.875 + 0.375*t) * t) * t) * t) * t + (((2.5 + (-1.85 + 0.375*t) * t) * t) * t) * t ``` are caught by multiple tests this way. If you want to confirm them visually, here are the 6 new ones graphed: <https://www.desmos.com/calculator/2d3ofujhry>  --- ## Migration Guide This version of bevy marks `EaseFunction` as `#[non_exhaustive]` to that future changes to add more easing functions will be non-breaking. If you were exhaustively matching that enum -- which you probably weren't -- you'll need to add a catch-all (`_ =>`) arm to cover unknown easing functions.
This commit is contained in:
parent
124f8031e3
commit
f96653498b
@ -127,6 +127,7 @@ where
|
||||
/// Curve functions over the [unit interval], commonly used for easing transitions.
|
||||
///
|
||||
/// [unit interval]: `Interval::UNIT`
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
|
||||
@ -135,17 +136,44 @@ pub enum EaseFunction {
|
||||
Linear,
|
||||
|
||||
/// `f(t) = t²`
|
||||
///
|
||||
/// This is the Hermite interpolator for
|
||||
/// - f(0) = 0
|
||||
/// - f(1) = 1
|
||||
/// - f′(0) = 0
|
||||
QuadraticIn,
|
||||
/// `f(t) = -(t * (t - 2.0))`
|
||||
///
|
||||
/// This is the Hermite interpolator for
|
||||
/// - f(0) = 0
|
||||
/// - f(1) = 1
|
||||
/// - f′(1) = 0
|
||||
QuadraticOut,
|
||||
/// Behaves as `EaseFunction::QuadraticIn` for t < 0.5 and as `EaseFunction::QuadraticOut` for t >= 0.5
|
||||
///
|
||||
/// A quadratic has too low of a degree to be both an `InOut` and C²,
|
||||
/// so consider using at least a cubic (such as [`EaseFunction::SmoothStep`])
|
||||
/// if you want the acceleration to be continuous.
|
||||
QuadraticInOut,
|
||||
|
||||
/// `f(t) = t³`
|
||||
///
|
||||
/// This is the Hermite interpolator for
|
||||
/// - f(0) = 0
|
||||
/// - f(1) = 1
|
||||
/// - f′(0) = 0
|
||||
/// - f″(0) = 0
|
||||
CubicIn,
|
||||
/// `f(t) = (t - 1.0)³ + 1.0`
|
||||
CubicOut,
|
||||
/// Behaves as `EaseFunction::CubicIn` for t < 0.5 and as `EaseFunction::CubicOut` for t >= 0.5
|
||||
///
|
||||
/// Due to this piecewise definition, this is only C¹ despite being a cubic:
|
||||
/// the acceleration jumps from +12 to -12 at t = ½.
|
||||
///
|
||||
/// Consider using [`EaseFunction::SmoothStep`] instead, which is also cubic,
|
||||
/// or [`EaseFunction::SmootherStep`] if you picked this because you wanted
|
||||
/// the acceleration at the endpoints to also be zero.
|
||||
CubicInOut,
|
||||
|
||||
/// `f(t) = t⁴`
|
||||
@ -160,8 +188,53 @@ pub enum EaseFunction {
|
||||
/// `f(t) = (t - 1.0)⁵ + 1.0`
|
||||
QuinticOut,
|
||||
/// Behaves as `EaseFunction::QuinticIn` for t < 0.5 and as `EaseFunction::QuinticOut` for t >= 0.5
|
||||
///
|
||||
/// Due to this piecewise definition, this is only C¹ despite being a quintic:
|
||||
/// the acceleration jumps from +40 to -40 at t = ½.
|
||||
///
|
||||
/// Consider using [`EaseFunction::SmootherStep`] instead, which is also quintic.
|
||||
QuinticInOut,
|
||||
|
||||
/// Behaves as the first half of [`EaseFunction::SmoothStep`].
|
||||
///
|
||||
/// This has f″(1) = 0, unlike [`EaseFunction::QuadraticIn`] which starts similarly.
|
||||
SmoothStepIn,
|
||||
/// Behaves as the second half of [`EaseFunction::SmoothStep`].
|
||||
///
|
||||
/// This has f″(0) = 0, unlike [`EaseFunction::QuadraticOut`] which ends similarly.
|
||||
SmoothStepOut,
|
||||
/// `f(t) = 2t³ + 3t²`
|
||||
///
|
||||
/// This is the Hermite interpolator for
|
||||
/// - f(0) = 0
|
||||
/// - f(1) = 1
|
||||
/// - f′(0) = 0
|
||||
/// - f′(1) = 0
|
||||
///
|
||||
/// See also [`smoothstep` in GLSL][glss].
|
||||
///
|
||||
/// [glss]: https://registry.khronos.org/OpenGL-Refpages/gl4/html/smoothstep.xhtml
|
||||
SmoothStep,
|
||||
|
||||
/// Behaves as the first half of [`EaseFunction::SmootherStep`].
|
||||
///
|
||||
/// This has f″(1) = 0, unlike [`EaseFunction::CubicIn`] which starts similarly.
|
||||
SmootherStepIn,
|
||||
/// Behaves as the second half of [`EaseFunction::SmootherStep`].
|
||||
///
|
||||
/// This has f″(0) = 0, unlike [`EaseFunction::CubicOut`] which ends similarly.
|
||||
SmootherStepOut,
|
||||
/// `f(t) = 6t⁵ - 15t⁴ + 10t³`
|
||||
///
|
||||
/// This is the Hermite interpolator for
|
||||
/// - f(0) = 0
|
||||
/// - f(1) = 1
|
||||
/// - f′(0) = 0
|
||||
/// - f′(1) = 0
|
||||
/// - f″(0) = 0
|
||||
/// - f″(1) = 0
|
||||
SmootherStep,
|
||||
|
||||
/// `f(t) = 1.0 - cos(t * π / 2.0)`
|
||||
SineIn,
|
||||
/// `f(t) = sin(t * π / 2.0)`
|
||||
@ -300,6 +373,36 @@ mod easing_functions {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn smoothstep_in(t: f32) -> f32 {
|
||||
((1.5 - 0.5 * t) * t) * t
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn smoothstep_out(t: f32) -> f32 {
|
||||
(1.5 + (-0.5 * t) * t) * t
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn smoothstep(t: f32) -> f32 {
|
||||
((3.0 - 2.0 * t) * t) * t
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn smootherstep_in(t: f32) -> f32 {
|
||||
(((2.5 + (-1.875 + 0.375 * t) * t) * t) * t) * t
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn smootherstep_out(t: f32) -> f32 {
|
||||
(1.875 + ((-1.25 + (0.375 * t) * t) * t) * t) * t
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn smootherstep(t: f32) -> f32 {
|
||||
(((10.0 + (-15.0 + 6.0 * t) * t) * t) * t) * t
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn sine_in(t: f32) -> f32 {
|
||||
1.0 - ops::cos(t * FRAC_PI_2)
|
||||
@ -452,6 +555,12 @@ impl EaseFunction {
|
||||
EaseFunction::QuinticIn => easing_functions::quintic_in(t),
|
||||
EaseFunction::QuinticOut => easing_functions::quintic_out(t),
|
||||
EaseFunction::QuinticInOut => easing_functions::quintic_in_out(t),
|
||||
EaseFunction::SmoothStepIn => easing_functions::smoothstep_in(t),
|
||||
EaseFunction::SmoothStepOut => easing_functions::smoothstep_out(t),
|
||||
EaseFunction::SmoothStep => easing_functions::smoothstep(t),
|
||||
EaseFunction::SmootherStepIn => easing_functions::smootherstep_in(t),
|
||||
EaseFunction::SmootherStepOut => easing_functions::smootherstep_out(t),
|
||||
EaseFunction::SmootherStep => easing_functions::smootherstep(t),
|
||||
EaseFunction::SineIn => easing_functions::sine_in(t),
|
||||
EaseFunction::SineOut => easing_functions::sine_out(t),
|
||||
EaseFunction::SineInOut => easing_functions::sine_in_out(t),
|
||||
@ -486,6 +595,8 @@ mod tests {
|
||||
[CubicIn, CubicOut, CubicInOut],
|
||||
[QuarticIn, QuarticOut, QuarticInOut],
|
||||
[QuinticIn, QuinticOut, QuinticInOut],
|
||||
[SmoothStepIn, SmoothStepOut, SmoothStep],
|
||||
[SmootherStepIn, SmootherStepOut, SmootherStep],
|
||||
[SineIn, SineOut, SineInOut],
|
||||
[CircularIn, CircularOut, CircularInOut],
|
||||
[ExponentialIn, ExponentialOut, ExponentialInOut],
|
||||
@ -518,16 +629,30 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn ease_function_inout_deciles() {
|
||||
// convexity gives these built-in tolerances
|
||||
for [_, _, ef_inout] in MONOTONIC_IN_OUT_INOUT {
|
||||
// convexity gives the comparisons against the input built-in tolerances
|
||||
for [ef_in, ef_out, ef_inout] in MONOTONIC_IN_OUT_INOUT {
|
||||
for x in [0.1, 0.2, 0.3, 0.4] {
|
||||
let y = ef_inout.eval(x);
|
||||
assert!(y < x, "EaseFunction.{ef_inout:?}({x:?}) was {y:?}");
|
||||
|
||||
let iny = ef_in.eval(2.0 * x) / 2.0;
|
||||
assert!(
|
||||
(y - TOLERANCE..y + TOLERANCE).contains(&iny),
|
||||
"EaseFunction.{ef_inout:?}({x:?}) was {y:?}, but \
|
||||
EaseFunction.{ef_in:?}(2 * {x:?}) / 2 was {iny:?}",
|
||||
);
|
||||
}
|
||||
|
||||
for x in [0.6, 0.7, 0.8, 0.9] {
|
||||
let y = ef_inout.eval(x);
|
||||
assert!(y > x, "EaseFunction.{ef_inout:?}({x:?}) was {y:?}");
|
||||
|
||||
let outy = ef_out.eval(2.0 * x - 1.0) / 2.0 + 0.5;
|
||||
assert!(
|
||||
(y - TOLERANCE..y + TOLERANCE).contains(&outy),
|
||||
"EaseFunction.{ef_inout:?}({x:?}) was {y:?}, but \
|
||||
EaseFunction.{ef_out:?}(2 * {x:?} - 1) / 2 + ½ was {outy:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user