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);
|
||||
|
||||
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;
|
||||
|
@ -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<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() {
|
||||
@ -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;
|
||||
@ -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.
|
||||
@ -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
|
||||
@ -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);
|
||||
/// # }
|
||||
@ -1071,7 +1105,7 @@ impl CubicSegment<Vec2> {
|
||||
/// y
|
||||
/// │ ●
|
||||
/// │ ⬈
|
||||
/// │ ⬈
|
||||
/// │ ⬈
|
||||
/// │ ⬈
|
||||
/// │ ⬈
|
||||
/// ●─────────── x (time)
|
||||
@ -1085,8 +1119,8 @@ impl CubicSegment<Vec2> {
|
||||
/// ```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);
|
||||
|
Loading…
Reference in New Issue
Block a user