Improve cubic segment bezier functionality (#17645)
# 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`
This commit is contained in:
parent
94e6fa168f
commit
c7531074bc
@ -8,7 +8,10 @@ use criterion::{
|
|||||||
criterion_group!(benches, segment_ease, curve_position, curve_iter_positions);
|
criterion_group!(benches, segment_ease, curve_position, curve_iter_positions);
|
||||||
|
|
||||||
fn segment_ease(c: &mut Criterion) {
|
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| {
|
c.bench_function(bench!("segment_ease"), |b| {
|
||||||
let mut t = 0;
|
let mut t = 0;
|
||||||
|
@ -15,7 +15,7 @@ use {alloc::vec, alloc::vec::Vec, core::iter::once, itertools::Itertools};
|
|||||||
/// A spline composed of a single cubic Bezier curve.
|
/// A spline composed of a single cubic Bezier curve.
|
||||||
///
|
///
|
||||||
/// Useful for user-drawn curves with local control, or animation easing. See
|
/// 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
|
/// ### Interpolation
|
||||||
///
|
///
|
||||||
@ -73,20 +73,10 @@ impl<P: VectorSpace> CubicGenerator<P> for CubicBezier<P> {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error> {
|
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
|
let segments = self
|
||||||
.control_points
|
.control_points
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| CubicSegment::coefficients(*p, char_matrix))
|
.map(|p| CubicSegment::new_bezier(*p))
|
||||||
.collect_vec();
|
.collect_vec();
|
||||||
|
|
||||||
if segments.is_empty() {
|
if segments.is_empty() {
|
||||||
@ -993,14 +983,21 @@ impl<P: VectorSpace> CubicSegment<P> {
|
|||||||
c * 2.0 + d * 6.0 * t
|
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.
|
/// 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]
|
#[inline]
|
||||||
fn coefficients(p: [P; 4], char_matrix: [[f32; 4]; 4]) -> Self {
|
fn coefficients(p: [P; 4], char_matrix: [[f32; 4]; 4]) -> Self {
|
||||||
let [c0, c1, c2, c3] = char_matrix;
|
let [c0, c1, c2, c3] = char_matrix;
|
||||||
@ -1014,6 +1011,46 @@ impl<P: VectorSpace> CubicSegment<P> {
|
|||||||
];
|
];
|
||||||
Self { coeff }
|
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.
|
/// The `CubicSegment<Vec2>` can be used as a 2-dimensional easing curve for animation.
|
||||||
@ -1029,12 +1066,9 @@ impl CubicSegment<Vec2> {
|
|||||||
/// This is a very common tool for UI animations that accelerate and decelerate smoothly. For
|
/// 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)`.
|
/// example, the ubiquitous "ease-in-out" is defined as `(0.25, 0.1), (0.25, 1.0)`.
|
||||||
#[cfg(feature = "alloc")]
|
#[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 (p0, p3) = (Vec2::ZERO, Vec2::ONE);
|
||||||
let bezier = CubicBezier::new([[p0, p1.into(), p2.into(), p3]])
|
Self::new_bezier([p0, p1.into(), p2.into(), p3])
|
||||||
.to_curve()
|
|
||||||
.unwrap(); // Succeeds because resulting curve is guaranteed to have one segment
|
|
||||||
bezier.segments[0]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Maximum allowable error for iterative Bezier solve
|
/// Maximum allowable error for iterative Bezier solve
|
||||||
@ -1051,7 +1085,7 @@ impl CubicSegment<Vec2> {
|
|||||||
/// # use bevy_math::prelude::*;
|
/// # use bevy_math::prelude::*;
|
||||||
/// # #[cfg(feature = "alloc")]
|
/// # #[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(0.0), 0.0);
|
||||||
/// assert_eq!(cubic_bezier.ease(1.0), 1.0);
|
/// assert_eq!(cubic_bezier.ease(1.0), 1.0);
|
||||||
/// # }
|
/// # }
|
||||||
@ -1656,7 +1690,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn easing_simple() {
|
fn easing_simple() {
|
||||||
// A curve similar to ease-in-out, but symmetric
|
// 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_eq!(bezier.ease(0.0), 0.0);
|
||||||
assert!(bezier.ease(0.2) < 0.2); // tests curve
|
assert!(bezier.ease(0.2) < 0.2); // tests curve
|
||||||
assert_eq!(bezier.ease(0.5), 0.5); // true due to symmetry
|
assert_eq!(bezier.ease(0.5), 0.5); // true due to symmetry
|
||||||
@ -1669,7 +1703,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn easing_overshoot() {
|
fn easing_overshoot() {
|
||||||
// A curve that forms an upside-down "U", that should extend above 1.0
|
// 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_eq!(bezier.ease(0.0), 0.0);
|
||||||
assert!(bezier.ease(0.5) > 1.5);
|
assert!(bezier.ease(0.5) > 1.5);
|
||||||
assert_eq!(bezier.ease(1.0), 1.0);
|
assert_eq!(bezier.ease(1.0), 1.0);
|
||||||
@ -1679,7 +1713,7 @@ mod tests {
|
|||||||
/// the start and end positions, e.g. bouncing.
|
/// the start and end positions, e.g. bouncing.
|
||||||
#[test]
|
#[test]
|
||||||
fn easing_undershoot() {
|
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_eq!(bezier.ease(0.0), 0.0);
|
||||||
assert!(bezier.ease(0.5) < -0.5);
|
assert!(bezier.ease(0.5) < -0.5);
|
||||||
assert_eq!(bezier.ease(1.0), 1.0);
|
assert_eq!(bezier.ease(1.0), 1.0);
|
||||||
|
Loading…
Reference in New Issue
Block a user