Skip to content

Commit

Permalink
Improve cubic segment bezier functionality (#17645)
Browse files Browse the repository at this point in the history
# Objective

- Fixes #17642

## Solution

- Implemented method `new_bezier(points: [P; 4]) -> Self` for
`CubicSegment<P>`
- Old implementation of `new_bezier` is now `new_bezier_easing(p1: impl
Into<Vec2>, p2: impl Into<Vec2>) -> 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`
  • Loading branch information
hocop authored Feb 26, 2025
1 parent 94e6fa1 commit c753107
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 32 deletions.
5 changes: 4 additions & 1 deletion benches/benches/bevy_math/bezier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
96 changes: 65 additions & 31 deletions crates/bevy_math/src/cubic_splines/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand Down Expand Up @@ -73,20 +73,10 @@ impl<P: VectorSpace> CubicGenerator<P> for CubicBezier<P> {

#[inline]
fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error> {
// A derivation for this matrix can be found in "General Matrix Representations for B-splines" by Kaihuai Qin.
// <https://xiaoxingchen.github.io/2020/03/02/bspline_in_so3/general_matrix_representation_for_bsplines.pdf>
// 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() {
Expand Down Expand Up @@ -993,14 +983,21 @@ impl<P: VectorSpace> CubicSegment<P> {
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.
// <https://xiaoxingchen.github.io/2020/03/02/bspline_in_so3/general_matrix_representation_for_bsplines.pdf>
// 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;
Expand All @@ -1014,6 +1011,46 @@ impl<P: VectorSpace> CubicSegment<P> {
];
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<Item = P> + '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<Item = f32> {
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<Item = P> + '_ {
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<Item = P> + '_ {
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<Item = P> + '_ {
self.iter_samples(subdivisions, Self::acceleration)
}
}

/// The `CubicSegment<Vec2>` can be used as a 2-dimensional easing curve for animation.
Expand All @@ -1029,12 +1066,9 @@ impl CubicSegment<Vec2> {
/// 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<Vec2>, p2: impl Into<Vec2>) -> Self {
pub fn new_bezier_easing(p1: impl Into<Vec2>, p2: impl Into<Vec2>) -> 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
Expand All @@ -1051,7 +1085,7 @@ impl CubicSegment<Vec2> {
/// # 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);
/// # }
Expand All @@ -1071,7 +1105,7 @@ impl CubicSegment<Vec2> {
/// y
/// │ ●
/// │ ⬈
/// │ ⬈
/// │ ⬈
/// │ ⬈
/// │ ⬈
/// ●─────────── x (time)
Expand All @@ -1085,8 +1119,8 @@ impl CubicSegment<Vec2> {
/// ```text
/// y
/// ⬈➔●
/// │ ⬈
/// │ ↑
/// │ ⬈
/// │ ↑
/// │ ↑
/// │ ⬈
/// ●➔⬈───────── x (time)
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand All @@ -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);
Expand Down

0 comments on commit c753107

Please sign in to comment.