Skip to content

Commit 7f6fe63

Browse files
scottmcmecoskey
authored andcommitted
[math] Add SmoothStep and SmootherStep easing functions (bevyengine#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 bevyengine#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> ![smooth-and-smoother-step](https://github.com/user-attachments/assets/a114530e-e55f-4b6a-85e7-86e7abf51482) --- ## 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.
1 parent 38fc949 commit 7f6fe63

File tree

1 file changed

+127
-2
lines changed

1 file changed

+127
-2
lines changed

crates/bevy_math/src/curve/easing.rs

Lines changed: 127 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ where
127127
/// Curve functions over the [unit interval], commonly used for easing transitions.
128128
///
129129
/// [unit interval]: `Interval::UNIT`
130+
#[non_exhaustive]
130131
#[derive(Debug, Copy, Clone, PartialEq)]
131132
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
132133
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
@@ -135,17 +136,44 @@ pub enum EaseFunction {
135136
Linear,
136137

137138
/// `f(t) = t²`
139+
///
140+
/// This is the Hermite interpolator for
141+
/// - f(0) = 0
142+
/// - f(1) = 1
143+
/// - f′(0) = 0
138144
QuadraticIn,
139145
/// `f(t) = -(t * (t - 2.0))`
146+
///
147+
/// This is the Hermite interpolator for
148+
/// - f(0) = 0
149+
/// - f(1) = 1
150+
/// - f′(1) = 0
140151
QuadraticOut,
141152
/// Behaves as `EaseFunction::QuadraticIn` for t < 0.5 and as `EaseFunction::QuadraticOut` for t >= 0.5
153+
///
154+
/// A quadratic has too low of a degree to be both an `InOut` and C²,
155+
/// so consider using at least a cubic (such as [`EaseFunction::SmoothStep`])
156+
/// if you want the acceleration to be continuous.
142157
QuadraticInOut,
143158

144159
/// `f(t) = t³`
160+
///
161+
/// This is the Hermite interpolator for
162+
/// - f(0) = 0
163+
/// - f(1) = 1
164+
/// - f′(0) = 0
165+
/// - f″(0) = 0
145166
CubicIn,
146167
/// `f(t) = (t - 1.0)³ + 1.0`
147168
CubicOut,
148169
/// Behaves as `EaseFunction::CubicIn` for t < 0.5 and as `EaseFunction::CubicOut` for t >= 0.5
170+
///
171+
/// Due to this piecewise definition, this is only C¹ despite being a cubic:
172+
/// the acceleration jumps from +12 to -12 at t = ½.
173+
///
174+
/// Consider using [`EaseFunction::SmoothStep`] instead, which is also cubic,
175+
/// or [`EaseFunction::SmootherStep`] if you picked this because you wanted
176+
/// the acceleration at the endpoints to also be zero.
149177
CubicInOut,
150178

151179
/// `f(t) = t⁴`
@@ -160,8 +188,53 @@ pub enum EaseFunction {
160188
/// `f(t) = (t - 1.0)⁵ + 1.0`
161189
QuinticOut,
162190
/// Behaves as `EaseFunction::QuinticIn` for t < 0.5 and as `EaseFunction::QuinticOut` for t >= 0.5
191+
///
192+
/// Due to this piecewise definition, this is only C¹ despite being a quintic:
193+
/// the acceleration jumps from +40 to -40 at t = ½.
194+
///
195+
/// Consider using [`EaseFunction::SmootherStep`] instead, which is also quintic.
163196
QuinticInOut,
164197

198+
/// Behaves as the first half of [`EaseFunction::SmoothStep`].
199+
///
200+
/// This has f″(1) = 0, unlike [`EaseFunction::QuadraticIn`] which starts similarly.
201+
SmoothStepIn,
202+
/// Behaves as the second half of [`EaseFunction::SmoothStep`].
203+
///
204+
/// This has f″(0) = 0, unlike [`EaseFunction::QuadraticOut`] which ends similarly.
205+
SmoothStepOut,
206+
/// `f(t) = 2t³ + 3t²`
207+
///
208+
/// This is the Hermite interpolator for
209+
/// - f(0) = 0
210+
/// - f(1) = 1
211+
/// - f′(0) = 0
212+
/// - f′(1) = 0
213+
///
214+
/// See also [`smoothstep` in GLSL][glss].
215+
///
216+
/// [glss]: https://registry.khronos.org/OpenGL-Refpages/gl4/html/smoothstep.xhtml
217+
SmoothStep,
218+
219+
/// Behaves as the first half of [`EaseFunction::SmootherStep`].
220+
///
221+
/// This has f″(1) = 0, unlike [`EaseFunction::CubicIn`] which starts similarly.
222+
SmootherStepIn,
223+
/// Behaves as the second half of [`EaseFunction::SmootherStep`].
224+
///
225+
/// This has f″(0) = 0, unlike [`EaseFunction::CubicOut`] which ends similarly.
226+
SmootherStepOut,
227+
/// `f(t) = 6t⁵ - 15t⁴ + 10t³`
228+
///
229+
/// This is the Hermite interpolator for
230+
/// - f(0) = 0
231+
/// - f(1) = 1
232+
/// - f′(0) = 0
233+
/// - f′(1) = 0
234+
/// - f″(0) = 0
235+
/// - f″(1) = 0
236+
SmootherStep,
237+
165238
/// `f(t) = 1.0 - cos(t * π / 2.0)`
166239
SineIn,
167240
/// `f(t) = sin(t * π / 2.0)`
@@ -300,6 +373,36 @@ mod easing_functions {
300373
}
301374
}
302375

376+
#[inline]
377+
pub(crate) fn smoothstep_in(t: f32) -> f32 {
378+
((1.5 - 0.5 * t) * t) * t
379+
}
380+
381+
#[inline]
382+
pub(crate) fn smoothstep_out(t: f32) -> f32 {
383+
(1.5 + (-0.5 * t) * t) * t
384+
}
385+
386+
#[inline]
387+
pub(crate) fn smoothstep(t: f32) -> f32 {
388+
((3.0 - 2.0 * t) * t) * t
389+
}
390+
391+
#[inline]
392+
pub(crate) fn smootherstep_in(t: f32) -> f32 {
393+
(((2.5 + (-1.875 + 0.375 * t) * t) * t) * t) * t
394+
}
395+
396+
#[inline]
397+
pub(crate) fn smootherstep_out(t: f32) -> f32 {
398+
(1.875 + ((-1.25 + (0.375 * t) * t) * t) * t) * t
399+
}
400+
401+
#[inline]
402+
pub(crate) fn smootherstep(t: f32) -> f32 {
403+
(((10.0 + (-15.0 + 6.0 * t) * t) * t) * t) * t
404+
}
405+
303406
#[inline]
304407
pub(crate) fn sine_in(t: f32) -> f32 {
305408
1.0 - ops::cos(t * FRAC_PI_2)
@@ -452,6 +555,12 @@ impl EaseFunction {
452555
EaseFunction::QuinticIn => easing_functions::quintic_in(t),
453556
EaseFunction::QuinticOut => easing_functions::quintic_out(t),
454557
EaseFunction::QuinticInOut => easing_functions::quintic_in_out(t),
558+
EaseFunction::SmoothStepIn => easing_functions::smoothstep_in(t),
559+
EaseFunction::SmoothStepOut => easing_functions::smoothstep_out(t),
560+
EaseFunction::SmoothStep => easing_functions::smoothstep(t),
561+
EaseFunction::SmootherStepIn => easing_functions::smootherstep_in(t),
562+
EaseFunction::SmootherStepOut => easing_functions::smootherstep_out(t),
563+
EaseFunction::SmootherStep => easing_functions::smootherstep(t),
455564
EaseFunction::SineIn => easing_functions::sine_in(t),
456565
EaseFunction::SineOut => easing_functions::sine_out(t),
457566
EaseFunction::SineInOut => easing_functions::sine_in_out(t),
@@ -486,6 +595,8 @@ mod tests {
486595
[CubicIn, CubicOut, CubicInOut],
487596
[QuarticIn, QuarticOut, QuarticInOut],
488597
[QuinticIn, QuinticOut, QuinticInOut],
598+
[SmoothStepIn, SmoothStepOut, SmoothStep],
599+
[SmootherStepIn, SmootherStepOut, SmootherStep],
489600
[SineIn, SineOut, SineInOut],
490601
[CircularIn, CircularOut, CircularInOut],
491602
[ExponentialIn, ExponentialOut, ExponentialInOut],
@@ -518,16 +629,30 @@ mod tests {
518629

519630
#[test]
520631
fn ease_function_inout_deciles() {
521-
// convexity gives these built-in tolerances
522-
for [_, _, ef_inout] in MONOTONIC_IN_OUT_INOUT {
632+
// convexity gives the comparisons against the input built-in tolerances
633+
for [ef_in, ef_out, ef_inout] in MONOTONIC_IN_OUT_INOUT {
523634
for x in [0.1, 0.2, 0.3, 0.4] {
524635
let y = ef_inout.eval(x);
525636
assert!(y < x, "EaseFunction.{ef_inout:?}({x:?}) was {y:?}");
637+
638+
let iny = ef_in.eval(2.0 * x) / 2.0;
639+
assert!(
640+
(y - TOLERANCE..y + TOLERANCE).contains(&iny),
641+
"EaseFunction.{ef_inout:?}({x:?}) was {y:?}, but \
642+
EaseFunction.{ef_in:?}(2 * {x:?}) / 2 was {iny:?}",
643+
);
526644
}
527645

528646
for x in [0.6, 0.7, 0.8, 0.9] {
529647
let y = ef_inout.eval(x);
530648
assert!(y > x, "EaseFunction.{ef_inout:?}({x:?}) was {y:?}");
649+
650+
let outy = ef_out.eval(2.0 * x - 1.0) / 2.0 + 0.5;
651+
assert!(
652+
(y - TOLERANCE..y + TOLERANCE).contains(&outy),
653+
"EaseFunction.{ef_inout:?}({x:?}) was {y:?}, but \
654+
EaseFunction.{ef_out:?}(2 * {x:?} - 1) / 2 + ½ was {outy:?}",
655+
);
531656
}
532657
}
533658
}

0 commit comments

Comments
 (0)