From c7531074bc090bdde97ee959b712cbd80ed30d59 Mon Sep 17 00:00:00 2001 From: Ruslan Baynazarov Date: Thu, 27 Feb 2025 01:36:54 +0500 Subject: [PATCH] Improve cubic segment bezier functionality (#17645) # Objective - Fixes #17642 ## Solution - Implemented method `new_bezier(points: [P; 4]) -> Self` for `CubicSegment

` - Old implementation of `new_bezier` is now `new_bezier_easing(p1: impl Into, p2: impl Into) -> Self` (**breaking change**) - ~~added method `new_bezier_with_anchor`, which can make a bezier curve between two points with one control anchor~~ - added methods `iter_positions`, `iter_velocities`, `iter_accelerations`, the same as in `CubicCurve` (**copied code, potentially can be reduced)** - bezier creation logic is moved from `CubicCurve` to `CubicSegment`, removing the unneeded allocation ## Testing - Did you test these changes? If so, how? - Run tests inside `crates/bevy_math/` - Tested the functionality in my project - Are there any parts that need more testing? - Did not run `cargo test` on the whole bevy directory because of OOM - Performance improvements are expected when creating `CubicCurve` with `new_bezier` and `new_bezier_easing`, but not tested - How can other people (reviewers) test your changes? Is there anything specific they need to know? - Use in any code that works created `CubicCurve::new_bezier` - If relevant, what platforms did you test these changes on, and are there any important ones you can't test? - I don't think relevant --- ## Showcase ```rust // Imagine a car goes towards a local target // Create a simple `CubicSegment`, without using heap let planned_path = CubicSegment::new_bezier([ car_pos, car_pos + car_dir * turn_radius, target_point - target_dir * turn_radius, target_point, ]); // Check if the planned path itersect other entities for pos in planned_path.iter_positions(8) { // do some collision checks } ``` ## Migration Guide > This section is optional. If there are no breaking changes, you can delete this section. - Replace `CubicCurve::new_bezier` with `CubicCurve::new_bezier_easing` --- benches/benches/bevy_math/bezier.rs | 5 +- crates/bevy_math/src/cubic_splines/mod.rs | 96 +++++++++++++++-------- 2 files changed, 69 insertions(+), 32 deletions(-) diff --git a/benches/benches/bevy_math/bezier.rs b/benches/benches/bevy_math/bezier.rs index 27affcaa71..a95cb4a821 100644 --- a/benches/benches/bevy_math/bezier.rs +++ b/benches/benches/bevy_math/bezier.rs @@ -8,7 +8,10 @@ use criterion::{ criterion_group!(benches, segment_ease, curve_position, curve_iter_positions); fn segment_ease(c: &mut Criterion) { - let segment = black_box(CubicSegment::new_bezier(vec2(0.25, 0.1), vec2(0.25, 1.0))); + let segment = black_box(CubicSegment::new_bezier_easing( + vec2(0.25, 0.1), + vec2(0.25, 1.0), + )); c.bench_function(bench!("segment_ease"), |b| { let mut t = 0; diff --git a/crates/bevy_math/src/cubic_splines/mod.rs b/crates/bevy_math/src/cubic_splines/mod.rs index 9feedd3170..29e0f643c1 100644 --- a/crates/bevy_math/src/cubic_splines/mod.rs +++ b/crates/bevy_math/src/cubic_splines/mod.rs @@ -15,7 +15,7 @@ use {alloc::vec, alloc::vec::Vec, core::iter::once, itertools::Itertools}; /// A spline composed of a single cubic Bezier curve. /// /// Useful for user-drawn curves with local control, or animation easing. See -/// [`CubicSegment::new_bezier`] for use in easing. +/// [`CubicSegment::new_bezier_easing`] for use in easing. /// /// ### Interpolation /// @@ -73,20 +73,10 @@ impl CubicGenerator

for CubicBezier

{ #[inline] fn to_curve(&self) -> Result, Self::Error> { - // A derivation for this matrix can be found in "General Matrix Representations for B-splines" by Kaihuai Qin. - // - // See section 4.2 and equation 11. - let char_matrix = [ - [1., 0., 0., 0.], - [-3., 3., 0., 0.], - [3., -6., 3., 0.], - [-1., 3., -3., 1.], - ]; - let segments = self .control_points .iter() - .map(|p| CubicSegment::coefficients(*p, char_matrix)) + .map(|p| CubicSegment::new_bezier(*p)) .collect_vec(); if segments.is_empty() { @@ -993,14 +983,21 @@ impl CubicSegment

{ c * 2.0 + d * 6.0 * t } + /// Creates a cubic segment from four points, representing a Bezier curve. + pub fn new_bezier(points: [P; 4]) -> Self { + // A derivation for this matrix can be found in "General Matrix Representations for B-splines" by Kaihuai Qin. + // + // See section 4.2 and equation 11. + let char_matrix = [ + [1., 0., 0., 0.], + [-3., 3., 0., 0.], + [3., -6., 3., 0.], + [-1., 3., -3., 1.], + ]; + Self::coefficients(points, char_matrix) + } + /// Calculate polynomial coefficients for the cubic curve using a characteristic matrix. - #[cfg_attr( - not(feature = "alloc"), - expect( - dead_code, - reason = "Method only used when `alloc` feature is enabled." - ) - )] #[inline] fn coefficients(p: [P; 4], char_matrix: [[f32; 4]; 4]) -> Self { let [c0, c1, c2, c3] = char_matrix; @@ -1014,6 +1011,46 @@ impl CubicSegment

{ ]; Self { coeff } } + + /// A flexible iterator used to sample curves with arbitrary functions. + /// + /// This splits the curve into `subdivisions` of evenly spaced `t` values across the + /// length of the curve from start (t = 0) to end (t = n), where `n = self.segment_count()`, + /// returning an iterator evaluating the curve with the supplied `sample_function` at each `t`. + /// + /// For `subdivisions = 2`, this will split the curve into two lines, or three points, and + /// return an iterator with 3 items, the three points, one at the start, middle, and end. + #[inline] + pub fn iter_samples<'a, 'b: 'a>( + &'b self, + subdivisions: usize, + mut sample_function: impl FnMut(&Self, f32) -> P + 'a, + ) -> impl Iterator + 'a { + self.iter_uniformly(subdivisions) + .map(move |t| sample_function(self, t)) + } + + /// An iterator that returns values of `t` uniformly spaced over `0..=subdivisions`. + #[inline] + fn iter_uniformly(&self, subdivisions: usize) -> impl Iterator { + let step = 1.0 / subdivisions as f32; + (0..=subdivisions).map(move |i| i as f32 * step) + } + + /// 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) + } + + /// 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) + } + + /// 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) + } } /// The `CubicSegment` can be used as a 2-dimensional easing curve for animation. @@ -1029,12 +1066,9 @@ impl CubicSegment { /// This is a very common tool for UI animations that accelerate and decelerate smoothly. For /// example, the ubiquitous "ease-in-out" is defined as `(0.25, 0.1), (0.25, 1.0)`. #[cfg(feature = "alloc")] - pub fn new_bezier(p1: impl Into, p2: impl Into) -> Self { + pub fn new_bezier_easing(p1: impl Into, p2: impl Into) -> Self { let (p0, p3) = (Vec2::ZERO, Vec2::ONE); - let bezier = CubicBezier::new([[p0, p1.into(), p2.into(), p3]]) - .to_curve() - .unwrap(); // Succeeds because resulting curve is guaranteed to have one segment - bezier.segments[0] + Self::new_bezier([p0, p1.into(), p2.into(), p3]) } /// Maximum allowable error for iterative Bezier solve @@ -1051,7 +1085,7 @@ impl CubicSegment { /// # use bevy_math::prelude::*; /// # #[cfg(feature = "alloc")] /// # { - /// let cubic_bezier = CubicSegment::new_bezier((0.25, 0.1), (0.25, 1.0)); + /// let cubic_bezier = CubicSegment::new_bezier_easing((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); /// # } @@ -1071,7 +1105,7 @@ impl CubicSegment { /// y /// │ ● /// │ ⬈ - /// │ ⬈ + /// │ ⬈ /// │ ⬈ /// │ ⬈ /// ●─────────── x (time) @@ -1085,8 +1119,8 @@ impl CubicSegment { /// ```text /// y /// ⬈➔● - /// │ ⬈ - /// │ ↑ + /// │ ⬈ + /// │ ↑ /// │ ↑ /// │ ⬈ /// ●➔⬈───────── x (time) @@ -1656,7 +1690,7 @@ mod tests { #[test] fn easing_simple() { // A curve similar to ease-in-out, but symmetric - let bezier = CubicSegment::new_bezier([1.0, 0.0], [0.0, 1.0]); + let bezier = CubicSegment::new_bezier_easing([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 @@ -1669,7 +1703,7 @@ mod tests { #[test] fn easing_overshoot() { // A curve that forms an upside-down "U", that should extend above 1.0 - let bezier = CubicSegment::new_bezier([0.0, 2.0], [1.0, 2.0]); + let bezier = CubicSegment::new_bezier_easing([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); @@ -1679,7 +1713,7 @@ mod tests { /// the start and end positions, e.g. bouncing. #[test] fn easing_undershoot() { - let bezier = CubicSegment::new_bezier([0.0, -2.0], [1.0, -2.0]); + let bezier = CubicSegment::new_bezier_easing([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);