From 9f1d9445968cea28e6ac3c9473e5a998e1b687d7 Mon Sep 17 00:00:00 2001 From: Aevyrie Date: Sun, 12 Feb 2023 21:10:58 -0800 Subject: [PATCH 01/18] Initial cubic bezier implementation --- crates/bevy_math/src/bezier.rs | 139 +++++++++++++++++++++++++++++++++ crates/bevy_math/src/lib.rs | 6 +- 2 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 crates/bevy_math/src/bezier.rs diff --git a/crates/bevy_math/src/bezier.rs b/crates/bevy_math/src/bezier.rs new file mode 100644 index 0000000000000..d397aeecadf45 --- /dev/null +++ b/crates/bevy_math/src/bezier.rs @@ -0,0 +1,139 @@ +use std::ops::{Add, Mul}; + +use glam::{Vec2, Vec3}; + +/// Provides methods for sampling Cubic Bezier curves. +pub trait CubicBezier { + /// Represents a coordinate in space, this can be 3d, 2d, or even 1d. + type Coord: Copy + Mul + Add; + + /// Returns the four control points of the cubic Bezier curve. + fn control_points(&self) -> [Self::Coord; 4]; + + /// Returns the point along the Bezier curve at the supplied parametric value `t`. + fn evaluate_at(&self, t: f32) -> Self::Coord { + let p = self.control_points(); + p[0] * (1. - t).powi(3) + + p[1] * t * 3.0 * (1.0 - t).powi(2) + + p[2] * 3.0 * (1.0 - t) * t.powi(2) + + p[3] * t.powi(3) + } + + /// Iterate over points in the bezier curve from `t = 0` to `t = 1`. + fn into_points(&self, subdivisions: i32) -> Vec { + (0..=subdivisions) + .map(|i| { + let t = i as f32 / subdivisions as f32; + self.evaluate_at(t) + }) + .collect() + } +} + +/// A 2-dimensional Bezier curve. +#[derive(Default, Clone, Copy, Debug, PartialEq)] +pub struct CubicBezier2d(pub [Vec2; 4]); + +impl CubicBezier for CubicBezier2d { + type Coord = Vec2; + + fn control_points(&self) -> [Self::Coord; 4] { + self.0 + } +} + +/// A 3-dimensional Bezier curve. +#[derive(Default, Clone, Copy, Debug, PartialEq)] +pub struct CubicBezier3d(pub [Vec3; 4]); + +impl CubicBezier for CubicBezier3d { + type Coord = Vec3; + + fn control_points(&self) -> [Self::Coord; 4] { + self.0 + } +} + +pub mod easing { + use super::CubicBezier; + + /// Used to optimize cubic bezier easing, which only does operations in one dimension at a time, + /// and whose first and last control points are constrained to 0 and 1 respectively. + #[derive(Default, Clone, Copy, Debug, PartialEq)] + struct CubicBezier1d(f32, f32); + + impl CubicBezier for CubicBezier1d { + type Coord = f32; + + fn control_points(&self) -> [Self::Coord; 4] { + [0.0, self.0, self.1, 1.0] + } + } + + /// A 2-dimensional Bezier curve used for easing in animation; the first and last control points + /// are constrained to (0, 0) and (1, 1) respectively. + #[derive(Default, Clone, Copy, Debug, PartialEq)] + pub struct CubicBezierEasing { + x: CubicBezier1d, + y: CubicBezier1d, + } + + impl CubicBezierEasing { + /// Construct a cubic bezier curve for animation easing, with control points P1 and P2. + /// + /// This is equivalent to the syntax used to define cubic bezier easing functions in, say, CSS: + /// `ease-in-out = cubic-bezier(0.42, 0.0, 0.58, 1.0)`. + pub fn new(p1_x: f32, p1_y: f32, p2_x: f32, p2_y: f32) -> Self { + Self { + x: CubicBezier1d(p1_x, p2_x), + y: CubicBezier1d(p1_y, p2_y), + } + } + + /// Maximum allowable error for iterative bezier solve + const MAX_ERROR: f32 = 1e-7; + + /// Maximum number of iterations during bezier solve + const MAX_ITERS: u8 = 8; + + /// Given a `time` within `0..=1`, remaps this using a shaping function to a new value. The + pub fn remap(&self, time: f32) -> f32 { + let x = time.clamp(0.0, 1.0); + let t = self.find_t_given_x(x); + self.y.evaluate_at(t) + } + + /// Compute the slope `dx/dt` of a cubic bezier easing curve at the given parametric `t`. + pub fn dx_dt(&self, t: f32) -> f32 { + let p0x = 0.0; + let p1x = self.x.0; + let p2x = self.x.1; + let p3x = 1.0; + 3. * (1. - t).powi(2) * (p1x - p0x) + + 6. * (1. - t) * t * (p2x - p1x) + + 3. * t.powi(2) * (p3x - p2x) + } + + /// Solve for the parametric value `t` corresponding to the given value of `x`. + pub fn find_t_given_x(&self, x: f32) -> f32 { + // We will use the desired value x as our initial guess for t. This is a good estimate, + // as cubic bezier curves for animation are usually near the line where x = t. + let mut t_guess = x; + let mut error = f32::MAX; + for _ in 0..Self::MAX_ITERS { + let x_guess = self.x.evaluate_at(t_guess); + error = x_guess - x; + if error.abs() <= Self::MAX_ERROR { + return t_guess; + } + let slope = self.dx_dt(t_guess); + t_guess -= error / slope; + } + if error.abs() <= Self::MAX_ERROR { + t_guess + } else { + x // fallback to linear interpolation if the solve fails + } + } + } +} diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index 3e397536f8c4c..c770b7e37e080 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -6,9 +6,11 @@ #![warn(missing_docs)] +mod bezier; mod ray; mod rect; +pub use bezier::{easing::CubicBezierEasing, CubicBezier2d, CubicBezier3d}; pub use ray::Ray; pub use rect::Rect; @@ -16,8 +18,8 @@ pub use rect::Rect; pub mod prelude { #[doc(hidden)] pub use crate::{ - BVec2, BVec3, BVec4, EulerRot, IVec2, IVec3, IVec4, Mat2, Mat3, Mat4, Quat, Ray, Rect, - UVec2, UVec3, UVec4, Vec2, Vec3, Vec4, + BVec2, BVec3, BVec4, CubicBezierEasing, EulerRot, IVec2, IVec3, IVec4, Mat2, Mat3, Mat4, + Quat, Ray, Rect, UVec2, UVec3, UVec4, Vec2, Vec3, Vec4, }; } From 358b47bc39974c47e37dfb493510271700e82bb2 Mon Sep 17 00:00:00 2001 From: Aevyrie Date: Sun, 12 Feb 2023 22:58:58 -0800 Subject: [PATCH 02/18] Add trait methods to types --- crates/bevy_math/src/bezier.rs | 28 ++++++++++++++++++++++++++-- crates/bevy_math/src/lib.rs | 4 ++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/crates/bevy_math/src/bezier.rs b/crates/bevy_math/src/bezier.rs index d397aeecadf45..d79cd36b07029 100644 --- a/crates/bevy_math/src/bezier.rs +++ b/crates/bevy_math/src/bezier.rs @@ -10,7 +10,7 @@ pub trait CubicBezier { /// Returns the four control points of the cubic Bezier curve. fn control_points(&self) -> [Self::Coord; 4]; - /// Returns the point along the Bezier curve at the supplied parametric value `t`. + /// Evaluate the cubic Bezier curve at the parametric value `t`. fn evaluate_at(&self, t: f32) -> Self::Coord { let p = self.control_points(); p[0] * (1. - t).powi(3) @@ -19,7 +19,7 @@ pub trait CubicBezier { + p[3] * t.powi(3) } - /// Iterate over points in the bezier curve from `t = 0` to `t = 1`. + /// Split the Bezier curve into `subdivisions`, and sample the position at each. fn into_points(&self, subdivisions: i32) -> Vec { (0..=subdivisions) .map(|i| { @@ -42,6 +42,18 @@ impl CubicBezier for CubicBezier2d { } } +impl CubicBezier2d { + /// Returns the point along the Bezier curve at the supplied parametric value `t`. + pub fn evaluate_at(&self, t: f32) -> Vec2 { + CubicBezier::evaluate_at(self, t) + } + + /// Iterate over points in the bezier curve from `t = 0` to `t = 1`. + pub fn into_points(&self, subdivisions: i32) -> Vec { + CubicBezier::into_points(self, subdivisions) + } +} + /// A 3-dimensional Bezier curve. #[derive(Default, Clone, Copy, Debug, PartialEq)] pub struct CubicBezier3d(pub [Vec3; 4]); @@ -54,6 +66,18 @@ impl CubicBezier for CubicBezier3d { } } +impl CubicBezier3d { + /// Returns the point along the Bezier curve at the supplied parametric value `t`. + pub fn evaluate_at(&self, t: f32) -> Vec3 { + CubicBezier::evaluate_at(self, t) + } + + /// Iterate over points in the bezier curve from `t = 0` to `t = 1`. + pub fn into_points(&self, subdivisions: i32) -> Vec { + CubicBezier::into_points(self, subdivisions) + } +} + pub mod easing { use super::CubicBezier; diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index c770b7e37e080..f6e99af1e398a 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -18,8 +18,8 @@ pub use rect::Rect; pub mod prelude { #[doc(hidden)] pub use crate::{ - BVec2, BVec3, BVec4, CubicBezierEasing, EulerRot, IVec2, IVec3, IVec4, Mat2, Mat3, Mat4, - Quat, Ray, Rect, UVec2, UVec3, UVec4, Vec2, Vec3, Vec4, + BVec2, BVec3, BVec4, CubicBezier2d, CubicBezier3d, CubicBezierEasing, EulerRot, IVec2, + IVec3, IVec4, Mat2, Mat3, Mat4, Quat, Ray, Rect, UVec2, UVec3, UVec4, Vec2, Vec3, Vec4, }; } From cbd556a09efcb7856efac6d3a9b28ee96b3d3993 Mon Sep 17 00:00:00 2001 From: Aevyrie Date: Sun, 12 Feb 2023 23:27:08 -0800 Subject: [PATCH 03/18] Rename `into_points` to `to_points` Methods that borrow `self` should use `to` instead of `into`. --- crates/bevy_math/src/bezier.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/bevy_math/src/bezier.rs b/crates/bevy_math/src/bezier.rs index d79cd36b07029..1ccb3029d3c86 100644 --- a/crates/bevy_math/src/bezier.rs +++ b/crates/bevy_math/src/bezier.rs @@ -20,7 +20,7 @@ pub trait CubicBezier { } /// Split the Bezier curve into `subdivisions`, and sample the position at each. - fn into_points(&self, subdivisions: i32) -> Vec { + fn to_points(&self, subdivisions: i32) -> Vec { (0..=subdivisions) .map(|i| { let t = i as f32 / subdivisions as f32; @@ -49,8 +49,8 @@ impl CubicBezier2d { } /// Iterate over points in the bezier curve from `t = 0` to `t = 1`. - pub fn into_points(&self, subdivisions: i32) -> Vec { - CubicBezier::into_points(self, subdivisions) + pub fn to_points(&self, subdivisions: i32) -> Vec { + CubicBezier::to_points(self, subdivisions) } } @@ -73,8 +73,8 @@ impl CubicBezier3d { } /// Iterate over points in the bezier curve from `t = 0` to `t = 1`. - pub fn into_points(&self, subdivisions: i32) -> Vec { - CubicBezier::into_points(self, subdivisions) + pub fn to_points(&self, subdivisions: i32) -> Vec { + CubicBezier::to_points(self, subdivisions) } } From 91a6f0c1b1b9816c4d89cadd68245c01e2a81448 Mon Sep 17 00:00:00 2001 From: Aevyrie Date: Mon, 13 Feb 2023 14:33:00 -0800 Subject: [PATCH 04/18] Refactor to use generic function instead of trait. --- crates/bevy_math/src/bezier.rs | 237 ++++++++++++++++----------------- crates/bevy_math/src/lib.rs | 2 +- 2 files changed, 114 insertions(+), 125 deletions(-) diff --git a/crates/bevy_math/src/bezier.rs b/crates/bevy_math/src/bezier.rs index 1ccb3029d3c86..923ad9aa46fe9 100644 --- a/crates/bevy_math/src/bezier.rs +++ b/crates/bevy_math/src/bezier.rs @@ -1,163 +1,152 @@ -use std::ops::{Add, Mul}; - use glam::{Vec2, Vec3}; -/// Provides methods for sampling Cubic Bezier curves. -pub trait CubicBezier { - /// Represents a coordinate in space, this can be 3d, 2d, or even 1d. - type Coord: Copy + Mul + Add; - - /// Returns the four control points of the cubic Bezier curve. - fn control_points(&self) -> [Self::Coord; 4]; - - /// Evaluate the cubic Bezier curve at the parametric value `t`. - fn evaluate_at(&self, t: f32) -> Self::Coord { - let p = self.control_points(); - p[0] * (1. - t).powi(3) - + p[1] * t * 3.0 * (1.0 - t).powi(2) - + p[2] * 3.0 * (1.0 - t) * t.powi(2) - + p[3] * t.powi(3) - } - - /// Split the Bezier curve into `subdivisions`, and sample the position at each. - fn to_points(&self, subdivisions: i32) -> Vec { - (0..=subdivisions) - .map(|i| { - let t = i as f32 / subdivisions as f32; - self.evaluate_at(t) - }) - .collect() - } -} - -/// A 2-dimensional Bezier curve. +/// A 2-dimensional Bezier curve, defined by its 4 [`Vec2`] control points. #[derive(Default, Clone, Copy, Debug, PartialEq)] pub struct CubicBezier2d(pub [Vec2; 4]); -impl CubicBezier for CubicBezier2d { - type Coord = Vec2; - - fn control_points(&self) -> [Self::Coord; 4] { - self.0 - } -} - impl CubicBezier2d { - /// Returns the point along the Bezier curve at the supplied parametric value `t`. + /// Returns the [`Vec2`] position along the Bezier curve at the supplied parametric value `t`. pub fn evaluate_at(&self, t: f32) -> Vec2 { - CubicBezier::evaluate_at(self, t) + bezier_impl::evaluate_cubic_bezier(self.0, t) } - /// Iterate over points in the bezier curve from `t = 0` to `t = 1`. + /// Split the Bezier curve into `subdivisions` across the length of the curve from t = `0..=1`. + /// evaluating the [`Vec2`] position at each step. pub fn to_points(&self, subdivisions: i32) -> Vec { - CubicBezier::to_points(self, subdivisions) + bezier_impl::cubic_bezier_to_points(self.0, subdivisions) } } -/// A 3-dimensional Bezier curve. +/// A 3-dimensional Bezier curve, defined by its 4 [`Vec3`] control points. #[derive(Default, Clone, Copy, Debug, PartialEq)] pub struct CubicBezier3d(pub [Vec3; 4]); -impl CubicBezier for CubicBezier3d { - type Coord = Vec3; - - fn control_points(&self) -> [Self::Coord; 4] { - self.0 - } -} - impl CubicBezier3d { - /// Returns the point along the Bezier curve at the supplied parametric value `t`. + /// Returns the [`Vec3`] position along the Bezier curve at the supplied parametric value `t`. pub fn evaluate_at(&self, t: f32) -> Vec3 { - CubicBezier::evaluate_at(self, t) + bezier_impl::evaluate_cubic_bezier(self.0, t) } - /// Iterate over points in the bezier curve from `t = 0` to `t = 1`. + /// Split the Bezier curve into `subdivisions` across the length of the curve from t = `0..=1`. + /// evaluating the [`Vec3`] position at each step. pub fn to_points(&self, subdivisions: i32) -> Vec { - CubicBezier::to_points(self, subdivisions) + bezier_impl::cubic_bezier_to_points(self.0, subdivisions) } } -pub mod easing { - use super::CubicBezier; +/// A 2-dimensional Bezier curve used for easing in animation. +/// +/// A cubic Bezier easing curve has control point `p0` at (0, 0) and `p3` at (1, 1), leaving only +/// `p1` and `p2` as the remaining degrees of freedom. The first and last control points are fixed +/// to ensure the animation begins at 0, and ends at 1. +#[derive(Default, Clone, Copy, Debug, PartialEq)] +pub struct CubicBezierEasing { + /// Control point P1 of the 2D cubic Bezier curve. Controls the start of the animation. + pub p1: Vec2, + /// Control point P2 of the 2D cubic Bezier curve. Controls the end of the animation. + pub p2: Vec2, +} - /// Used to optimize cubic bezier easing, which only does operations in one dimension at a time, - /// and whose first and last control points are constrained to 0 and 1 respectively. - #[derive(Default, Clone, Copy, Debug, PartialEq)] - struct CubicBezier1d(f32, f32); +impl CubicBezierEasing { + /// Construct a cubic bezier curve for animation easing, with control points `p1` and `p2`. + pub fn new(p1: Vec2, p2: Vec2) -> Self { + Self { p1, p2 } + } - impl CubicBezier for CubicBezier1d { - type Coord = f32; + /// Maximum allowable error for iterative bezier solve + const MAX_ERROR: f32 = 1e-7; - fn control_points(&self) -> [Self::Coord; 4] { - [0.0, self.0, self.1, 1.0] - } + /// Maximum number of iterations during bezier solve + const MAX_ITERS: u8 = 8; + + /// Returns the x-coordinate of the point along the Bezier curve at the supplied parametric + /// value `t`. + pub fn evaluate_x_at(&self, t: f32) -> f32 { + bezier_impl::evaluate_cubic_bezier([0.0, self.p1.x, self.p2.x, 1.0], t) + } + + /// Returns the y-coordinate of the point along the Bezier curve at the supplied parametric + /// value `t`. + pub fn evaluate_y_at(&self, t: f32) -> f32 { + bezier_impl::evaluate_cubic_bezier([0.0, self.p1.y, self.p2.y, 1.0], t) } - /// A 2-dimensional Bezier curve used for easing in animation; the first and last control points - /// are constrained to (0, 0) and (1, 1) respectively. - #[derive(Default, Clone, Copy, Debug, PartialEq)] - pub struct CubicBezierEasing { - x: CubicBezier1d, - y: CubicBezier1d, + /// Given a `time` within `0..=1`, remaps to a new value using the cubic Bezier curve as a + /// shaping function, where `x = time`, and `y = animation progress`. This will return `0` when + /// `t = 0`, and `1` when `t = 1`. + pub fn remap(&self, time: f32) -> f32 { + let x = time.clamp(0.0, 1.0); + let t = self.find_t_given_x(x); + self.evaluate_y_at(t) } - impl CubicBezierEasing { - /// Construct a cubic bezier curve for animation easing, with control points P1 and P2. - /// - /// This is equivalent to the syntax used to define cubic bezier easing functions in, say, CSS: - /// `ease-in-out = cubic-bezier(0.42, 0.0, 0.58, 1.0)`. - pub fn new(p1_x: f32, p1_y: f32, p2_x: f32, p2_y: f32) -> Self { - Self { - x: CubicBezier1d(p1_x, p2_x), - y: CubicBezier1d(p1_y, p2_y), + /// Compute the slope `dx/dt` of a cubic bezier easing curve at the given parametric `t`. + pub fn dx_dt(&self, t: f32) -> f32 { + let p0x = 0.0; + let p1x = self.p1.x; + let p2x = self.p2.x; + let p3x = 1.0; + 3. * (1. - t).powi(2) * (p1x - p0x) + + 6. * (1. - t) * t * (p2x - p1x) + + 3. * t.powi(2) * (p3x - p2x) + } + + /// Solve for the parametric value `t` corresponding to the given value of `x`. + /// + /// This will return `x` if the solve fails to converge, which corresponds to a simple linear + /// interpolation. + pub fn find_t_given_x(&self, x: f32) -> f32 { + // We will use the desired value x as our initial guess for t. This is a good estimate, + // as cubic bezier curves for animation are usually near the line where x = t. + let mut t_guess = x; + let mut error = f32::MAX; + for _ in 0..Self::MAX_ITERS { + let x_guess = self.evaluate_x_at(t_guess); + error = x_guess - x; + if error.abs() <= Self::MAX_ERROR { + return t_guess; } + let slope = self.dx_dt(t_guess); + t_guess -= error / slope; } + if error.abs() <= Self::MAX_ERROR { + t_guess + } else { + x // fallback to linear interpolation if the solve fails + } + } +} - /// Maximum allowable error for iterative bezier solve - const MAX_ERROR: f32 = 1e-7; +/// The bezier implementation is wrapped inside a private module to keep the public interface +/// simple. This allows us to reuse the generic code across various cubic Bezier types, without +/// exposing users to any unwieldy traits or generics in the IDE or documentation. +#[doc(hidden)] +mod bezier_impl { + use glam::{Vec2, Vec3}; + use std::ops::{Add, Mul}; - /// Maximum number of iterations during bezier solve - const MAX_ITERS: u8 = 8; + /// A point in space of any dimension that supports addition and multiplication. + pub trait Point: Copy + Mul + Add {} + impl Point for Vec3 {} + impl Point for Vec2 {} + impl Point for f32 {} - /// Given a `time` within `0..=1`, remaps this using a shaping function to a new value. The - pub fn remap(&self, time: f32) -> f32 { - let x = time.clamp(0.0, 1.0); - let t = self.find_t_given_x(x); - self.y.evaluate_at(t) - } - - /// Compute the slope `dx/dt` of a cubic bezier easing curve at the given parametric `t`. - pub fn dx_dt(&self, t: f32) -> f32 { - let p0x = 0.0; - let p1x = self.x.0; - let p2x = self.x.1; - let p3x = 1.0; - 3. * (1. - t).powi(2) * (p1x - p0x) - + 6. * (1. - t) * t * (p2x - p1x) - + 3. * t.powi(2) * (p3x - p2x) - } + /// Evaluate the cubic Bezier curve at the parametric value `t`. + pub fn evaluate_cubic_bezier(control_points: [P; 4], t: f32) -> P { + let p = control_points; + p[0] * (1. - t).powi(3) + + p[1] * t * 3.0 * (1.0 - t).powi(2) + + p[2] * 3.0 * (1.0 - t) * t.powi(2) + + p[3] * t.powi(3) + } - /// Solve for the parametric value `t` corresponding to the given value of `x`. - pub fn find_t_given_x(&self, x: f32) -> f32 { - // We will use the desired value x as our initial guess for t. This is a good estimate, - // as cubic bezier curves for animation are usually near the line where x = t. - let mut t_guess = x; - let mut error = f32::MAX; - for _ in 0..Self::MAX_ITERS { - let x_guess = self.x.evaluate_at(t_guess); - error = x_guess - x; - if error.abs() <= Self::MAX_ERROR { - return t_guess; - } - let slope = self.dx_dt(t_guess); - t_guess -= error / slope; - } - if error.abs() <= Self::MAX_ERROR { - t_guess - } else { - x // fallback to linear interpolation if the solve fails - } - } + /// Split the Bezier curve into `subdivisions`, and sample the position at each [`Point`] `P`. + pub fn cubic_bezier_to_points(control_points: [P; 4], subdivisions: i32) -> Vec

