Skip to content
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

[math] Add SmoothStep and SmootherStep easing functions #16957

Merged
merged 1 commit into from
Dec 24, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 127 additions & 2 deletions crates/bevy_math/src/curve/easing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ where
/// Curve functions over the [unit interval], commonly used for easing transitions.
///
/// [unit interval]: `Interval::UNIT`
#[non_exhaustive]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that adding something to an enum is technically a breaking change, I figured I'd do the broader breaking change of marking this #[non_exhaustive] so that any future additions to the enum will be non-breaking.

#[derive(Debug, Copy, Clone, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
Expand All @@ -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⁴`
Expand All @@ -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)`
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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:?}",
);
}
}
}
Expand Down
Loading