{ + (0..=subdivisions) + .map(|i| { + let t = i as f32 / subdivisions as f32; + evaluate_cubic_bezier(control_points, t) + }) + .collect() } } diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index f6e99af1e398a..1e16dc173c999 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -10,7 +10,7 @@ mod bezier; mod ray; mod rect; -pub use bezier::{easing::CubicBezierEasing, CubicBezier2d, CubicBezier3d}; +pub use bezier::{CubicBezier2d, CubicBezier3d, CubicBezierEasing}; pub use ray::Ray; pub use rect::Rect; From 7b884b60d92fdc4b48cad8da2da6bc3da83070f1 Mon Sep 17 00:00:00 2001 From: Aevyrie Date: Mon, 13 Feb 2023 14:36:29 -0800 Subject: [PATCH 05/18] Ensure implementation functions are inlined --- crates/bevy_math/src/bezier.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bevy_math/src/bezier.rs b/crates/bevy_math/src/bezier.rs index 923ad9aa46fe9..4f5eee191f931 100644 --- a/crates/bevy_math/src/bezier.rs +++ b/crates/bevy_math/src/bezier.rs @@ -132,6 +132,7 @@ mod bezier_impl { impl Point for f32 {} /// Evaluate the cubic Bezier curve at the parametric value `t`. + #[inline] pub fn evaluate_cubic_bezier(control_points: [P; 4], t: f32) -> P { let p = control_points; p[0] * (1. - t).powi(3) @@ -141,6 +142,7 @@ mod bezier_impl { } /// Split the Bezier curve into `subdivisions`, and sample the position at each [`Point`] `P`. + #[inline] pub fn cubic_bezier_to_points(control_points: [P; 4], subdivisions: i32) -> Vec

{ (0..=subdivisions) .map(|i| { From 25beaeacac075020c82667ed31688da633946649 Mon Sep 17 00:00:00 2001 From: Aevyrie Date: Tue, 14 Feb 2023 03:43:35 -0800 Subject: [PATCH 06/18] generic impl with benchmarks --- benches/Cargo.toml | 6 + benches/benches/bevy_math/bezier.rs | 95 +++++++++ crates/bevy_math/src/bezier.rs | 287 +++++++++++++++++++--------- crates/bevy_math/src/lib.rs | 8 +- 4 files changed, 300 insertions(+), 96 deletions(-) create mode 100644 benches/benches/bevy_math/bezier.rs diff --git a/benches/Cargo.toml b/benches/Cargo.toml index bad48a02e1421..1af1e4f618662 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -16,6 +16,7 @@ bevy_ecs = { path = "../crates/bevy_ecs" } bevy_reflect = { path = "../crates/bevy_reflect" } bevy_tasks = { path = "../crates/bevy_tasks" } bevy_utils = { path = "../crates/bevy_utils" } +bevy_math = { path = "../crates/bevy_math" } [profile.release] opt-level = 3 @@ -50,3 +51,8 @@ harness = false name = "iter" path = "benches/bevy_tasks/iter.rs" harness = false + +[[bench]] +name = "bezier" +path = "benches/bevy_math/bezier.rs" +harness = false diff --git a/benches/benches/bevy_math/bezier.rs b/benches/benches/bevy_math/bezier.rs new file mode 100644 index 0000000000000..b0cea6cee99c3 --- /dev/null +++ b/benches/benches/bevy_math/bezier.rs @@ -0,0 +1,95 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +use bevy_math::*; + +fn easing(c: &mut Criterion) { + let cubic_bezier = CubicBezierEasing::new(vec2(0.25, 0.1), vec2(0.25, 1.0)); + c.bench_function("easing_1000", |b| { + b.iter(|| { + (0..1000).map(|i| i as f32 / 1000.0).for_each(|t| { + cubic_bezier.ease(black_box(t)); + }) + }); + }); +} + +fn fifteen_degree(c: &mut Criterion) { + let bezier = Bezier::new([ + vec3(0.0, 0.0, 0.0), + vec3(0.0, 1.0, 0.0), + vec3(1.0, 0.0, 0.0), + vec3(1.0, 1.0, 1.0), + vec3(0.0, 0.0, 0.0), + vec3(0.0, 1.0, 0.0), + vec3(1.0, 0.0, 0.0), + vec3(1.0, 1.0, 1.0), + vec3(0.0, 0.0, 0.0), + vec3(0.0, 1.0, 0.0), + vec3(1.0, 0.0, 0.0), + vec3(1.0, 1.0, 1.0), + vec3(0.0, 0.0, 0.0), + vec3(0.0, 1.0, 0.0), + vec3(1.0, 0.0, 0.0), + vec3(1.0, 1.0, 1.0), + ]); + c.bench_function("fifteen_degree_position", |b| { + b.iter(|| bezier.position(black_box(0.5))); + }); +} + +fn quadratic(c: &mut Criterion) { + let bezier = QuadraticBezier3d::new([ + vec3a(0.0, 0.0, 0.0), + vec3a(0.0, 1.0, 0.0), + vec3a(1.0, 1.0, 1.0), + ]); + c.bench_function("quadratic_position", |b| { + b.iter(|| bezier.position(black_box(0.5))); + }); +} + +fn quadratic_vec3(c: &mut Criterion) { + let bezier = Bezier::new([ + vec3(0.0, 0.0, 0.0), + vec3(0.0, 1.0, 0.0), + vec3(1.0, 1.0, 1.0), + ]); + c.bench_function("quadratic_position_Vec3", |b| { + b.iter(|| bezier.position(black_box(0.5))); + }); +} + +fn cubic(c: &mut Criterion) { + let bezier = CubicBezier3d::new([ + vec3a(0.0, 0.0, 0.0), + vec3a(0.0, 1.0, 0.0), + vec3a(1.0, 0.0, 0.0), + vec3a(1.0, 1.0, 1.0), + ]); + c.bench_function("cubic_position", |b| { + b.iter(|| bezier.position(black_box(0.5))); + }); +} + +fn cubic_vec3(c: &mut Criterion) { + let bezier = Bezier::new([ + vec3(0.0, 0.0, 0.0), + vec3(0.0, 1.0, 0.0), + vec3(1.0, 0.0, 0.0), + vec3(1.0, 1.0, 1.0), + ]); + c.bench_function("cubic_position_Vec3", |b| { + b.iter(|| bezier.position(black_box(0.5))); + }); +} + +criterion_group!( + benches, + easing, + fifteen_degree, + quadratic, + quadratic_vec3, + cubic, + cubic_vec3 +); +criterion_main!(benches); diff --git a/crates/bevy_math/src/bezier.rs b/crates/bevy_math/src/bezier.rs index 4f5eee191f931..980ec97c6bd8b 100644 --- a/crates/bevy_math/src/bezier.rs +++ b/crates/bevy_math/src/bezier.rs @@ -1,36 +1,99 @@ -use glam::{Vec2, Vec3}; +use glam::{Vec2, Vec3, Vec3A}; + +use std::{ + fmt::Debug, + iter::Sum, + ops::{Add, Mul, Sub}, +}; + +/// A point in space of any dimension that supports addition and multiplication. +pub trait Point: + Mul + + Add + + Sub + + Sum + + Default + + Debug + + Clone + + PartialEq + + Copy +{ +} +impl Point for Vec3 {} // 3D +impl Point for Vec3A {} // 3D +impl Point for Vec2 {} // 2D +impl Point for f32 {} // 1D + +/// A cubic Bezier curve in 2D space +pub type CubicBezier2d = Bezier; +/// A cubic Bezier curve in 3D space +pub type CubicBezier3d = Bezier; +/// A quadratic Bezier curve in 2D space +pub type QuadraticBezier2d = Bezier; +/// A quadratic Bezier curve in 3D space +pub type QuadraticBezier3d = Bezier; + +/// A Bezier curve with `N` control points, and dimension defined by `P`. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Bezier(pub [P; N]); + +impl Default for Bezier { + fn default() -> Self { + Bezier([P::default(); N]) + } +} -/// A 2-dimensional Bezier curve, defined by its 4 [`Vec2`] control points. -#[derive(Default, Clone, Copy, Debug, PartialEq)] -pub struct CubicBezier2d(pub [Vec2; 4]); +impl Bezier { + /// Construct a new Bezier curve + pub fn new(control_points: [P; N]) -> Self { + Self(control_points) + } -impl CubicBezier2d { - /// Returns the [`Vec2`] position along the Bezier curve at the supplied parametric value `t`. - pub fn evaluate_at(&self, t: f32) -> Vec2 { - bezier_impl::evaluate_cubic_bezier(self.0, t) + /// Compute the [`Vec3`] position along the Bezier curve at the supplied parametric value `t`. + pub fn position(&self, t: f32) -> P { + bezier_impl::position(self.0, t) } - /// Split the Bezier curve into `subdivisions` across the length of the curve from t = `0..=1`. - /// evaluating the [`Vec2`] position at each step. - pub fn to_points(&self, subdivisions: i32) -> Vec { - bezier_impl::cubic_bezier_to_points(self.0, subdivisions) + /// Compute the first derivative B'(t) of this cubic bezier at `t` with respect to t. This is + /// the instantaneous velocity of a point tracing the Bezier curve from t = 0 to 1. + pub fn velocity(&self, t: f32) -> P { + bezier_impl::velocity(self.0, t) } -} -/// A 3-dimensional Bezier curve, defined by its 4 [`Vec3`] control points. -#[derive(Default, Clone, Copy, Debug, PartialEq)] -pub struct CubicBezier3d(pub [Vec3; 4]); + /// Compute the second derivative B''(t) of this cubic bezier at `t` with respect to t.This is + /// the instantaneous acceleration of a point tracing the Bezier curve from t = 0 to 1. + pub fn acceleration(&self, t: f32) -> P { + bezier_impl::acceleration(self.0, t) + } -impl CubicBezier3d { - /// Returns the [`Vec3`] position along the Bezier curve at the supplied parametric value `t`. - pub fn evaluate_at(&self, t: f32) -> Vec3 { - bezier_impl::evaluate_cubic_bezier(self.0, t) + /// Split the cubic Bezier curve of degree `N-1` into `subdivisions`, and sample with the + /// supplied `sample_function`. + #[inline] + pub fn sample(&self, subdivisions: i32, sample_function: fn(&Self, f32) -> P) -> Vec

{ + (0..=subdivisions) + .map(|i| { + let t = i as f32 / subdivisions as f32; + sample_function(self, t) + }) + .collect() } /// Split the Bezier curve into `subdivisions` across the length of the curve from t = `0..=1`. - /// evaluating the [`Vec3`] position at each step. - pub fn to_points(&self, subdivisions: i32) -> Vec { - bezier_impl::cubic_bezier_to_points(self.0, subdivisions) + /// sampling the position at each step. + pub fn to_positions(&self, subdivisions: i32) -> Vec

{ + self.sample(subdivisions, Self::position) + } + + /// Split the Bezier curve into `subdivisions` across the length of the curve from t = `0..=1`. + /// sampling the velocity at each step. + pub fn to_velocities(&self, subdivisions: i32) -> Vec

{ + self.sample(subdivisions, Self::velocity) + } + + /// Split the Bezier curve into `subdivisions` across the length of the curve from t = `0..=1`. + /// sampling the acceleration at each step. + pub fn to_accelerations(&self, subdivisions: i32) -> Vec

{ + self.sample(subdivisions, Self::acceleration) } } @@ -42,9 +105,9 @@ impl CubicBezier3d { #[derive(Default, Clone, Copy, Debug, PartialEq)] pub struct CubicBezierEasing { /// Control point P1 of the 2D cubic Bezier curve. Controls the start of the animation. - pub p1: Vec2, + p1: Vec2, /// Control point P2 of the 2D cubic Bezier curve. Controls the end of the animation. - pub p2: Vec2, + p2: Vec2, } impl CubicBezierEasing { @@ -57,98 +120,134 @@ impl CubicBezierEasing { const MAX_ERROR: f32 = 1e-7; /// Maximum number of iterations during bezier solve - const MAX_ITERS: u8 = 8; + const MAX_ITERS: u8 = 16; + + /// Given a `time` within `0..=1`, remaps to a new value using the cubic Bezier curve as a + /// shaping function, for which when plotted `x = time` and `y = animation progress`. This will + /// return `0` when `t = 0`, and `1` when `t = 1`. + pub fn ease(&self, time: f32) -> f32 { + let x = time.clamp(0.0, 1.0); + let t = self.find_t_given_x(x); + self.evaluate_y_at(t) + } - /// Returns the x-coordinate of the point along the Bezier curve at the supplied parametric - /// value `t`. + /// Compute the x-coordinate of the point along the Bezier curve at `t`. + #[inline] pub fn evaluate_x_at(&self, t: f32) -> f32 { - bezier_impl::evaluate_cubic_bezier([0.0, self.p1.x, self.p2.x, 1.0], t) + bezier_impl::position([0.0, self.p1.x, self.p2.x, 1.0], t) } - /// Returns the y-coordinate of the point along the Bezier curve at the supplied parametric - /// value `t`. + /// Compute the y-coordinate of the point along the Bezier curve at `t`. + #[inline] pub fn evaluate_y_at(&self, t: f32) -> f32 { - bezier_impl::evaluate_cubic_bezier([0.0, self.p1.y, self.p2.y, 1.0], t) + bezier_impl::position([0.0, self.p1.y, self.p2.y, 1.0], t) } - /// Given a `time` within `0..=1`, remaps to a new value using the cubic Bezier curve as a - /// shaping function, where `x = time`, and `y = animation progress`. This will return `0` when - /// `t = 0`, and `1` when `t = 1`. - pub fn remap(&self, time: f32) -> f32 { - let x = time.clamp(0.0, 1.0); - let t = self.find_t_given_x(x); - self.evaluate_y_at(t) + /// Compute the slope of the line at the given parametric value `t` with respect to t. + #[inline] + pub fn dx_dt(&self, t: f32) -> f32 { + bezier_impl::velocity([0.0, self.p1.x, self.p2.x, 1.0], t) } - /// Compute the slope `dx/dt` of a cubic bezier easing curve at the given parametric `t`. - pub fn dx_dt(&self, t: f32) -> f32 { - let p0x = 0.0; - let p1x = self.p1.x; - let p2x = self.p2.x; - let p3x = 1.0; - 3. * (1. - t).powi(2) * (p1x - p0x) - + 6. * (1. - t) * t * (p2x - p1x) - + 3. * t.powi(2) * (p3x - p2x) - } - - /// Solve for the parametric value `t` corresponding to the given value of `x`. - /// - /// This will return `x` if the solve fails to converge, which corresponds to a simple linear - /// interpolation. + /// Solve for the parametric value `t` that corresponds to the given value of `x`. + #[inline] pub fn find_t_given_x(&self, x: f32) -> f32 { - // We will use the desired value x as our initial guess for t. This is a good estimate, - // as cubic bezier curves for animation are usually near the line where x = t. let mut t_guess = x; - let mut error = f32::MAX; - for _ in 0..Self::MAX_ITERS { + (0..Self::MAX_ITERS).any(|_| { let x_guess = self.evaluate_x_at(t_guess); - error = x_guess - x; + let error = x_guess - x; if error.abs() <= Self::MAX_ERROR { - return t_guess; + return true; + } else { + let slope = self.dx_dt(t_guess); + t_guess -= error / slope; + return false; } - let slope = self.dx_dt(t_guess); - t_guess -= error / slope; - } - if error.abs() <= Self::MAX_ERROR { - t_guess - } else { - x // fallback to linear interpolation if the solve fails - } + }); + t_guess.clamp(0.0, 1.0) } } -/// The bezier implementation is wrapped inside a private module to keep the public interface -/// simple. This allows us to reuse the generic code across various cubic Bezier types, without -/// exposing users to any unwieldy traits or generics in the IDE or documentation. -#[doc(hidden)] -mod bezier_impl { - use glam::{Vec2, Vec3}; - use std::ops::{Add, Mul}; - - /// A point in space of any dimension that supports addition and multiplication. - pub trait Point: Copy + Mul + Add {} - impl Point for Vec3 {} - impl Point for Vec2 {} - impl Point for f32 {} - - /// Evaluate the cubic Bezier curve at the parametric value `t`. +/// Generic implementations for sampling cubic Bezier curves. Consider using the methods on +/// [`Bezier`] for more ergonomic use. +pub mod bezier_impl { + use super::Point; + + /// Compute the bernstein basis polynomial for iteration `i`, for a Bezier curve with with + /// degree `n`, at `t`. + #[inline] + pub fn bernstein_basis(n: usize, i: usize, t: f32) -> f32 { + (1. - t).powi((n - i) as i32) * t.powi(i as i32) + } + + /// Efficiently compute the binomial coefficient #[inline] - pub fn evaluate_cubic_bezier(control_points: [P; 4], t: f32) -> P { + fn binomial_coeff(n: usize, k: usize) -> usize { + let mut i = 0; + let mut result = 1; + let k = match k > n - k { + true => n - k, + false => k, + }; + while i < k { + result *= n - i; + result /= i + 1; + i += 1; + } + result + } + + /// Evaluate the Bezier curve B(t) of degree `N-1` at the parametric value `t`. + #[inline] + pub fn position(control_points: [P; N], t: f32) -> P { let p = control_points; - p[0] * (1. - t).powi(3) - + p[1] * t * 3.0 * (1.0 - t).powi(2) - + p[2] * 3.0 * (1.0 - t) * t.powi(2) - + p[3] * t.powi(3) + let degree = N - 1; + (0..=degree) + .map(|i| p[i] * binomial_coeff(degree, i) as f32 * bernstein_basis(degree, i, t)) + .sum() } - /// Split the Bezier curve into `subdivisions`, and sample the position at each [`Point`] `P`. + /// Compute the first derivative B'(t) of Bezier curve B(t) of degree `N-1` at the given + /// parametric value `t` with respect to t. #[inline] - pub fn cubic_bezier_to_points(control_points: [P; 4], subdivisions: i32) -> Vec

{ - (0..=subdivisions) + pub fn velocity(control_points: [P; N], t: f32) -> P { + debug_assert!( + N > 1, + "Velocity can only be computed on Bezier curves with a degree of 1 or higher" + ); + let p = control_points; + let degree = N - 1; + let degree_vel = N - 2; // the velocity Bezier is one degree lower than the position Bezier + (0..=degree_vel) .map(|i| { - let t = i as f32 / subdivisions as f32; - evaluate_cubic_bezier(control_points, t) + // Point on the velocity Bezier curve: + let p = (p[i + 1] - p[i]) * degree as f32; + p * binomial_coeff(degree_vel, i) as f32 * bernstein_basis(degree_vel, i, t) }) - .collect() + .sum() + } + + /// Compute the second derivative B''(t) of Bezier curve B(t) of degree `N-1` at the given + /// parametric value `t` with respect to t. + #[inline] + pub fn acceleration(control_points: [P; N], t: f32) -> P { + debug_assert!( + N > 2, + "Acceleration can only be computed on Bezier curves with a degree of 2 or higher" + ); + let p = control_points; + let degree = N - 1; + let degree_vel = N - 2; // the velocity Bezier is one degree lower than the position Bezier + let degree_accel = N - 3; // the accel Bezier is one degree lower than the velocity Bezier + (0..degree_vel) + .map(|i| { + // Points on the velocity Bezier curve: + let p0 = (p[i + 1] - p[i]) * degree as f32; + let p1 = (p[i + 2] - p[i + 1]) * degree as f32; + // Point on the acceleration Bezier curve: + let p = (p1 - p0) * (degree_vel) as f32; + p * binomial_coeff(degree_accel, i) as f32 * bernstein_basis(degree_accel, i, t) + }) + .sum() } } diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index 1e16dc173c999..3c92f919edbe3 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -10,7 +10,10 @@ mod bezier; mod ray; mod rect; -pub use bezier::{CubicBezier2d, CubicBezier3d, CubicBezierEasing}; +pub use bezier::{ + bezier_impl, Bezier, CubicBezier2d, CubicBezier3d, CubicBezierEasing, QuadraticBezier2d, + QuadraticBezier3d, +}; pub use ray::Ray; pub use rect::Rect; @@ -19,7 +22,8 @@ pub mod prelude { #[doc(hidden)] pub use crate::{ BVec2, BVec3, BVec4, CubicBezier2d, CubicBezier3d, CubicBezierEasing, EulerRot, IVec2, - IVec3, IVec4, Mat2, Mat3, Mat4, Quat, Ray, Rect, UVec2, UVec3, UVec4, Vec2, Vec3, Vec4, + IVec3, IVec4, Mat2, Mat3, Mat4, QuadraticBezier2d, QuadraticBezier3d, Quat, Ray, Rect, + UVec2, UVec3, UVec4, Vec2, Vec3, Vec4, }; } From d1df0d87a88bb748e33e63d3ab0d04338dca5cbf Mon Sep 17 00:00:00 2001 From: Aevyrie Date: Tue, 14 Feb 2023 03:49:56 -0800 Subject: [PATCH 07/18] Remove unnecessary `return`s --- crates/bevy_math/src/bezier.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_math/src/bezier.rs b/crates/bevy_math/src/bezier.rs index 980ec97c6bd8b..b1f0ac43e61bb 100644 --- a/crates/bevy_math/src/bezier.rs +++ b/crates/bevy_math/src/bezier.rs @@ -157,11 +157,11 @@ impl CubicBezierEasing { let x_guess = self.evaluate_x_at(t_guess); let error = x_guess - x; if error.abs() <= Self::MAX_ERROR { - return true; + true } else { let slope = self.dx_dt(t_guess); t_guess -= error / slope; - return false; + false } }); t_guess.clamp(0.0, 1.0) From af90a00ef0bd76741dac2549d79483c5a8dda81a Mon Sep 17 00:00:00 2001 From: Aevyrie Date: Tue, 14 Feb 2023 13:38:46 -0600 Subject: [PATCH 08/18] Update crates/bevy_math/src/bezier.rs Co-authored-by: Alice Cecile --- crates/bevy_math/src/bezier.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_math/src/bezier.rs b/crates/bevy_math/src/bezier.rs index b1f0ac43e61bb..84984fc143e74 100644 --- a/crates/bevy_math/src/bezier.rs +++ b/crates/bevy_math/src/bezier.rs @@ -173,7 +173,7 @@ impl CubicBezierEasing { pub mod bezier_impl { use super::Point; - /// Compute the bernstein basis polynomial for iteration `i`, for a Bezier curve with with + /// Compute the Bernstein basis polynomial for iteration `i`, for a Bezier curve with with /// degree `n`, at `t`. #[inline] pub fn bernstein_basis(n: usize, i: usize, t: f32) -> f32 { From 09354afdc1137aa9b51541db9a941773471eabf9 Mon Sep 17 00:00:00 2001 From: Aevyrie Date: Tue, 14 Feb 2023 11:54:50 -0800 Subject: [PATCH 09/18] REview feedback --- crates/bevy_math/src/bezier.rs | 40 ++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/crates/bevy_math/src/bezier.rs b/crates/bevy_math/src/bezier.rs index 84984fc143e74..efda12f7bd622 100644 --- a/crates/bevy_math/src/bezier.rs +++ b/crates/bevy_math/src/bezier.rs @@ -66,8 +66,9 @@ impl Bezier { bezier_impl::acceleration(self.0, t) } - /// Split the cubic Bezier curve of degree `N-1` into `subdivisions`, and sample with the - /// supplied `sample_function`. + /// Split the cubic Bezier curve of degree `N-1` into `subdivisions` evenly spaced `t` values + /// across the length of the curve from t = `0..=1`, and sample with the supplied + /// `sample_function`. #[inline] pub fn sample(&self, subdivisions: i32, sample_function: fn(&Self, f32) -> P) -> Vec

{ (0..=subdivisions) @@ -78,20 +79,20 @@ impl Bezier { .collect() } - /// Split the Bezier curve into `subdivisions` across the length of the curve from t = `0..=1`. - /// sampling the position at each step. + /// Split the Bezier curve into `subdivisions` evenly spaced `t` values across the length of the + /// curve from t = `0..=1`. sampling the position at each step. pub fn to_positions(&self, subdivisions: i32) -> Vec

{ self.sample(subdivisions, Self::position) } - /// Split the Bezier curve into `subdivisions` across the length of the curve from t = `0..=1`. - /// sampling the velocity at each step. + /// Split the Bezier curve into `subdivisions` evenly spaced `t` values across the length of the + /// curve from t = `0..=1`. sampling the velocity at each step. pub fn to_velocities(&self, subdivisions: i32) -> Vec

{ self.sample(subdivisions, Self::velocity) } - /// Split the Bezier curve into `subdivisions` across the length of the curve from t = `0..=1`. - /// sampling the acceleration at each step. + /// Split the Bezier curve into `subdivisions` evenly spaced `t` values across the length of the + /// curve from t = `0..=1` . sampling the acceleration at each step. pub fn to_accelerations(&self, subdivisions: i32) -> Vec

{ self.sample(subdivisions, Self::acceleration) } @@ -120,7 +121,7 @@ impl CubicBezierEasing { const MAX_ERROR: f32 = 1e-7; /// Maximum number of iterations during bezier solve - const MAX_ITERS: u8 = 16; + const MAX_ITERS: u8 = 8; /// Given a `time` within `0..=1`, remaps to a new value using the cubic Bezier curve as a /// shaping function, for which when plotted `x = time` and `y = animation progress`. This will @@ -149,7 +150,8 @@ impl CubicBezierEasing { bezier_impl::velocity([0.0, self.p1.x, self.p2.x, 1.0], t) } - /// Solve for the parametric value `t` that corresponds to the given value of `x`. + /// Solve for the parametric value `t` that corresponds to the given value of `x` using the + /// Newton-Raphson method. #[inline] pub fn find_t_given_x(&self, x: f32) -> f32 { let mut t_guess = x; @@ -159,6 +161,7 @@ impl CubicBezierEasing { if error.abs() <= Self::MAX_ERROR { true } else { + // Using Newton's method, use the tangent line to estimate a better guess value. let slope = self.dx_dt(t_guess); t_guess -= error / slope; false @@ -182,7 +185,7 @@ pub mod bezier_impl { /// Efficiently compute the binomial coefficient #[inline] - fn binomial_coeff(n: usize, k: usize) -> usize { + const fn binomial_coeff(n: usize, k: usize) -> usize { let mut i = 0; let mut result = 1; let k = match k > n - k { @@ -211,10 +214,10 @@ pub mod bezier_impl { /// parametric value `t` with respect to t. #[inline] pub fn velocity(control_points: [P; N], t: f32) -> P { - debug_assert!( - N > 1, - "Velocity can only be computed on Bezier curves with a degree of 1 or higher" - ); + if N <= 1 { + return P::default(); // Zero for numeric types + } + let p = control_points; let degree = N - 1; let degree_vel = N - 2; // the velocity Bezier is one degree lower than the position Bezier @@ -231,10 +234,9 @@ pub mod bezier_impl { /// parametric value `t` with respect to t. #[inline] pub fn acceleration(control_points: [P; N], t: f32) -> P { - debug_assert!( - N > 2, - "Acceleration can only be computed on Bezier curves with a degree of 2 or higher" - ); + if N <= 2 { + return P::default(); // Zero for numeric types + } let p = control_points; let degree = N - 1; let degree_vel = N - 2; // the velocity Bezier is one degree lower than the position Bezier From 67863998da08bed0a02abd7cecf810c9b566582b Mon Sep 17 00:00:00 2001 From: Aevyrie Date: Tue, 14 Feb 2023 12:33:04 -0800 Subject: [PATCH 10/18] Improve easing API --- crates/bevy_math/src/bezier.rs | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/crates/bevy_math/src/bezier.rs b/crates/bevy_math/src/bezier.rs index efda12f7bd622..b3605104abd46 100644 --- a/crates/bevy_math/src/bezier.rs +++ b/crates/bevy_math/src/bezier.rs @@ -106,15 +106,22 @@ impl Bezier { #[derive(Default, Clone, Copy, Debug, PartialEq)] pub struct CubicBezierEasing { /// Control point P1 of the 2D cubic Bezier curve. Controls the start of the animation. - p1: Vec2, + pub p1: Vec2, /// Control point P2 of the 2D cubic Bezier curve. Controls the end of the animation. - p2: Vec2, + pub p2: Vec2, } impl CubicBezierEasing { /// Construct a cubic bezier curve for animation easing, with control points `p1` and `p2`. - pub fn new(p1: Vec2, p2: Vec2) -> Self { - Self { p1, p2 } + /// These correspond to the two free "handles" of the bezier curve. + /// + /// This is a very common tool for animations that accelerate and decelerate smoothly. For + /// example, the ubiquitous "ease-in-out" is defined as `(0.25, 0.1), (0.25, 1.0)`. + pub fn new(p1: impl Into, p2: impl Into) -> Self { + Self { + p1: p1.into(), + p2: p2.into(), + } } /// Maximum allowable error for iterative bezier solve @@ -123,9 +130,10 @@ impl CubicBezierEasing { /// Maximum number of iterations during bezier solve const MAX_ITERS: u8 = 8; - /// Given a `time` within `0..=1`, remaps to a new value using the cubic Bezier curve as a - /// shaping function, for which when plotted `x = time` and `y = animation progress`. This will - /// return `0` when `t = 0`, and `1` when `t = 1`. + /// Given a `time` within `0..=1`, returns an eased value within `0..=1` that follows the cubic + /// Bezier curve instead of a straight line. + /// + /// The start and endpoints will match: ease(0) = 0 and ease(1) = 1. pub fn ease(&self, time: f32) -> f32 { let x = time.clamp(0.0, 1.0); let t = self.find_t_given_x(x); @@ -134,26 +142,26 @@ impl CubicBezierEasing { /// Compute the x-coordinate of the point along the Bezier curve at `t`. #[inline] - pub fn evaluate_x_at(&self, t: f32) -> f32 { + fn evaluate_x_at(&self, t: f32) -> f32 { bezier_impl::position([0.0, self.p1.x, self.p2.x, 1.0], t) } /// Compute the y-coordinate of the point along the Bezier curve at `t`. #[inline] - pub fn evaluate_y_at(&self, t: f32) -> f32 { + fn evaluate_y_at(&self, t: f32) -> f32 { bezier_impl::position([0.0, self.p1.y, self.p2.y, 1.0], t) } /// Compute the slope of the line at the given parametric value `t` with respect to t. #[inline] - pub fn dx_dt(&self, t: f32) -> f32 { + fn dx_dt(&self, t: f32) -> f32 { bezier_impl::velocity([0.0, self.p1.x, self.p2.x, 1.0], t) } /// Solve for the parametric value `t` that corresponds to the given value of `x` using the /// Newton-Raphson method. #[inline] - pub fn find_t_given_x(&self, x: f32) -> f32 { + fn find_t_given_x(&self, x: f32) -> f32 { let mut t_guess = x; (0..Self::MAX_ITERS).any(|_| { let x_guess = self.evaluate_x_at(t_guess); From c8c4eef7b19c85d9b6ca0081e134f01da129b2b5 Mon Sep 17 00:00:00 2001 From: Aevyrie Date: Tue, 14 Feb 2023 15:02:08 -0800 Subject: [PATCH 11/18] Improve docs --- crates/bevy_math/src/bezier.rs | 129 ++++++++++++++++++++++++++++++--- 1 file changed, 119 insertions(+), 10 deletions(-) diff --git a/crates/bevy_math/src/bezier.rs b/crates/bevy_math/src/bezier.rs index b3605104abd46..4765f2ea11a7d 100644 --- a/crates/bevy_math/src/bezier.rs +++ b/crates/bevy_math/src/bezier.rs @@ -24,16 +24,52 @@ impl Point for Vec3A {} // 3D impl Point for Vec2 {} // 2D impl Point for f32 {} // 1D -/// A cubic Bezier curve in 2D space +/// A cubic Bezier spline in 2D space pub type CubicBezier2d = Bezier; -/// A cubic Bezier curve in 3D space +/// A cubic Bezier spline in 3D space pub type CubicBezier3d = Bezier; -/// A quadratic Bezier curve in 2D space +/// A quadratic Bezier spline in 2D space pub type QuadraticBezier2d = Bezier; -/// A quadratic Bezier curve in 3D space +/// A quadratic Bezier spline in 3D space pub type QuadraticBezier3d = Bezier; -/// A Bezier curve with `N` control points, and dimension defined by `P`. +/// A generic Bezier spline with `N` control points, and dimension defined by `P`. +/// +/// The Bezier degree is equal to `N - 1`. For example, a cubic Bezier has 4 control points, and a +/// degree of 3. The time-complexity of evaluating a Bezier increases superlinearly with the number +/// of control points. As such, it is recommended to instead use a chain of quadratic or cubic +/// `Beziers` instead of a high-degree Bezier. +/// +/// ### About Bezier Splines +/// +/// `Bezier` splines are parametric implicit functions; all that means is they take a parameter `t`, +/// and output a point in space, like: +/// +/// > B(t) = (x, y, z) +/// +/// So, all that is needed to find a point in space along a Bezier spline is the parameter `t`. +/// Additionally, the values of `t` are straightforward: `t` is 0 at the start of the spline (first +/// control point) and 1 at the end (last control point). +/// +/// ### Plotting +/// +/// To plot a Bezier spline then, all you need to do is plug in a series of values of `t` in +/// `0..=1`, like \[0.0, 0.2, 0.4, 0.6, 0.8, 1.0\], which will trace out the spline with 6 points. +/// The functions to do this are [`Bezier::position`] to sample the spline at a value of `t`, and +/// [`Bezier::to_positions`] to generate a list of positions given a number of desired subdivisions. +/// +/// ### Velocity and Acceleration +/// +/// In addition to the position of a point on the Bezier spline, it is also useful to get +/// information about the curvature of the spline. Methods are provided to help with this: +/// +/// - [`Bezier::velocity`]: the instantaneous velocity vector with respect to `t`. This is a vector +/// that points in the direction a point is traveling when it is at point `t`. This vector is +/// then tangent to the spline. +/// - [`Bezier::acceleration`]: the instantaneous acceleration vector with respect to `t`. This is a +/// vector that points in the direction a point is accelerating towards when it is at point +/// `t`. This vector will point to the inside of turns, the direction the point is being +/// pulled toward to change direction. #[derive(Clone, Copy, Debug, PartialEq)] pub struct Bezier(pub [P; N]); @@ -133,7 +169,68 @@ impl CubicBezierEasing { /// Given a `time` within `0..=1`, returns an eased value within `0..=1` that follows the cubic /// Bezier curve instead of a straight line. /// - /// The start and endpoints will match: ease(0) = 0 and ease(1) = 1. + /// The start and endpoints will match: `ease(0) = 0` and `ease(1) = 1`. + /// + /// ``` + /// # use bevy_math::CubicBezierEasing; + /// let cubic_bezier = CubicBezierEasing::new((0.25, 0.1), (0.25, 1.0)); + /// assert_eq!(cubic_bezier.ease(0.0), 0.0); + /// assert_eq!(cubic_bezier.ease(1.0), 1.0); + /// ``` + /// + /// ### How cubic Bezier easing works + /// + /// Easing is generally accomplished with the help of "shaping functions". These are curves that + /// start at (0,0) and end at (1,1). The x-axis of this plot is the current `time` of the + /// animation, from 0 to 1. The y-axis is how far along the animation is, also from 0 to 1. You + /// can imagine that if the shaping function is a straight line, there is a 1:1 mapping between + /// the `time` and how far along your animation is. If the `time` = 0.5, the animation is + /// halfway through. This is known as linear interpolation, and results in objects animating + /// with a constant velocity, and no smooth acceleration or deceleration at the start or end. + /// + /// ```ignore + /// y + /// │ ● + /// │ ⬈ + /// │ ⬈ + /// │ ⬈ + /// │ ⬈ + /// ●─────────── x (time) + /// ``` + /// + /// Using cubic Beziers, we have a spline that starts at (0,0), ends at (1,1), and follows a + /// path determined by the two remaining control points (handles). These handles allow us to + /// define a smooth curve. As `time` (x-axis) progresses, we now follow the curved spline, and + /// use the `y` value to determine how far along the animation is. + /// + /// ```ignore + /// y + /// │ ⬈➔➔● + /// │ ⬈ + /// │ ↑ + /// │ ↑ + /// │ ⬈ + /// ●➔➔⬈───────── x (time) + /// ``` + /// + /// To accomplish this, we need to be able to find the position `y` on a Bezier curve, given the + /// `x` value. As discussed in the [`Bezier`] documentation, a Bezier spline is an implicit + /// parametric function like B(t) = (x,y). To find `y`, we first solve for `t` that corresponds + /// to the given `x` (`time`). We use the Newton-Raphson root-finding method to quickly find a + /// value of `t` that matches `x`. Once we have this we can easily plug that `t` into our + /// Bezier's `position` function, to find the `y` component, which is how far along our + /// animation should be. In other words: + /// + /// > Given `time` in `0..=1` + /// + /// > Use Newton's method to find a value of `t` that results in B(t) = (x,y) where `x == time` + /// + /// > Once a solution is found, use the resulting `y` value as the final result + /// + /// ### Performance + /// + /// This operation can be used frequently without fear of performance issues. Benchmarks show + /// this operation taking on the order of 50 nanoseconds. pub fn ease(&self, time: f32) -> f32 { let x = time.clamp(0.0, 1.0); let t = self.find_t_given_x(x); @@ -159,9 +256,19 @@ impl CubicBezierEasing { } /// Solve for the parametric value `t` that corresponds to the given value of `x` using the - /// Newton-Raphson method. + /// Newton-Raphson method. See documentation on [`Self::ease`] for more details. #[inline] fn find_t_given_x(&self, x: f32) -> f32 { + // PERFORMANCE NOTE: + // + // I tried pre-solving and caching 11 values along the curve at struct instantiation in an + // attempt to give the solver a better starting guess. This ended up being slightly slower, + // possibly due to the increased size of the type. Another option would be to store the last + // `t`, and use that, however it's possible this could end up in a bad state where t is very + // far from the naive but generally safe guess of x, e.g. after an animation resets. + // + // Further optimization might not be needed however - benchmarks are showing it takes about + // 50 nanoseconds for an ease operation on my modern laptop, which seems sufficiently fast. let mut t_guess = x; (0..Self::MAX_ITERS).any(|_| { let x_guess = self.evaluate_x_at(t_guess); @@ -191,7 +298,7 @@ pub mod bezier_impl { (1. - t).powi((n - i) as i32) * t.powi(i as i32) } - /// Efficiently compute the binomial coefficient + /// Efficiently compute the binomial coefficient of `n` choose `k`. #[inline] const fn binomial_coeff(n: usize, k: usize) -> usize { let mut i = 0; @@ -219,7 +326,8 @@ pub mod bezier_impl { } /// Compute the first derivative B'(t) of Bezier curve B(t) of degree `N-1` at the given - /// parametric value `t` with respect to t. + /// parametric value `t` with respect to t. Note that the first derivative of a Bezier is also + /// a Bezier, of degree `N-2`. #[inline] pub fn velocity(control_points: [P; N], t: f32) -> P { if N <= 1 { @@ -239,7 +347,8 @@ pub mod bezier_impl { } /// Compute the second derivative B''(t) of Bezier curve B(t) of degree `N-1` at the given - /// parametric value `t` with respect to t. + /// parametric value `t` with respect to t. Note that the second derivative of a Bezier is also + /// a Bezier, of degree `N-3`. #[inline] pub fn acceleration(control_points: [P; N], t: f32) -> P { if N <= 2 { From d0253ca0ae112003f42d9c929bf9719beae9f47a Mon Sep 17 00:00:00 2001 From: Aevyrie Date: Tue, 14 Feb 2023 20:54:59 -0800 Subject: [PATCH 12/18] Doc corrections --- crates/bevy_math/src/bezier.rs | 96 +++++++++++++++++++--------------- crates/bevy_math/src/lib.rs | 10 ++-- 2 files changed, 60 insertions(+), 46 deletions(-) diff --git a/crates/bevy_math/src/bezier.rs b/crates/bevy_math/src/bezier.rs index 4765f2ea11a7d..0a592b1f6dc84 100644 --- a/crates/bevy_math/src/bezier.rs +++ b/crates/bevy_math/src/bezier.rs @@ -24,48 +24,63 @@ impl Point for Vec3A {} // 3D impl Point for Vec2 {} // 2D impl Point for f32 {} // 1D -/// A cubic Bezier spline in 2D space +/// A cubic Bezier curve in 2D space pub type CubicBezier2d = Bezier; -/// A cubic Bezier spline in 3D space +/// A cubic Bezier curve in 3D space pub type CubicBezier3d = Bezier; -/// A quadratic Bezier spline in 2D space +/// A quadratic Bezier curve in 2D space pub type QuadraticBezier2d = Bezier; -/// A quadratic Bezier spline in 3D space +/// A quadratic Bezier curve in 3D space pub type QuadraticBezier3d = Bezier; -/// A generic Bezier spline with `N` control points, and dimension defined by `P`. +/// A generic Bezier curve with `N` control points, and dimension defined by `P`. +/// +/// Consider the following type aliases for most common uses: +/// - [`CubicBezier2d`] +/// - [`CubicBezier3d`] +/// - [`QuadraticBezier2d`] +/// - [`QuadraticBezier3d`] /// /// The Bezier degree is equal to `N - 1`. For example, a cubic Bezier has 4 control points, and a /// degree of 3. The time-complexity of evaluating a Bezier increases superlinearly with the number /// of control points. As such, it is recommended to instead use a chain of quadratic or cubic /// `Beziers` instead of a high-degree Bezier. /// -/// ### About Bezier Splines +/// ### About Bezier curves /// -/// `Bezier` splines are parametric implicit functions; all that means is they take a parameter `t`, +/// `Bezier` curves are parametric implicit functions; all that means is they take a parameter `t`, /// and output a point in space, like: /// /// > B(t) = (x, y, z) /// -/// So, all that is needed to find a point in space along a Bezier spline is the parameter `t`. -/// Additionally, the values of `t` are straightforward: `t` is 0 at the start of the spline (first +/// So, all that is needed to find a point in space along a Bezier curve is the parameter `t`. +/// Additionally, the values of `t` are straightforward: `t` is 0 at the start of the curve (first /// control point) and 1 at the end (last control point). /// +/// ``` +/// # use bevy_math::{Bezier, vec2}; +/// let p0 = vec2(0.25, 0.1); +/// let p1 = vec2(0.25, 1.0); +/// let cubic_bezier = Bezier::new([p0, p1]); +/// assert_eq!(cubic_bezier.position(0.0), p0); +/// assert_eq!(cubic_bezier.position(1.0), p1); +/// ``` +/// /// ### Plotting /// -/// To plot a Bezier spline then, all you need to do is plug in a series of values of `t` in -/// `0..=1`, like \[0.0, 0.2, 0.4, 0.6, 0.8, 1.0\], which will trace out the spline with 6 points. -/// The functions to do this are [`Bezier::position`] to sample the spline at a value of `t`, and +/// To plot a Bezier curve then, all you need to do is plug in a series of values of `t` in `0..=1`, +/// like \[0.0, 0.2, 0.4, 0.6, 0.8, 1.0\], which will trace out the curve with 6 points. The +/// functions to do this are [`Bezier::position`] to sample the curve at a value of `t`, and /// [`Bezier::to_positions`] to generate a list of positions given a number of desired subdivisions. /// /// ### Velocity and Acceleration /// -/// In addition to the position of a point on the Bezier spline, it is also useful to get -/// information about the curvature of the spline. Methods are provided to help with this: +/// In addition to the position of a point on the Bezier curve, it is also useful to get information +/// about the curvature of the curve. Methods are provided to help with this: /// /// - [`Bezier::velocity`]: the instantaneous velocity vector with respect to `t`. This is a vector /// that points in the direction a point is traveling when it is at point `t`. This vector is -/// then tangent to the spline. +/// then tangent to the curve. /// - [`Bezier::acceleration`]: the instantaneous acceleration vector with respect to `t`. This is a /// vector that points in the direction a point is accelerating towards when it is at point /// `t`. This vector will point to the inside of turns, the direction the point is being @@ -85,26 +100,25 @@ impl Bezier { Self(control_points) } - /// Compute the [`Vec3`] position along the Bezier curve at the supplied parametric value `t`. + /// Compute the position along the Bezier curve at the supplied parametric value `t`. pub fn position(&self, t: f32) -> P { - bezier_impl::position(self.0, t) + generic::position(self.0, t) } - /// Compute the first derivative B'(t) of this cubic bezier at `t` with respect to t. This is - /// the instantaneous velocity of a point tracing the Bezier curve from t = 0 to 1. + /// Compute the first derivative B'(t) of this bezier at `t` with respect to t. This is the + /// instantaneous velocity of a point tracing the Bezier curve from t = 0 to 1. pub fn velocity(&self, t: f32) -> P { - bezier_impl::velocity(self.0, t) + generic::velocity(self.0, t) } - /// Compute the second derivative B''(t) of this cubic bezier at `t` with respect to t.This is - /// the instantaneous acceleration of a point tracing the Bezier curve from t = 0 to 1. + /// Compute the second derivative B''(t) of this bezier at `t` with respect to t.This is the + /// instantaneous acceleration of a point tracing the Bezier curve from t = 0 to 1. pub fn acceleration(&self, t: f32) -> P { - bezier_impl::acceleration(self.0, t) + generic::acceleration(self.0, t) } - /// Split the cubic Bezier curve of degree `N-1` into `subdivisions` evenly spaced `t` values - /// across the length of the curve from t = `0..=1`, and sample with the supplied - /// `sample_function`. + /// Split the Bezier curve of degree `N-1` into `subdivisions` evenly spaced `t` values across + /// the length of the curve from t = `0..=1`, and sample with the supplied `sample_function`. #[inline] pub fn sample(&self, subdivisions: i32, sample_function: fn(&Self, f32) -> P) -> Vec

{ (0..=subdivisions) @@ -178,7 +192,7 @@ impl CubicBezierEasing { /// assert_eq!(cubic_bezier.ease(1.0), 1.0); /// ``` /// - /// ### How cubic Bezier easing works + /// # How cubic Bezier easing works /// /// Easing is generally accomplished with the help of "shaping functions". These are curves that /// start at (0,0) and end at (1,1). The x-axis of this plot is the current `time` of the @@ -198,10 +212,10 @@ impl CubicBezierEasing { /// ●─────────── x (time) /// ``` /// - /// Using cubic Beziers, we have a spline that starts at (0,0), ends at (1,1), and follows a - /// path determined by the two remaining control points (handles). These handles allow us to - /// define a smooth curve. As `time` (x-axis) progresses, we now follow the curved spline, and - /// use the `y` value to determine how far along the animation is. + /// Using cubic Beziers, we have a curve that starts at (0,0), ends at (1,1), and follows a path + /// determined by the two remaining control points (handles). These handles allow us to define a + /// smooth curve. As `time` (x-axis) progresses, we now follow the curved curve, and use the `y` + /// value to determine how far along the animation is. /// /// ```ignore /// y @@ -214,7 +228,7 @@ impl CubicBezierEasing { /// ``` /// /// To accomplish this, we need to be able to find the position `y` on a Bezier curve, given the - /// `x` value. As discussed in the [`Bezier`] documentation, a Bezier spline is an implicit + /// `x` value. As discussed in the [`Bezier`] documentation, a Bezier curve is an implicit /// parametric function like B(t) = (x,y). To find `y`, we first solve for `t` that corresponds /// to the given `x` (`time`). We use the Newton-Raphson root-finding method to quickly find a /// value of `t` that matches `x`. Once we have this we can easily plug that `t` into our @@ -227,7 +241,7 @@ impl CubicBezierEasing { /// /// > Once a solution is found, use the resulting `y` value as the final result /// - /// ### Performance + /// # Performance /// /// This operation can be used frequently without fear of performance issues. Benchmarks show /// this operation taking on the order of 50 nanoseconds. @@ -240,19 +254,19 @@ impl CubicBezierEasing { /// Compute the x-coordinate of the point along the Bezier curve at `t`. #[inline] fn evaluate_x_at(&self, t: f32) -> f32 { - bezier_impl::position([0.0, self.p1.x, self.p2.x, 1.0], t) + generic::position([0.0, self.p1.x, self.p2.x, 1.0], t) } /// Compute the y-coordinate of the point along the Bezier curve at `t`. #[inline] fn evaluate_y_at(&self, t: f32) -> f32 { - bezier_impl::position([0.0, self.p1.y, self.p2.y, 1.0], t) + generic::position([0.0, self.p1.y, self.p2.y, 1.0], t) } /// Compute the slope of the line at the given parametric value `t` with respect to t. #[inline] fn dx_dt(&self, t: f32) -> f32 { - bezier_impl::velocity([0.0, self.p1.x, self.p2.x, 1.0], t) + generic::velocity([0.0, self.p1.x, self.p2.x, 1.0], t) } /// Solve for the parametric value `t` that corresponds to the given value of `x` using the @@ -286,9 +300,9 @@ impl CubicBezierEasing { } } -/// Generic implementations for sampling cubic Bezier curves. Consider using the methods on -/// [`Bezier`] for more ergonomic use. -pub mod bezier_impl { +/// Generic implementations for sampling Bezier curves. Consider using the methods on [`Bezier`] for +/// more ergonomic use. +pub mod generic { use super::Point; /// Compute the Bernstein basis polynomial for iteration `i`, for a Bezier curve with with @@ -326,8 +340,8 @@ pub mod bezier_impl { } /// Compute the first derivative B'(t) of Bezier curve B(t) of degree `N-1` at the given - /// parametric value `t` with respect to t. Note that the first derivative of a Bezier is also - /// a Bezier, of degree `N-2`. + /// parametric value `t` with respect to t. Note that the first derivative of a Bezier is also a + /// Bezier, of degree `N-2`. #[inline] pub fn velocity(control_points: [P; N], t: f32) -> P { if N <= 1 { diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index 3c92f919edbe3..e054475f16e4c 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -11,8 +11,8 @@ mod ray; mod rect; pub use bezier::{ - bezier_impl, Bezier, CubicBezier2d, CubicBezier3d, CubicBezierEasing, QuadraticBezier2d, - QuadraticBezier3d, + generic as generic_bezier, Bezier, CubicBezier2d, CubicBezier3d, CubicBezierEasing, + QuadraticBezier2d, QuadraticBezier3d, }; pub use ray::Ray; pub use rect::Rect; @@ -21,9 +21,9 @@ pub use rect::Rect; pub mod prelude { #[doc(hidden)] pub use crate::{ - BVec2, BVec3, BVec4, CubicBezier2d, CubicBezier3d, CubicBezierEasing, EulerRot, IVec2, - IVec3, IVec4, Mat2, Mat3, Mat4, QuadraticBezier2d, QuadraticBezier3d, Quat, Ray, Rect, - UVec2, UVec3, UVec4, Vec2, Vec3, Vec4, + BVec2, BVec3, BVec4, Bezier, CubicBezier2d, CubicBezier3d, CubicBezierEasing, EulerRot, + IVec2, IVec3, IVec4, Mat2, Mat3, Mat4, QuadraticBezier2d, QuadraticBezier3d, Quat, Ray, + Rect, UVec2, UVec3, UVec4, Vec2, Vec3, Vec4, }; } From de415ccab1aef041f1e59462824392ab86cff461 Mon Sep 17 00:00:00 2001 From: Aevyrie Date: Fri, 17 Feb 2023 14:26:39 -0800 Subject: [PATCH 13/18] Mark inline code art as text --- crates/bevy_math/src/bezier.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_math/src/bezier.rs b/crates/bevy_math/src/bezier.rs index 0a592b1f6dc84..bce2483f12dab 100644 --- a/crates/bevy_math/src/bezier.rs +++ b/crates/bevy_math/src/bezier.rs @@ -202,7 +202,7 @@ impl CubicBezierEasing { /// halfway through. This is known as linear interpolation, and results in objects animating /// with a constant velocity, and no smooth acceleration or deceleration at the start or end. /// - /// ```ignore + /// ```text /// y /// │ ● /// │ ⬈ @@ -217,7 +217,7 @@ impl CubicBezierEasing { /// smooth curve. As `time` (x-axis) progresses, we now follow the curved curve, and use the `y` /// value to determine how far along the animation is. /// - /// ```ignore + /// ```text /// y /// │ ⬈➔➔● /// │ ⬈ From b5077e728adb4ccf1e089d1c2ddf52998ea6a9d8 Mon Sep 17 00:00:00 2001 From: Aevyrie Date: Fri, 17 Feb 2023 22:56:50 -0800 Subject: [PATCH 14/18] Update crates/bevy_math/src/bezier.rs Co-authored-by: JoJoJet <21144246+JoJoJet@users.noreply.github.com> --- crates/bevy_math/src/bezier.rs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/crates/bevy_math/src/bezier.rs b/crates/bevy_math/src/bezier.rs index bce2483f12dab..762d6794a5dfe 100644 --- a/crates/bevy_math/src/bezier.rs +++ b/crates/bevy_math/src/bezier.rs @@ -315,18 +315,8 @@ pub mod generic { /// Efficiently compute the binomial coefficient of `n` choose `k`. #[inline] const fn binomial_coeff(n: usize, k: usize) -> usize { - let mut i = 0; - let mut result = 1; - let k = match k > n - k { - true => n - k, - false => k, - }; - while i < k { - result *= n - i; - result /= i + 1; - i += 1; - } - result + let k = usize::min(k, n - k); + (0..k).fold(1, |val, i| val * (n - i) / (i + 1)) } /// Evaluate the Bezier curve B(t) of degree `N-1` at the parametric value `t`. From 7c2ea64df4c8308ffb3a1698b53001d112ae5471 Mon Sep 17 00:00:00 2001 From: Aevyrie Date: Fri, 17 Feb 2023 23:34:14 -0800 Subject: [PATCH 15/18] fn cannot be constant --- crates/bevy_math/src/bezier.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_math/src/bezier.rs b/crates/bevy_math/src/bezier.rs index 762d6794a5dfe..246e80ff143a3 100644 --- a/crates/bevy_math/src/bezier.rs +++ b/crates/bevy_math/src/bezier.rs @@ -314,7 +314,7 @@ pub mod generic { /// Efficiently compute the binomial coefficient of `n` choose `k`. #[inline] - const fn binomial_coeff(n: usize, k: usize) -> usize { + fn binomial_coeff(n: usize, k: usize) -> usize { let k = usize::min(k, n - k); (0..k).fold(1, |val, i| val * (n - i) / (i + 1)) } From 41e6624c12a93b4736f83f1e0738c57c50dc8d89 Mon Sep 17 00:00:00 2001 From: Aevyrie Date: Sun, 19 Feb 2023 22:51:26 -0600 Subject: [PATCH 16/18] Apply suggestions from code review Co-authored-by: James Liu --- crates/bevy_math/src/bezier.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/bevy_math/src/bezier.rs b/crates/bevy_math/src/bezier.rs index 246e80ff143a3..6496ce8451869 100644 --- a/crates/bevy_math/src/bezier.rs +++ b/crates/bevy_math/src/bezier.rs @@ -305,8 +305,10 @@ impl CubicBezierEasing { pub mod generic { use super::Point; - /// Compute the Bernstein basis polynomial for iteration `i`, for a Bezier curve with with + /// Compute the Bernstein basis polynomial for iteration `i`, for a Bezier curve with /// degree `n`, at `t`. + /// + /// For more information, see https://en.wikipedia.org/wiki/Bernstein_polynomial. #[inline] pub fn bernstein_basis(n: usize, i: usize, t: f32) -> f32 { (1. - t).powi((n - i) as i32) * t.powi(i as i32) From 2aa5a94d4221b4e81dd397eb79ec3b72bc918650 Mon Sep 17 00:00:00 2001 From: Aevyrie Date: Mon, 20 Feb 2023 03:00:38 -0800 Subject: [PATCH 17/18] Add tests, update docs, improve constructor --- benches/benches/bevy_math/bezier.rs | 112 +++++++++----- crates/bevy_math/src/bezier.rs | 228 +++++++++++++++++++--------- 2 files changed, 226 insertions(+), 114 deletions(-) diff --git a/benches/benches/bevy_math/bezier.rs b/benches/benches/bevy_math/bezier.rs index b0cea6cee99c3..e925976de22b0 100644 --- a/benches/benches/bevy_math/bezier.rs +++ b/benches/benches/bevy_math/bezier.rs @@ -14,82 +14,116 @@ fn easing(c: &mut Criterion) { } fn fifteen_degree(c: &mut Criterion) { - let bezier = Bezier::new([ - vec3(0.0, 0.0, 0.0), - vec3(0.0, 1.0, 0.0), - vec3(1.0, 0.0, 0.0), - vec3(1.0, 1.0, 1.0), - vec3(0.0, 0.0, 0.0), - vec3(0.0, 1.0, 0.0), - vec3(1.0, 0.0, 0.0), - vec3(1.0, 1.0, 1.0), - vec3(0.0, 0.0, 0.0), - vec3(0.0, 1.0, 0.0), - vec3(1.0, 0.0, 0.0), - vec3(1.0, 1.0, 1.0), - vec3(0.0, 0.0, 0.0), - vec3(0.0, 1.0, 0.0), - vec3(1.0, 0.0, 0.0), - vec3(1.0, 1.0, 1.0), + let bezier = Bezier::::new([ + [0.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [1.0, 0.0, 0.0], + [1.0, 1.0, 1.0], + [0.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [1.0, 0.0, 0.0], + [1.0, 1.0, 1.0], + [0.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [1.0, 0.0, 0.0], + [1.0, 1.0, 1.0], + [0.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [1.0, 0.0, 0.0], + [1.0, 1.0, 1.0], ]); c.bench_function("fifteen_degree_position", |b| { b.iter(|| bezier.position(black_box(0.5))); }); } +fn quadratic_2d(c: &mut Criterion) { + let bezier = QuadraticBezier2d::new([[0.0, 0.0], [0.0, 1.0], [1.0, 1.0]]); + c.bench_function("quadratic_position_Vec2", |b| { + b.iter(|| bezier.position(black_box(0.5))); + }); +} + fn quadratic(c: &mut Criterion) { - let bezier = QuadraticBezier3d::new([ - vec3a(0.0, 0.0, 0.0), - vec3a(0.0, 1.0, 0.0), - vec3a(1.0, 1.0, 1.0), - ]); - c.bench_function("quadratic_position", |b| { + let bezier = QuadraticBezier3d::new([[0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 1.0, 1.0]]); + c.bench_function("quadratic_position_Vec3A", |b| { b.iter(|| bezier.position(black_box(0.5))); }); } fn quadratic_vec3(c: &mut Criterion) { - let bezier = Bezier::new([ - vec3(0.0, 0.0, 0.0), - vec3(0.0, 1.0, 0.0), - vec3(1.0, 1.0, 1.0), - ]); + let bezier = Bezier::::new([[0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 1.0, 1.0]]); c.bench_function("quadratic_position_Vec3", |b| { b.iter(|| bezier.position(black_box(0.5))); }); } +fn cubic_2d(c: &mut Criterion) { + let bezier = CubicBezier2d::new([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]]); + c.bench_function("cubic_position_Vec2", |b| { + b.iter(|| bezier.position(black_box(0.5))); + }); +} + fn cubic(c: &mut Criterion) { let bezier = CubicBezier3d::new([ - vec3a(0.0, 0.0, 0.0), - vec3a(0.0, 1.0, 0.0), - vec3a(1.0, 0.0, 0.0), - vec3a(1.0, 1.0, 1.0), + [0.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [1.0, 0.0, 0.0], + [1.0, 1.0, 1.0], ]); - c.bench_function("cubic_position", |b| { + c.bench_function("cubic_position_Vec3A", |b| { b.iter(|| bezier.position(black_box(0.5))); }); } fn cubic_vec3(c: &mut Criterion) { - let bezier = Bezier::new([ - vec3(0.0, 0.0, 0.0), - vec3(0.0, 1.0, 0.0), - vec3(1.0, 0.0, 0.0), - vec3(1.0, 1.0, 1.0), + let bezier = Bezier::::new([ + [0.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [1.0, 0.0, 0.0], + [1.0, 1.0, 1.0], ]); c.bench_function("cubic_position_Vec3", |b| { b.iter(|| bezier.position(black_box(0.5))); }); } +fn build_pos_cubic(c: &mut Criterion) { + let bezier = CubicBezier3d::new([ + [0.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [1.0, 0.0, 0.0], + [1.0, 1.0, 1.0], + ]); + c.bench_function("build_pos_cubic_100_points", |b| { + b.iter(|| bezier.iter_positions(black_box(100)).collect::>()); + }); +} + +fn build_accel_cubic(c: &mut Criterion) { + let bezier = CubicBezier3d::new([ + [0.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [1.0, 0.0, 0.0], + [1.0, 1.0, 1.0], + ]); + c.bench_function("build_accel_cubic_100_points", |b| { + b.iter(|| bezier.iter_positions(black_box(100)).collect::>()); + }); +} + criterion_group!( benches, easing, fifteen_degree, + quadratic_2d, quadratic, quadratic_vec3, + cubic_2d, cubic, - cubic_vec3 + cubic_vec3, + build_pos_cubic, + build_accel_cubic, ); criterion_main!(benches); diff --git a/crates/bevy_math/src/bezier.rs b/crates/bevy_math/src/bezier.rs index 6496ce8451869..2ce5eb1ee5e2c 100644 --- a/crates/bevy_math/src/bezier.rs +++ b/crates/bevy_math/src/bezier.rs @@ -6,7 +6,7 @@ use std::{ ops::{Add, Mul, Sub}, }; -/// A point in space of any dimension that supports addition and multiplication. +/// A point in space of any dimension that supports the mathematical operations needed by [`Bezier]. pub trait Point: Mul + Add @@ -58,29 +58,28 @@ pub type QuadraticBezier3d = Bezier; /// control point) and 1 at the end (last control point). /// /// ``` -/// # use bevy_math::{Bezier, vec2}; +/// # use bevy_math::{Bezier, Vec2, vec2}; /// let p0 = vec2(0.25, 0.1); /// let p1 = vec2(0.25, 1.0); -/// let cubic_bezier = Bezier::new([p0, p1]); -/// assert_eq!(cubic_bezier.position(0.0), p0); -/// assert_eq!(cubic_bezier.position(1.0), p1); +/// let bezier = Bezier::::new([p0, p1]); +/// assert_eq!(bezier.position(0.0), p0); +/// assert_eq!(bezier.position(1.0), p1); /// ``` /// /// ### Plotting /// -/// To plot a Bezier curve then, all you need to do is plug in a series of values of `t` in `0..=1`, -/// like \[0.0, 0.2, 0.4, 0.6, 0.8, 1.0\], which will trace out the curve with 6 points. The -/// functions to do this are [`Bezier::position`] to sample the curve at a value of `t`, and -/// [`Bezier::to_positions`] to generate a list of positions given a number of desired subdivisions. +/// To plot a Bezier curve, simply plug in a series of values of `t` from zero to one. The functions +/// to do this are [`Bezier::position`] to sample the curve at a value of `t`, and +/// [`Bezier::iter_positions`] to iterate over the curve with a number of subdivisions. /// /// ### Velocity and Acceleration /// /// In addition to the position of a point on the Bezier curve, it is also useful to get information -/// about the curvature of the curve. Methods are provided to help with this: +/// about the curvature. Methods are provided to help with this: /// /// - [`Bezier::velocity`]: the instantaneous velocity vector with respect to `t`. This is a vector /// that points in the direction a point is traveling when it is at point `t`. This vector is -/// then tangent to the curve. +/// tangent to the curve. /// - [`Bezier::acceleration`]: the instantaneous acceleration vector with respect to `t`. This is a /// vector that points in the direction a point is accelerating towards when it is at point /// `t`. This vector will point to the inside of turns, the direction the point is being @@ -95,56 +94,68 @@ impl Default for Bezier { } impl Bezier { - /// Construct a new Bezier curve - pub fn new(control_points: [P; N]) -> Self { + /// Construct a new Bezier curve. + pub fn new(control_points: [impl Into

; N]) -> Self { + let control_points = control_points.map(|v| v.into()); Self(control_points) } - /// Compute the position along the Bezier curve at the supplied parametric value `t`. + /// Compute the position of a point along the Bezier curve at the supplied parametric value `t`. pub fn position(&self, t: f32) -> P { generic::position(self.0, t) } - /// Compute the first derivative B'(t) of this bezier at `t` with respect to t. This is the - /// instantaneous velocity of a point tracing the Bezier curve from t = 0 to 1. + /// Compute the first derivative B'(t) of this Bezier at `t` with respect to t. This is the + /// instantaneous velocity of a point on the Bezier curve at `t`. pub fn velocity(&self, t: f32) -> P { generic::velocity(self.0, t) } - /// Compute the second derivative B''(t) of this bezier at `t` with respect to t.This is the - /// instantaneous acceleration of a point tracing the Bezier curve from t = 0 to 1. + /// Compute the second derivative B''(t) of this Bezier at `t` with respect to t. This is the + /// instantaneous acceleration of a point on the Bezier curve at `t`. pub fn acceleration(&self, t: f32) -> P { generic::acceleration(self.0, t) } - /// Split the Bezier curve of degree `N-1` into `subdivisions` evenly spaced `t` values across - /// the length of the curve from t = `0..=1`, and sample with the supplied `sample_function`. + /// A flexible iterator used to sample [`Bezier`] curves with arbitrary functions. + /// + /// This splits the Bezier into `subdivisions` of evenly spaced `t` values across the length of + /// the curve from start (t = 0) to end (t = 1), returning an iterator that evaluates the curve + /// with the supplied `sample_function` at each `t`. + /// + /// Given `subdivisions = 2`, this will split the curve into two lines, or three points, and + /// return an iterator over those three points, one at the start, middle, and end. #[inline] - pub fn sample(&self, subdivisions: i32, sample_function: fn(&Self, f32) -> P) -> Vec

{ - (0..=subdivisions) - .map(|i| { - let t = i as f32 / subdivisions as f32; - sample_function(self, t) - }) - .collect() + pub fn iter_samples( + &self, + subdivisions: usize, + sample_function: fn(&Self, f32) -> P, + ) -> impl Iterator + '_ { + (0..=subdivisions).map(move |i| { + let t = i as f32 / subdivisions as f32; + sample_function(self, t) + }) } - /// Split the Bezier curve into `subdivisions` evenly spaced `t` values across the length of the - /// curve from t = `0..=1`. sampling the position at each step. - pub fn to_positions(&self, subdivisions: i32) -> Vec

{ - self.sample(subdivisions, Self::position) + /// Iterate over the curve split into `subdivisions`, sampling the position at each step. + pub fn iter_positions(&self, subdivisions: usize) -> impl Iterator + '_ { + self.iter_samples(subdivisions, Self::position) } - /// Split the Bezier curve into `subdivisions` evenly spaced `t` values across the length of the - /// curve from t = `0..=1`. sampling the velocity at each step. - pub fn to_velocities(&self, subdivisions: i32) -> Vec

{ - self.sample(subdivisions, Self::velocity) + /// Iterate over the curve split into `subdivisions`, sampling the velocity at each step. + pub fn iter_velocities(&self, subdivisions: usize) -> impl Iterator + '_ { + self.iter_samples(subdivisions, Self::velocity) } - /// Split the Bezier curve into `subdivisions` evenly spaced `t` values across the length of the - /// curve from t = `0..=1` . sampling the acceleration at each step. - pub fn to_accelerations(&self, subdivisions: i32) -> Vec

{ - self.sample(subdivisions, Self::acceleration) + /// Iterate over the curve split into `subdivisions`, sampling the acceleration at each step. + pub fn iter_accelerations(&self, subdivisions: usize) -> impl Iterator + '_ { + self.iter_samples(subdivisions, Self::acceleration) + } +} + +impl, P: Point, const N: usize> From<[T; N]> for Bezier { + fn from(control_points: [T; N]) -> Self { + Bezier::new(control_points) } } @@ -162,8 +173,8 @@ pub struct CubicBezierEasing { } impl CubicBezierEasing { - /// Construct a cubic bezier curve for animation easing, with control points `p1` and `p2`. - /// These correspond to the two free "handles" of the bezier curve. + /// Construct a cubic Bezier curve for animation easing, with control points `p1` and `p2`. + /// These correspond to the two free "handles" of the Bezier curve. /// /// This is a very common tool for animations that accelerate and decelerate smoothly. For /// example, the ubiquitous "ease-in-out" is defined as `(0.25, 0.1), (0.25, 1.0)`. @@ -174,16 +185,15 @@ impl CubicBezierEasing { } } - /// Maximum allowable error for iterative bezier solve - const MAX_ERROR: f32 = 1e-7; + /// Maximum allowable error for iterative Bezier solve + const MAX_ERROR: f32 = 1e-5; - /// Maximum number of iterations during bezier solve + /// Maximum number of iterations during Bezier solve const MAX_ITERS: u8 = 8; - /// Given a `time` within `0..=1`, returns an eased value within `0..=1` that follows the cubic - /// Bezier curve instead of a straight line. - /// - /// The start and endpoints will match: `ease(0) = 0` and `ease(1) = 1`. + /// Given a `time` within `0..=1`, returns an eased value that follows the cubic Bezier curve + /// instead of a straight line. This eased result may be outside the range `0..=1`, however it + /// will always start at 0 and end at 1: `ease(0) = 0` and `ease(1) = 1`. /// /// ``` /// # use bevy_math::CubicBezierEasing; @@ -214,17 +224,17 @@ impl CubicBezierEasing { /// /// Using cubic Beziers, we have a curve that starts at (0,0), ends at (1,1), and follows a path /// determined by the two remaining control points (handles). These handles allow us to define a - /// smooth curve. As `time` (x-axis) progresses, we now follow the curved curve, and use the `y` - /// value to determine how far along the animation is. + /// smooth curve. As `time` (x-axis) progresses, we now follow the curve, and use the `y` value + /// to determine how far along the animation is. /// /// ```text /// y - /// │ ⬈➔➔● - /// │ ⬈ - /// │ ↑ - /// │ ↑ - /// │ ⬈ - /// ●➔➔⬈───────── x (time) + /// ⬈➔● + /// │ ⬈ + /// │ ↑ + /// │ ↑ + /// │ ⬈ + /// ●➔⬈───────── x (time) /// ``` /// /// To accomplish this, we need to be able to find the position `y` on a Bezier curve, given the @@ -275,40 +285,44 @@ impl CubicBezierEasing { fn find_t_given_x(&self, x: f32) -> f32 { // PERFORMANCE NOTE: // - // I tried pre-solving and caching 11 values along the curve at struct instantiation in an - // attempt to give the solver a better starting guess. This ended up being slightly slower, - // possibly due to the increased size of the type. Another option would be to store the last - // `t`, and use that, however it's possible this could end up in a bad state where t is very - // far from the naive but generally safe guess of x, e.g. after an animation resets. + // I tried pre-solving and caching values along the curve at struct instantiation to give + // the solver a better starting guess. This ended up being slightly slower, possibly due to + // the increased size of the type. Another option would be to store the last `t`, and use + // that, however it's possible this could end up in a bad state where t is very far from the + // naive but generally safe guess of x, e.g. after an animation resets. // // Further optimization might not be needed however - benchmarks are showing it takes about // 50 nanoseconds for an ease operation on my modern laptop, which seems sufficiently fast. let mut t_guess = x; - (0..Self::MAX_ITERS).any(|_| { + for _ in 0..Self::MAX_ITERS { let x_guess = self.evaluate_x_at(t_guess); let error = x_guess - x; if error.abs() <= Self::MAX_ERROR { - true - } else { - // Using Newton's method, use the tangent line to estimate a better guess value. - let slope = self.dx_dt(t_guess); - t_guess -= error / slope; - false + break; } - }); + // Using Newton's method, use the tangent line to estimate a better guess value. + let slope = self.dx_dt(t_guess); + t_guess -= error / slope; + } t_guess.clamp(0.0, 1.0) } } -/// Generic implementations for sampling Bezier curves. Consider using the methods on [`Bezier`] for -/// more ergonomic use. +impl> From<[P; 2]> for CubicBezierEasing { + fn from(points: [P; 2]) -> Self { + let [p0, p1] = points; + CubicBezierEasing::new(p0, p1) + } +} + +/// Generic implementations for sampling Bezier curves. Consider using the methods on +/// [`Bezier`](crate::Bezier) for more ergonomic use. pub mod generic { use super::Point; - /// Compute the Bernstein basis polynomial for iteration `i`, for a Bezier curve with - /// degree `n`, at `t`. + /// Compute the Bernstein basis polynomial `i` of degree `n`, at `t`. /// - /// For more information, see https://en.wikipedia.org/wiki/Bernstein_polynomial. + /// For more information, see [https://en.wikipedia.org/wiki/Bernstein_polynomial]. #[inline] pub fn bernstein_basis(n: usize, i: usize, t: f32) -> f32 { (1. - t).powi((n - i) as i32) * t.powi(i as i32) @@ -339,7 +353,6 @@ pub mod generic { if N <= 1 { return P::default(); // Zero for numeric types } - let p = control_points; let degree = N - 1; let degree_vel = N - 2; // the velocity Bezier is one degree lower than the position Bezier @@ -376,3 +389,68 @@ pub mod generic { .sum() } } + +#[cfg(test)] +mod tests { + use glam::Vec2; + + use crate::{CubicBezier2d, CubicBezierEasing}; + + /// How close two floats can be and still be considered equal + const FLOAT_EQ: f32 = 1e-5; + + /// Basic cubic Bezier easing test to verify the shape of the curve. + #[test] + fn easing_simple() { + // A curve similar to ease-in-out, but symmetric + let bezier = CubicBezierEasing::new([1.0, 0.0], [0.0, 1.0]); + assert_eq!(bezier.ease(0.0), 0.0); + assert!(bezier.ease(0.2) < 0.2); // tests curve + assert_eq!(bezier.ease(0.5), 0.5); // true due to symmetry + assert!(bezier.ease(0.8) > 0.8); // tests curve + assert_eq!(bezier.ease(1.0), 1.0); + } + + /// A curve that forms an upside-down "U", that should extend below 0.0. Useful for animations + /// that go beyond the start and end positions, e.g. bouncing. + #[test] + fn easing_overshoot() { + // A curve that forms an upside-down "U", that should extend above 1.0 + let bezier = CubicBezierEasing::new([0.0, 2.0], [1.0, 2.0]); + assert_eq!(bezier.ease(0.0), 0.0); + assert!(bezier.ease(0.5) > 1.5); + assert_eq!(bezier.ease(1.0), 1.0); + } + + /// A curve that forms a "U", that should extend below 0.0. Useful for animations that go beyond + /// the start and end positions, e.g. bouncing. + #[test] + fn easing_undershoot() { + let bezier = CubicBezierEasing::new([0.0, -2.0], [1.0, -2.0]); + assert_eq!(bezier.ease(0.0), 0.0); + assert!(bezier.ease(0.5) < -0.5); + assert_eq!(bezier.ease(1.0), 1.0); + } + + /// Sweep along the full length of a 3D cubic Bezier, and manually check the position. + #[test] + fn cubic() { + const N_SAMPLES: usize = 1000; + let bezier = CubicBezier2d::new([[-1.0, -20.0], [3.0, 2.0], [5.0, 3.0], [9.0, 8.0]]); + assert_eq!(bezier.position(0.0), bezier.0[0]); // 0 == Start + assert_eq!(bezier.position(1.0), bezier.0[3]); // 1 == End + for i in 0..=N_SAMPLES { + let t = i as f32 / N_SAMPLES as f32; // Check along entire length + assert!(bezier.position(t).distance(cubic_manual(t, bezier)) <= FLOAT_EQ); + } + } + + /// Manual, hardcoded function for computing the position along a cubic bezier. + fn cubic_manual(t: f32, bezier: CubicBezier2d) -> Vec2 { + let [p0, p1, p2, p3] = bezier.0; + p0 * (1.0 - t).powi(3) + + 3.0 * p1 * t * (1.0 - t).powi(2) + + 3.0 * p2 * t.powi(2) * (1.0 - t) + + p3 * t.powi(3) + } +} From 109a17fb1f20e4c2936eedecbbd1d3788de8c8f5 Mon Sep 17 00:00:00 2001 From: Aevyrie Date: Mon, 20 Feb 2023 09:54:57 -0800 Subject: [PATCH 18/18] ci fixes --- crates/bevy_math/src/bezier.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/bevy_math/src/bezier.rs b/crates/bevy_math/src/bezier.rs index 2ce5eb1ee5e2c..520fcebd0b3cf 100644 --- a/crates/bevy_math/src/bezier.rs +++ b/crates/bevy_math/src/bezier.rs @@ -6,7 +6,8 @@ use std::{ ops::{Add, Mul, Sub}, }; -/// A point in space of any dimension that supports the mathematical operations needed by [`Bezier]. +/// A point in space of any dimension that supports the mathematical operations needed by +/// [`Bezier`]. pub trait Point: Mul + Add @@ -322,7 +323,7 @@ pub mod generic { /// Compute the Bernstein basis polynomial `i` of degree `n`, at `t`. /// - /// For more information, see [https://en.wikipedia.org/wiki/Bernstein_polynomial]. + /// For more information, see . #[inline] pub fn bernstein_basis(n: usize, i: usize, t: f32) -> f32 { (1. - t).powi((n - i) as i32) * t.powi(i as i32)