bevy_math/cubic_splines.rs
1//! Provides types for building cubic splines for rendering curves and use with animation easing.
2
3use core::{fmt::Debug, iter::once};
4
5use crate::{ops::FloatPow, Vec2, VectorSpace};
6
7use derive_more::derive::{Display, Error};
8use itertools::Itertools;
9
10#[cfg(feature = "curve")]
11use crate::curve::{Curve, Interval};
12
13#[cfg(feature = "bevy_reflect")]
14use bevy_reflect::{std_traits::ReflectDefault, Reflect};
15
16/// A spline composed of a single cubic Bezier curve.
17///
18/// Useful for user-drawn curves with local control, or animation easing. See
19/// [`CubicSegment::new_bezier`] for use in easing.
20///
21/// ### Interpolation
22/// The curve only passes through the first and last control point in each set of four points. The curve
23/// is divided into "segments" by every fourth control point.
24///
25/// ### Tangency
26/// Tangents are manually defined by the two intermediate control points within each set of four points.
27/// You can think of the control points the curve passes through as "anchors", and as the intermediate
28/// control points as the anchors displaced along their tangent vectors
29///
30/// ### Continuity
31/// A Bezier curve is at minimum C0 continuous, meaning it has no holes or jumps. Each curve segment is
32/// C2, meaning the tangent vector changes smoothly between each set of four control points, but this
33/// doesn't hold at the control points between segments. Making the whole curve C1 or C2 requires moving
34/// the intermediate control points to align the tangent vectors between segments, and can result in a
35/// loss of local control.
36///
37/// ### Usage
38///
39/// ```
40/// # use bevy_math::{*, prelude::*};
41/// let points = [[
42/// vec2(-1.0, -20.0),
43/// vec2(3.0, 2.0),
44/// vec2(5.0, 3.0),
45/// vec2(9.0, 8.0),
46/// ]];
47/// let bezier = CubicBezier::new(points).to_curve().unwrap();
48/// let positions: Vec<_> = bezier.iter_positions(100).collect();
49/// ```
50#[derive(Clone, Debug)]
51#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
52pub struct CubicBezier<P: VectorSpace> {
53 /// The control points of the Bezier curve.
54 pub control_points: Vec<[P; 4]>,
55}
56
57impl<P: VectorSpace> CubicBezier<P> {
58 /// Create a new cubic Bezier curve from sets of control points.
59 pub fn new(control_points: impl Into<Vec<[P; 4]>>) -> Self {
60 Self {
61 control_points: control_points.into(),
62 }
63 }
64}
65impl<P: VectorSpace> CubicGenerator<P> for CubicBezier<P> {
66 type Error = CubicBezierError;
67
68 #[inline]
69 fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error> {
70 // A derivation for this matrix can be found in "General Matrix Representations for B-splines" by Kaihuai Qin.
71 // <https://xiaoxingchen.github.io/2020/03/02/bspline_in_so3/general_matrix_representation_for_bsplines.pdf>
72 // See section 4.2 and equation 11.
73 let char_matrix = [
74 [1., 0., 0., 0.],
75 [-3., 3., 0., 0.],
76 [3., -6., 3., 0.],
77 [-1., 3., -3., 1.],
78 ];
79
80 let segments = self
81 .control_points
82 .iter()
83 .map(|p| CubicSegment::coefficients(*p, char_matrix))
84 .collect_vec();
85
86 if segments.is_empty() {
87 Err(CubicBezierError)
88 } else {
89 Ok(CubicCurve { segments })
90 }
91 }
92}
93
94/// An error returned during cubic curve generation for cubic Bezier curves indicating that a
95/// segment of control points was not present.
96#[derive(Clone, Debug, Error, Display)]
97#[display("Unable to generate cubic curve: at least one set of control points is required")]
98pub struct CubicBezierError;
99
100/// A spline interpolated continuously between the nearest two control points, with the position and
101/// velocity of the curve specified at both control points. This curve passes through all control
102/// points, with the specified velocity which includes direction and parametric speed.
103///
104/// Useful for smooth interpolation when you know the position and velocity at two points in time,
105/// such as network prediction.
106///
107/// ### Interpolation
108/// The curve passes through every control point.
109///
110/// ### Tangency
111/// Tangents are explicitly defined at each control point.
112///
113/// ### Continuity
114/// The curve is at minimum C1 continuous, meaning that it has no holes or jumps and the tangent vector also
115/// has no sudden jumps.
116///
117/// ### Parametrization
118/// The first segment of the curve connects the first two control points, the second connects the second and
119/// third, and so on. This remains true when a cyclic curve is formed with [`to_curve_cyclic`], in which case
120/// the final curve segment connects the last control point to the first.
121///
122/// ### Usage
123///
124/// ```
125/// # use bevy_math::{*, prelude::*};
126/// let points = [
127/// vec2(-1.0, -20.0),
128/// vec2(3.0, 2.0),
129/// vec2(5.0, 3.0),
130/// vec2(9.0, 8.0),
131/// ];
132/// let tangents = [
133/// vec2(0.0, 1.0),
134/// vec2(0.0, 1.0),
135/// vec2(0.0, 1.0),
136/// vec2(0.0, 1.0),
137/// ];
138/// let hermite = CubicHermite::new(points, tangents).to_curve().unwrap();
139/// let positions: Vec<_> = hermite.iter_positions(100).collect();
140/// ```
141///
142/// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic
143#[derive(Clone, Debug)]
144#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
145pub struct CubicHermite<P: VectorSpace> {
146 /// The control points of the Hermite curve.
147 pub control_points: Vec<(P, P)>,
148}
149impl<P: VectorSpace> CubicHermite<P> {
150 /// Create a new Hermite curve from sets of control points.
151 pub fn new(
152 control_points: impl IntoIterator<Item = P>,
153 tangents: impl IntoIterator<Item = P>,
154 ) -> Self {
155 Self {
156 control_points: control_points.into_iter().zip(tangents).collect(),
157 }
158 }
159
160 /// The characteristic matrix for this spline construction.
161 ///
162 /// Each row of this matrix expresses the coefficients of a [`CubicSegment`] as a linear
163 /// combination of `p_i`, `v_i`, `p_{i+1}`, and `v_{i+1}`, where `(p_i, v_i)` and
164 /// `(p_{i+1}, v_{i+1})` are consecutive control points with tangents.
165 #[inline]
166 fn char_matrix(&self) -> [[f32; 4]; 4] {
167 [
168 [1., 0., 0., 0.],
169 [0., 1., 0., 0.],
170 [-3., -2., 3., -1.],
171 [2., 1., -2., 1.],
172 ]
173 }
174}
175impl<P: VectorSpace> CubicGenerator<P> for CubicHermite<P> {
176 type Error = InsufficientDataError;
177
178 #[inline]
179 fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error> {
180 let segments = self
181 .control_points
182 .windows(2)
183 .map(|p| {
184 let (p0, v0, p1, v1) = (p[0].0, p[0].1, p[1].0, p[1].1);
185 CubicSegment::coefficients([p0, v0, p1, v1], self.char_matrix())
186 })
187 .collect_vec();
188
189 if segments.is_empty() {
190 Err(InsufficientDataError {
191 expected: 2,
192 given: self.control_points.len(),
193 })
194 } else {
195 Ok(CubicCurve { segments })
196 }
197 }
198}
199impl<P: VectorSpace> CyclicCubicGenerator<P> for CubicHermite<P> {
200 type Error = InsufficientDataError;
201
202 #[inline]
203 fn to_curve_cyclic(&self) -> Result<CubicCurve<P>, Self::Error> {
204 let segments = self
205 .control_points
206 .iter()
207 .circular_tuple_windows()
208 .map(|(&j0, &j1)| {
209 let (p0, v0, p1, v1) = (j0.0, j0.1, j1.0, j1.1);
210 CubicSegment::coefficients([p0, v0, p1, v1], self.char_matrix())
211 })
212 .collect_vec();
213
214 if segments.is_empty() {
215 Err(InsufficientDataError {
216 expected: 2,
217 given: self.control_points.len(),
218 })
219 } else {
220 Ok(CubicCurve { segments })
221 }
222 }
223}
224
225/// A spline interpolated continuously across the nearest four control points, with the position of
226/// the curve specified at every control point and the tangents computed automatically. The associated [`CubicCurve`]
227/// has one segment between each pair of adjacent control points.
228///
229/// **Note** the Catmull-Rom spline is a special case of Cardinal spline where the tension is 0.5.
230///
231/// ### Interpolation
232/// The curve passes through every control point.
233///
234/// ### Tangency
235/// Tangents are automatically computed based on the positions of control points.
236///
237/// ### Continuity
238/// The curve is at minimum C1, meaning that it is continuous (it has no holes or jumps), and its tangent
239/// vector is also well-defined everywhere, without sudden jumps.
240///
241/// ### Parametrization
242/// The first segment of the curve connects the first two control points, the second connects the second and
243/// third, and so on. This remains true when a cyclic curve is formed with [`to_curve_cyclic`], in which case
244/// the final curve segment connects the last control point to the first.
245///
246/// ### Usage
247///
248/// ```
249/// # use bevy_math::{*, prelude::*};
250/// let points = [
251/// vec2(-1.0, -20.0),
252/// vec2(3.0, 2.0),
253/// vec2(5.0, 3.0),
254/// vec2(9.0, 8.0),
255/// ];
256/// let cardinal = CubicCardinalSpline::new(0.3, points).to_curve().unwrap();
257/// let positions: Vec<_> = cardinal.iter_positions(100).collect();
258/// ```
259///
260/// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic
261#[derive(Clone, Debug)]
262#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
263pub struct CubicCardinalSpline<P: VectorSpace> {
264 /// Tension
265 pub tension: f32,
266 /// The control points of the Cardinal spline
267 pub control_points: Vec<P>,
268}
269
270impl<P: VectorSpace> CubicCardinalSpline<P> {
271 /// Build a new Cardinal spline.
272 pub fn new(tension: f32, control_points: impl Into<Vec<P>>) -> Self {
273 Self {
274 tension,
275 control_points: control_points.into(),
276 }
277 }
278
279 /// Build a new Catmull-Rom spline, the special case of a Cardinal spline where tension = 1/2.
280 pub fn new_catmull_rom(control_points: impl Into<Vec<P>>) -> Self {
281 Self {
282 tension: 0.5,
283 control_points: control_points.into(),
284 }
285 }
286
287 /// The characteristic matrix for this spline construction.
288 ///
289 /// Each row of this matrix expresses the coefficients of a [`CubicSegment`] as a linear
290 /// combination of four consecutive control points.
291 #[inline]
292 fn char_matrix(&self) -> [[f32; 4]; 4] {
293 let s = self.tension;
294 [
295 [0., 1., 0., 0.],
296 [-s, 0., s, 0.],
297 [2. * s, s - 3., 3. - 2. * s, -s],
298 [-s, 2. - s, s - 2., s],
299 ]
300 }
301}
302impl<P: VectorSpace> CubicGenerator<P> for CubicCardinalSpline<P> {
303 type Error = InsufficientDataError;
304
305 #[inline]
306 fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error> {
307 let length = self.control_points.len();
308
309 // Early return to avoid accessing an invalid index
310 if length < 2 {
311 return Err(InsufficientDataError {
312 expected: 2,
313 given: self.control_points.len(),
314 });
315 }
316
317 // Extend the list of control points by mirroring the last second-to-last control points on each end;
318 // this allows tangents for the endpoints to be provided, and the overall effect is that the tangent
319 // at an endpoint is proportional to twice the vector between it and its adjacent control point.
320 //
321 // The expression used here is P_{-1} := P_0 - (P_1 - P_0) = 2P_0 - P_1. (Analogously at the other end.)
322 let mirrored_first = self.control_points[0] * 2. - self.control_points[1];
323 let mirrored_last = self.control_points[length - 1] * 2. - self.control_points[length - 2];
324 let extended_control_points = once(&mirrored_first)
325 .chain(self.control_points.iter())
326 .chain(once(&mirrored_last));
327
328 let segments = extended_control_points
329 .tuple_windows()
330 .map(|(&p0, &p1, &p2, &p3)| {
331 CubicSegment::coefficients([p0, p1, p2, p3], self.char_matrix())
332 })
333 .collect_vec();
334
335 Ok(CubicCurve { segments })
336 }
337}
338impl<P: VectorSpace> CyclicCubicGenerator<P> for CubicCardinalSpline<P> {
339 type Error = InsufficientDataError;
340
341 #[inline]
342 fn to_curve_cyclic(&self) -> Result<CubicCurve<P>, Self::Error> {
343 let len = self.control_points.len();
344
345 if len < 2 {
346 return Err(InsufficientDataError {
347 expected: 2,
348 given: self.control_points.len(),
349 });
350 }
351
352 // This would ordinarily be the last segment, but we pick it out so that we can make it first
353 // in order to get a desirable parametrization where the first segment connects the first two
354 // control points instead of the second and third.
355 let first_segment = {
356 // We take the indices mod `len` in case `len` is very small.
357 let p0 = self.control_points[len - 1];
358 let p1 = self.control_points[0];
359 let p2 = self.control_points[1 % len];
360 let p3 = self.control_points[2 % len];
361 CubicSegment::coefficients([p0, p1, p2, p3], self.char_matrix())
362 };
363
364 let later_segments = self
365 .control_points
366 .iter()
367 .circular_tuple_windows()
368 .map(|(&p0, &p1, &p2, &p3)| {
369 CubicSegment::coefficients([p0, p1, p2, p3], self.char_matrix())
370 })
371 .take(len - 1);
372
373 let mut segments = Vec::with_capacity(len);
374 segments.push(first_segment);
375 segments.extend(later_segments);
376
377 Ok(CubicCurve { segments })
378 }
379}
380
381/// A spline interpolated continuously across the nearest four control points. The curve does not
382/// necessarily pass through any of the control points.
383///
384/// ### Interpolation
385/// The curve does not necessarily pass through its control points.
386///
387/// ### Tangency
388/// Tangents are automatically computed based on the positions of control points.
389///
390/// ### Continuity
391/// The curve is C2 continuous, meaning it has no holes or jumps, the tangent vector changes smoothly along
392/// the entire curve, and the acceleration also varies continuously. The acceleration continuity of this
393/// spline makes it useful for camera paths.
394///
395/// ### Parametrization
396/// Each curve segment is defined by a window of four control points taken in sequence. When [`to_curve_cyclic`]
397/// is used to form a cyclic curve, the three additional segments used to close the curve come last.
398///
399/// ### Usage
400///
401/// ```
402/// # use bevy_math::{*, prelude::*};
403/// let points = [
404/// vec2(-1.0, -20.0),
405/// vec2(3.0, 2.0),
406/// vec2(5.0, 3.0),
407/// vec2(9.0, 8.0),
408/// ];
409/// let b_spline = CubicBSpline::new(points).to_curve().unwrap();
410/// let positions: Vec<_> = b_spline.iter_positions(100).collect();
411/// ```
412///
413/// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic
414#[derive(Clone, Debug)]
415#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
416pub struct CubicBSpline<P: VectorSpace> {
417 /// The control points of the spline
418 pub control_points: Vec<P>,
419}
420impl<P: VectorSpace> CubicBSpline<P> {
421 /// Build a new B-Spline.
422 pub fn new(control_points: impl Into<Vec<P>>) -> Self {
423 Self {
424 control_points: control_points.into(),
425 }
426 }
427
428 /// The characteristic matrix for this spline construction.
429 ///
430 /// Each row of this matrix expresses the coefficients of a [`CubicSegment`] as a linear
431 /// combination of four consecutive control points.
432 #[inline]
433 fn char_matrix(&self) -> [[f32; 4]; 4] {
434 // A derivation for this matrix can be found in "General Matrix Representations for B-splines" by Kaihuai Qin.
435 // <https://xiaoxingchen.github.io/2020/03/02/bspline_in_so3/general_matrix_representation_for_bsplines.pdf>
436 // See section 4.1 and equations 7 and 8.
437 let mut char_matrix = [
438 [1.0, 4.0, 1.0, 0.0],
439 [-3.0, 0.0, 3.0, 0.0],
440 [3.0, -6.0, 3.0, 0.0],
441 [-1.0, 3.0, -3.0, 1.0],
442 ];
443
444 char_matrix
445 .iter_mut()
446 .for_each(|r| r.iter_mut().for_each(|c| *c /= 6.0));
447
448 char_matrix
449 }
450}
451impl<P: VectorSpace> CubicGenerator<P> for CubicBSpline<P> {
452 type Error = InsufficientDataError;
453
454 #[inline]
455 fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error> {
456 let segments = self
457 .control_points
458 .windows(4)
459 .map(|p| CubicSegment::coefficients([p[0], p[1], p[2], p[3]], self.char_matrix()))
460 .collect_vec();
461
462 if segments.is_empty() {
463 Err(InsufficientDataError {
464 expected: 4,
465 given: self.control_points.len(),
466 })
467 } else {
468 Ok(CubicCurve { segments })
469 }
470 }
471}
472
473impl<P: VectorSpace> CyclicCubicGenerator<P> for CubicBSpline<P> {
474 type Error = InsufficientDataError;
475
476 #[inline]
477 fn to_curve_cyclic(&self) -> Result<CubicCurve<P>, Self::Error> {
478 let segments = self
479 .control_points
480 .iter()
481 .circular_tuple_windows()
482 .map(|(&a, &b, &c, &d)| CubicSegment::coefficients([a, b, c, d], self.char_matrix()))
483 .collect_vec();
484
485 // Note that the parametrization is consistent with the one for `to_curve` but with
486 // the extra curve segments all tacked on at the end. This might be slightly counter-intuitive,
487 // since it means the first segment doesn't go "between" the first two control points, but
488 // between the second and third instead.
489
490 if segments.is_empty() {
491 Err(InsufficientDataError {
492 expected: 2,
493 given: self.control_points.len(),
494 })
495 } else {
496 Ok(CubicCurve { segments })
497 }
498 }
499}
500
501/// Error during construction of [`CubicNurbs`]
502#[derive(Clone, Debug, Error, Display)]
503pub enum CubicNurbsError {
504 /// Provided the wrong number of knots.
505 #[display("Wrong number of knots: expected {expected}, provided {provided}")]
506 KnotsNumberMismatch {
507 /// Expected number of knots
508 expected: usize,
509 /// Provided number of knots
510 provided: usize,
511 },
512 /// The provided knots had a descending knot pair. Subsequent knots must
513 /// either increase or stay the same.
514 #[display("Invalid knots: contains descending knot pair")]
515 DescendingKnots,
516 /// The provided knots were all equal. Knots must contain at least one increasing pair.
517 #[display("Invalid knots: all knots are equal")]
518 ConstantKnots,
519 /// Provided a different number of weights and control points.
520 #[display("Incorrect number of weights: expected {expected}, provided {provided}")]
521 WeightsNumberMismatch {
522 /// Expected number of weights
523 expected: usize,
524 /// Provided number of weights
525 provided: usize,
526 },
527 /// The number of control points provided is less than 4.
528 #[display("Not enough control points, at least 4 are required, {provided} were provided")]
529 NotEnoughControlPoints {
530 /// The number of control points provided
531 provided: usize,
532 },
533}
534
535/// Non-uniform Rational B-Splines (NURBS) are a powerful generalization of the [`CubicBSpline`] which can
536/// represent a much more diverse class of curves (like perfect circles and ellipses).
537///
538/// ### Non-uniformity
539/// The 'NU' part of NURBS stands for "Non-Uniform". This has to do with a parameter called 'knots'.
540/// The knots are a non-decreasing sequence of floating point numbers. The first and last three pairs of
541/// knots control the behavior of the curve as it approaches its endpoints. The intermediate pairs
542/// each control the length of one segment of the curve. Multiple repeated knot values are called
543/// "knot multiplicity". Knot multiplicity in the intermediate knots causes a "zero-length" segment,
544/// and can create sharp corners.
545///
546/// ### Rationality
547/// The 'R' part of NURBS stands for "Rational". This has to do with NURBS allowing each control point to
548/// be assigned a weighting, which controls how much it affects the curve compared to the other points.
549///
550/// ### Interpolation
551/// The curve will not pass through the control points except where a knot has multiplicity four.
552///
553/// ### Tangency
554/// Tangents are automatically computed based on the position of control points.
555///
556/// ### Continuity
557/// When there is no knot multiplicity, the curve is C2 continuous, meaning it has no holes or jumps and the
558/// tangent vector changes smoothly along the entire curve length. Like the [`CubicBSpline`], the acceleration
559/// continuity makes it useful for camera paths. Knot multiplicity of 2 in intermediate knots reduces the
560/// continuity to C1, and knot multiplicity of 3 reduces the continuity to C0. The curve is always at least
561/// C0, meaning it has no jumps or holes.
562///
563/// ### Usage
564///
565/// ```
566/// # use bevy_math::{*, prelude::*};
567/// let points = [
568/// vec2(-1.0, -20.0),
569/// vec2(3.0, 2.0),
570/// vec2(5.0, 3.0),
571/// vec2(9.0, 8.0),
572/// ];
573/// let weights = [1.0, 1.0, 2.0, 1.0];
574/// let knots = [0.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 5.0];
575/// let nurbs = CubicNurbs::new(points, Some(weights), Some(knots))
576/// .expect("NURBS construction failed!")
577/// .to_curve()
578/// .unwrap();
579/// let positions: Vec<_> = nurbs.iter_positions(100).collect();
580/// ```
581#[derive(Clone, Debug)]
582#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
583pub struct CubicNurbs<P: VectorSpace> {
584 /// The control points of the NURBS
585 pub control_points: Vec<P>,
586 /// Weights
587 pub weights: Vec<f32>,
588 /// Knots
589 pub knots: Vec<f32>,
590}
591impl<P: VectorSpace> CubicNurbs<P> {
592 /// Build a Non-Uniform Rational B-Spline.
593 ///
594 /// If provided, weights must be the same length as the control points. Defaults to equal weights.
595 ///
596 /// If provided, the number of knots must be n + 4 elements, where n is the amount of control
597 /// points. Defaults to open uniform knots: [`Self::open_uniform_knots`]. Knots cannot
598 /// all be equal.
599 ///
600 /// At least 4 points must be provided, otherwise an error will be returned.
601 pub fn new(
602 control_points: impl Into<Vec<P>>,
603 weights: Option<impl Into<Vec<f32>>>,
604 knots: Option<impl Into<Vec<f32>>>,
605 ) -> Result<Self, CubicNurbsError> {
606 let mut control_points: Vec<P> = control_points.into();
607 let control_points_len = control_points.len();
608
609 if control_points_len < 4 {
610 return Err(CubicNurbsError::NotEnoughControlPoints {
611 provided: control_points_len,
612 });
613 }
614
615 let weights = weights
616 .map(Into::into)
617 .unwrap_or_else(|| vec![1.0; control_points_len]);
618
619 let mut knots: Vec<f32> = knots.map(Into::into).unwrap_or_else(|| {
620 Self::open_uniform_knots(control_points_len)
621 .expect("The amount of control points was checked")
622 });
623
624 let expected_knots_len = Self::knots_len(control_points_len);
625
626 // Check the number of knots is correct
627 if knots.len() != expected_knots_len {
628 return Err(CubicNurbsError::KnotsNumberMismatch {
629 expected: expected_knots_len,
630 provided: knots.len(),
631 });
632 }
633
634 // Ensure the knots are non-descending (previous element is less than or equal
635 // to the next)
636 if knots.windows(2).any(|win| win[0] > win[1]) {
637 return Err(CubicNurbsError::DescendingKnots);
638 }
639
640 // Ensure the knots are non-constant
641 if knots.windows(2).all(|win| win[0] == win[1]) {
642 return Err(CubicNurbsError::ConstantKnots);
643 }
644
645 // Check that the number of weights equals the number of control points
646 if weights.len() != control_points_len {
647 return Err(CubicNurbsError::WeightsNumberMismatch {
648 expected: control_points_len,
649 provided: weights.len(),
650 });
651 }
652
653 // To align the evaluation behavior of nurbs with the other splines,
654 // make the intervals between knots form an exact cover of [0, N], where N is
655 // the number of segments of the final curve.
656 let curve_length = (control_points.len() - 3) as f32;
657 let min = *knots.first().unwrap();
658 let max = *knots.last().unwrap();
659 let knot_delta = max - min;
660 knots = knots
661 .into_iter()
662 .map(|k| k - min)
663 .map(|k| k * curve_length / knot_delta)
664 .collect();
665
666 control_points
667 .iter_mut()
668 .zip(weights.iter())
669 .for_each(|(p, w)| *p = *p * *w);
670
671 Ok(Self {
672 control_points,
673 weights,
674 knots,
675 })
676 }
677
678 /// Generates uniform knots that will generate the same curve as [`CubicBSpline`].
679 ///
680 /// "Uniform" means that the difference between two subsequent knots is the same.
681 ///
682 /// Will return `None` if there are less than 4 control points.
683 pub fn uniform_knots(control_points: usize) -> Option<Vec<f32>> {
684 if control_points < 4 {
685 return None;
686 }
687 Some(
688 (0..Self::knots_len(control_points))
689 .map(|v| v as f32)
690 .collect(),
691 )
692 }
693
694 /// Generates open uniform knots, which makes the ends of the curve pass through the
695 /// start and end points.
696 ///
697 /// The start and end knots have multiplicity 4, and intermediate knots have multiplicity 0 and
698 /// difference of 1.
699 ///
700 /// Will return `None` if there are less than 4 control points.
701 pub fn open_uniform_knots(control_points: usize) -> Option<Vec<f32>> {
702 if control_points < 4 {
703 return None;
704 }
705 let last_knots_value = control_points - 3;
706 Some(
707 core::iter::repeat(0.0)
708 .take(4)
709 .chain((1..last_knots_value).map(|v| v as f32))
710 .chain(core::iter::repeat(last_knots_value as f32).take(4))
711 .collect(),
712 )
713 }
714
715 #[inline(always)]
716 const fn knots_len(control_points_len: usize) -> usize {
717 control_points_len + 4
718 }
719
720 /// Generates a non-uniform B-spline characteristic matrix from a sequence of six knots. Each six
721 /// knots describe the relationship between four successive control points. For padding reasons,
722 /// this takes a vector of 8 knots, but only six are actually used.
723 fn generate_matrix(knots: &[f32; 8]) -> [[f32; 4]; 4] {
724 // A derivation for this matrix can be found in "General Matrix Representations for B-splines" by Kaihuai Qin.
725 // <https://xiaoxingchen.github.io/2020/03/02/bspline_in_so3/general_matrix_representation_for_bsplines.pdf>
726 // See section 3.1.
727
728 let t = knots;
729 // In the notation of the paper:
730 // t[1] := t_i-2
731 // t[2] := t_i-1
732 // t[3] := t_i (the lower extent of the current knot span)
733 // t[4] := t_i+1 (the upper extent of the current knot span)
734 // t[5] := t_i+2
735 // t[6] := t_i+3
736
737 let m00 = (t[4] - t[3]).squared() / ((t[4] - t[2]) * (t[4] - t[1]));
738 let m02 = (t[3] - t[2]).squared() / ((t[5] - t[2]) * (t[4] - t[2]));
739 let m12 = (3.0 * (t[4] - t[3]) * (t[3] - t[2])) / ((t[5] - t[2]) * (t[4] - t[2]));
740 let m22 = 3.0 * (t[4] - t[3]).squared() / ((t[5] - t[2]) * (t[4] - t[2]));
741 let m33 = (t[4] - t[3]).squared() / ((t[6] - t[3]) * (t[5] - t[3]));
742 let m32 = -m22 / 3.0 - m33 - (t[4] - t[3]).squared() / ((t[5] - t[3]) * (t[5] - t[2]));
743 [
744 [m00, 1.0 - m00 - m02, m02, 0.0],
745 [-3.0 * m00, 3.0 * m00 - m12, m12, 0.0],
746 [3.0 * m00, -3.0 * m00 - m22, m22, 0.0],
747 [-m00, m00 - m32 - m33, m32, m33],
748 ]
749 }
750}
751impl<P: VectorSpace> RationalGenerator<P> for CubicNurbs<P> {
752 type Error = InsufficientDataError;
753
754 #[inline]
755 fn to_curve(&self) -> Result<RationalCurve<P>, Self::Error> {
756 let segments = self
757 .control_points
758 .windows(4)
759 .zip(self.weights.windows(4))
760 .zip(self.knots.windows(8))
761 .filter(|(_, knots)| knots[4] - knots[3] > 0.0)
762 .map(|((points, weights), knots)| {
763 // This is curve segment i. It uses control points P_i, P_i+2, P_i+2 and P_i+3,
764 // It is associated with knot span i+3 (which is the interval between knots i+3
765 // and i+4) and its characteristic matrix uses knots i+1 through i+6 (because
766 // those define the two knot spans on either side).
767 let span = knots[4] - knots[3];
768 let coefficient_knots = knots.try_into().expect("Knot windows are of length 6");
769 let matrix = Self::generate_matrix(coefficient_knots);
770 RationalSegment::coefficients(
771 points.try_into().expect("Point windows are of length 4"),
772 weights.try_into().expect("Weight windows are of length 4"),
773 span,
774 matrix,
775 )
776 })
777 .collect_vec();
778 if segments.is_empty() {
779 Err(InsufficientDataError {
780 expected: 4,
781 given: self.control_points.len(),
782 })
783 } else {
784 Ok(RationalCurve { segments })
785 }
786 }
787}
788
789/// A spline interpolated linearly between the nearest 2 points.
790///
791/// ### Interpolation
792/// The curve passes through every control point.
793///
794/// ### Tangency
795/// The curve is not generally differentiable at control points.
796///
797/// ### Continuity
798/// The curve is C0 continuous, meaning it has no holes or jumps.
799///
800/// ### Parametrization
801/// Each curve segment connects two adjacent control points in sequence. When a cyclic curve is
802/// formed with [`to_curve_cyclic`], the final segment connects the last control point with the first.
803///
804/// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic
805#[derive(Clone, Debug)]
806#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
807pub struct LinearSpline<P: VectorSpace> {
808 /// The control points of the linear spline.
809 pub points: Vec<P>,
810}
811impl<P: VectorSpace> LinearSpline<P> {
812 /// Create a new linear spline from a list of points to be interpolated.
813 pub fn new(points: impl Into<Vec<P>>) -> Self {
814 Self {
815 points: points.into(),
816 }
817 }
818}
819impl<P: VectorSpace> CubicGenerator<P> for LinearSpline<P> {
820 type Error = InsufficientDataError;
821
822 #[inline]
823 fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error> {
824 let segments = self
825 .points
826 .windows(2)
827 .map(|points| {
828 let a = points[0];
829 let b = points[1];
830 CubicSegment {
831 coeff: [a, b - a, P::default(), P::default()],
832 }
833 })
834 .collect_vec();
835
836 if segments.is_empty() {
837 Err(InsufficientDataError {
838 expected: 2,
839 given: self.points.len(),
840 })
841 } else {
842 Ok(CubicCurve { segments })
843 }
844 }
845}
846impl<P: VectorSpace> CyclicCubicGenerator<P> for LinearSpline<P> {
847 type Error = InsufficientDataError;
848
849 #[inline]
850 fn to_curve_cyclic(&self) -> Result<CubicCurve<P>, Self::Error> {
851 let segments = self
852 .points
853 .iter()
854 .circular_tuple_windows()
855 .map(|(&a, &b)| CubicSegment {
856 coeff: [a, b - a, P::default(), P::default()],
857 })
858 .collect_vec();
859
860 if segments.is_empty() {
861 Err(InsufficientDataError {
862 expected: 2,
863 given: self.points.len(),
864 })
865 } else {
866 Ok(CubicCurve { segments })
867 }
868 }
869}
870
871/// An error indicating that a spline construction didn't have enough control points to generate a curve.
872#[derive(Clone, Debug, Error, Display)]
873#[display("Not enough data to build curve: needed at least {expected} control points but was only given {given}")]
874pub struct InsufficientDataError {
875 expected: usize,
876 given: usize,
877}
878
879/// Implement this on cubic splines that can generate a cubic curve from their spline parameters.
880pub trait CubicGenerator<P: VectorSpace> {
881 /// An error type indicating why construction might fail.
882 type Error;
883
884 /// Build a [`CubicCurve`] by computing the interpolation coefficients for each curve segment.
885 fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error>;
886}
887
888/// Implement this on cubic splines that can generate a cyclic cubic curve from their spline parameters.
889///
890/// This makes sense only when the control data can be interpreted cyclically.
891pub trait CyclicCubicGenerator<P: VectorSpace> {
892 /// An error type indicating why construction might fail.
893 type Error;
894
895 /// Build a cyclic [`CubicCurve`] by computing the interpolation coefficients for each curve segment,
896 /// treating the control data as cyclic so that the result is a closed curve.
897 fn to_curve_cyclic(&self) -> Result<CubicCurve<P>, Self::Error>;
898}
899
900/// A segment of a cubic curve, used to hold precomputed coefficients for fast interpolation.
901/// It is a [`Curve`] with domain `[0, 1]`.
902///
903/// Segments can be chained together to form a longer [compound curve].
904///
905/// [compound curve]: CubicCurve
906#[derive(Copy, Clone, Debug, Default, PartialEq)]
907#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
908#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Default))]
909pub struct CubicSegment<P: VectorSpace> {
910 /// Polynomial coefficients for the segment.
911 pub coeff: [P; 4],
912}
913
914impl<P: VectorSpace> CubicSegment<P> {
915 /// Instantaneous position of a point at parametric value `t`.
916 #[inline]
917 pub fn position(&self, t: f32) -> P {
918 let [a, b, c, d] = self.coeff;
919 // Evaluate `a + bt + ct^2 + dt^3`, avoiding exponentiation
920 a + (b + (c + d * t) * t) * t
921 }
922
923 /// Instantaneous velocity of a point at parametric value `t`.
924 #[inline]
925 pub fn velocity(&self, t: f32) -> P {
926 let [_, b, c, d] = self.coeff;
927 // Evaluate the derivative, which is `b + 2ct + 3dt^2`, avoiding exponentiation
928 b + (c * 2.0 + d * 3.0 * t) * t
929 }
930
931 /// Instantaneous acceleration of a point at parametric value `t`.
932 #[inline]
933 pub fn acceleration(&self, t: f32) -> P {
934 let [_, _, c, d] = self.coeff;
935 // Evaluate the second derivative, which is `2c + 6dt`
936 c * 2.0 + d * 6.0 * t
937 }
938
939 /// Calculate polynomial coefficients for the cubic curve using a characteristic matrix.
940 #[inline]
941 fn coefficients(p: [P; 4], char_matrix: [[f32; 4]; 4]) -> Self {
942 let [c0, c1, c2, c3] = char_matrix;
943 // These are the polynomial coefficients, computed by multiplying the characteristic
944 // matrix by the point matrix.
945 let coeff = [
946 p[0] * c0[0] + p[1] * c0[1] + p[2] * c0[2] + p[3] * c0[3],
947 p[0] * c1[0] + p[1] * c1[1] + p[2] * c1[2] + p[3] * c1[3],
948 p[0] * c2[0] + p[1] * c2[1] + p[2] * c2[2] + p[3] * c2[3],
949 p[0] * c3[0] + p[1] * c3[1] + p[2] * c3[2] + p[3] * c3[3],
950 ];
951 Self { coeff }
952 }
953}
954
955/// The `CubicSegment<Vec2>` can be used as a 2-dimensional easing curve for animation.
956///
957/// The x-axis of the curve is time, and the y-axis is the output value. This struct provides
958/// methods for extremely fast solves for y given x.
959impl CubicSegment<Vec2> {
960 /// Construct a cubic Bezier curve for animation easing, with control points `p1` and `p2`. A
961 /// cubic Bezier easing curve has control point `p0` at (0, 0) and `p3` at (1, 1), leaving only
962 /// `p1` and `p2` as the remaining degrees of freedom. The first and last control points are
963 /// fixed to ensure the animation begins at 0, and ends at 1.
964 ///
965 /// This is a very common tool for UI animations that accelerate and decelerate smoothly. For
966 /// example, the ubiquitous "ease-in-out" is defined as `(0.25, 0.1), (0.25, 1.0)`.
967 pub fn new_bezier(p1: impl Into<Vec2>, p2: impl Into<Vec2>) -> Self {
968 let (p0, p3) = (Vec2::ZERO, Vec2::ONE);
969 let bezier = CubicBezier::new([[p0, p1.into(), p2.into(), p3]])
970 .to_curve()
971 .unwrap(); // Succeeds because resulting curve is guaranteed to have one segment
972 bezier.segments[0]
973 }
974
975 /// Maximum allowable error for iterative Bezier solve
976 const MAX_ERROR: f32 = 1e-5;
977
978 /// Maximum number of iterations during Bezier solve
979 const MAX_ITERS: u8 = 8;
980
981 /// Given a `time` within `0..=1`, returns an eased value that follows the cubic curve instead
982 /// of a straight line. This eased result may be outside the range `0..=1`, however it will
983 /// always start at 0 and end at 1: `ease(0) = 0` and `ease(1) = 1`.
984 ///
985 /// ```
986 /// # use bevy_math::prelude::*;
987 /// let cubic_bezier = CubicSegment::new_bezier((0.25, 0.1), (0.25, 1.0));
988 /// assert_eq!(cubic_bezier.ease(0.0), 0.0);
989 /// assert_eq!(cubic_bezier.ease(1.0), 1.0);
990 /// ```
991 ///
992 /// # How cubic easing works
993 ///
994 /// Easing is generally accomplished with the help of "shaping functions". These are curves that
995 /// start at (0,0) and end at (1,1). The x-axis of this plot is the current `time` of the
996 /// animation, from 0 to 1. The y-axis is how far along the animation is, also from 0 to 1. You
997 /// can imagine that if the shaping function is a straight line, there is a 1:1 mapping between
998 /// the `time` and how far along your animation is. If the `time` = 0.5, the animation is
999 /// halfway through. This is known as linear interpolation, and results in objects animating
1000 /// with a constant velocity, and no smooth acceleration or deceleration at the start or end.
1001 ///
1002 /// ```text
1003 /// y
1004 /// │ ●
1005 /// │ ⬈
1006 /// │ ⬈
1007 /// │ ⬈
1008 /// │ ⬈
1009 /// ●─────────── x (time)
1010 /// ```
1011 ///
1012 /// Using cubic Beziers, we have a curve that starts at (0,0), ends at (1,1), and follows a path
1013 /// determined by the two remaining control points (handles). These handles allow us to define a
1014 /// smooth curve. As `time` (x-axis) progresses, we now follow the curve, and use the `y` value
1015 /// to determine how far along the animation is.
1016 ///
1017 /// ```text
1018 /// y
1019 /// ⬈➔●
1020 /// │ ⬈
1021 /// │ ↑
1022 /// │ ↑
1023 /// │ ⬈
1024 /// ●➔⬈───────── x (time)
1025 /// ```
1026 ///
1027 /// To accomplish this, we need to be able to find the position `y` on a curve, given the `x`
1028 /// value. Cubic curves are implicit parametric functions like B(t) = (x,y). To find `y`, we
1029 /// first solve for `t` that corresponds to the given `x` (`time`). We use the Newton-Raphson
1030 /// root-finding method to quickly find a value of `t` that is very near the desired value of
1031 /// `x`. Once we have this we can easily plug that `t` into our curve's `position` function, to
1032 /// find the `y` component, which is how far along our animation should be. In other words:
1033 ///
1034 /// > Given `time` in `0..=1`
1035 ///
1036 /// > Use Newton's method to find a value of `t` that results in B(t) = (x,y) where `x == time`
1037 ///
1038 /// > Once a solution is found, use the resulting `y` value as the final result
1039 #[inline]
1040 pub fn ease(&self, time: f32) -> f32 {
1041 let x = time.clamp(0.0, 1.0);
1042 self.find_y_given_x(x)
1043 }
1044
1045 /// Find the `y` value of the curve at the given `x` value using the Newton-Raphson method.
1046 #[inline]
1047 fn find_y_given_x(&self, x: f32) -> f32 {
1048 let mut t_guess = x;
1049 let mut pos_guess = Vec2::ZERO;
1050 for _ in 0..Self::MAX_ITERS {
1051 pos_guess = self.position(t_guess);
1052 let error = pos_guess.x - x;
1053 if error.abs() <= Self::MAX_ERROR {
1054 break;
1055 }
1056 // Using Newton's method, use the tangent line to estimate a better guess value.
1057 let slope = self.velocity(t_guess).x; // dx/dt
1058 t_guess -= error / slope;
1059 }
1060 pos_guess.y
1061 }
1062}
1063
1064#[cfg(feature = "curve")]
1065impl<P: VectorSpace> Curve<P> for CubicSegment<P> {
1066 #[inline]
1067 fn domain(&self) -> Interval {
1068 Interval::UNIT
1069 }
1070
1071 #[inline]
1072 fn sample_unchecked(&self, t: f32) -> P {
1073 self.position(t)
1074 }
1075}
1076
1077/// A collection of [`CubicSegment`]s chained into a single parametric curve. It is a [`Curve`]
1078/// with domain `[0, N]`, where `N` is its number of segments.
1079///
1080/// Use any struct that implements the [`CubicGenerator`] trait to create a new curve, such as
1081/// [`CubicBezier`].
1082#[derive(Clone, Debug, PartialEq)]
1083#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
1084#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
1085pub struct CubicCurve<P: VectorSpace> {
1086 /// The segments comprising the curve. This must always be nonempty.
1087 segments: Vec<CubicSegment<P>>,
1088}
1089
1090impl<P: VectorSpace> CubicCurve<P> {
1091 /// Create a new curve from a collection of segments. If the collection of segments is empty,
1092 /// a curve cannot be built and `None` will be returned instead.
1093 pub fn from_segments(segments: impl Into<Vec<CubicSegment<P>>>) -> Option<Self> {
1094 let segments: Vec<_> = segments.into();
1095 if segments.is_empty() {
1096 None
1097 } else {
1098 Some(Self { segments })
1099 }
1100 }
1101
1102 /// Compute the position of a point on the cubic curve at the parametric value `t`.
1103 ///
1104 /// Note that `t` varies from `0..=(n_points - 3)`.
1105 #[inline]
1106 pub fn position(&self, t: f32) -> P {
1107 let (segment, t) = self.segment(t);
1108 segment.position(t)
1109 }
1110
1111 /// Compute the first derivative with respect to t at `t`. This is the instantaneous velocity of
1112 /// a point on the cubic curve at `t`.
1113 ///
1114 /// Note that `t` varies from `0..=(n_points - 3)`.
1115 #[inline]
1116 pub fn velocity(&self, t: f32) -> P {
1117 let (segment, t) = self.segment(t);
1118 segment.velocity(t)
1119 }
1120
1121 /// Compute the second derivative with respect to t at `t`. This is the instantaneous
1122 /// acceleration of a point on the cubic curve at `t`.
1123 ///
1124 /// Note that `t` varies from `0..=(n_points - 3)`.
1125 #[inline]
1126 pub fn acceleration(&self, t: f32) -> P {
1127 let (segment, t) = self.segment(t);
1128 segment.acceleration(t)
1129 }
1130
1131 /// A flexible iterator used to sample curves with arbitrary functions.
1132 ///
1133 /// This splits the curve into `subdivisions` of evenly spaced `t` values across the
1134 /// length of the curve from start (t = 0) to end (t = n), where `n = self.segment_count()`,
1135 /// returning an iterator evaluating the curve with the supplied `sample_function` at each `t`.
1136 ///
1137 /// For `subdivisions = 2`, this will split the curve into two lines, or three points, and
1138 /// return an iterator with 3 items, the three points, one at the start, middle, and end.
1139 #[inline]
1140 pub fn iter_samples<'a, 'b: 'a>(
1141 &'b self,
1142 subdivisions: usize,
1143 mut sample_function: impl FnMut(&Self, f32) -> P + 'a,
1144 ) -> impl Iterator<Item = P> + 'a {
1145 self.iter_uniformly(subdivisions)
1146 .map(move |t| sample_function(self, t))
1147 }
1148
1149 /// An iterator that returns values of `t` uniformly spaced over `0..=subdivisions`.
1150 #[inline]
1151 fn iter_uniformly(&self, subdivisions: usize) -> impl Iterator<Item = f32> {
1152 let segments = self.segments.len() as f32;
1153 let step = segments / subdivisions as f32;
1154 (0..=subdivisions).map(move |i| i as f32 * step)
1155 }
1156
1157 /// The list of segments contained in this `CubicCurve`.
1158 ///
1159 /// This spline's global `t` value is equal to how many segments it has.
1160 ///
1161 /// All method accepting `t` on `CubicCurve` depends on the global `t`.
1162 /// When sampling over the entire curve, you should either use one of the
1163 /// `iter_*` methods or account for the segment count using `curve.segments().len()`.
1164 #[inline]
1165 pub fn segments(&self) -> &[CubicSegment<P>] {
1166 &self.segments
1167 }
1168
1169 /// Iterate over the curve split into `subdivisions`, sampling the position at each step.
1170 pub fn iter_positions(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
1171 self.iter_samples(subdivisions, Self::position)
1172 }
1173
1174 /// Iterate over the curve split into `subdivisions`, sampling the velocity at each step.
1175 pub fn iter_velocities(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
1176 self.iter_samples(subdivisions, Self::velocity)
1177 }
1178
1179 /// Iterate over the curve split into `subdivisions`, sampling the acceleration at each step.
1180 pub fn iter_accelerations(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
1181 self.iter_samples(subdivisions, Self::acceleration)
1182 }
1183
1184 #[inline]
1185 /// Adds a segment to the curve
1186 pub fn push_segment(&mut self, segment: CubicSegment<P>) {
1187 self.segments.push(segment);
1188 }
1189
1190 /// Returns the [`CubicSegment`] and local `t` value given a spline's global `t` value.
1191 #[inline]
1192 fn segment(&self, t: f32) -> (&CubicSegment<P>, f32) {
1193 if self.segments.len() == 1 {
1194 (&self.segments[0], t)
1195 } else {
1196 let i = (t.floor() as usize).clamp(0, self.segments.len() - 1);
1197 (&self.segments[i], t - i as f32)
1198 }
1199 }
1200}
1201
1202#[cfg(feature = "curve")]
1203impl<P: VectorSpace> Curve<P> for CubicCurve<P> {
1204 #[inline]
1205 fn domain(&self) -> Interval {
1206 // The non-emptiness invariant guarantees the success of this.
1207 Interval::new(0.0, self.segments.len() as f32)
1208 .expect("CubicCurve is invalid because it has no segments")
1209 }
1210
1211 #[inline]
1212 fn sample_unchecked(&self, t: f32) -> P {
1213 self.position(t)
1214 }
1215}
1216
1217impl<P: VectorSpace> Extend<CubicSegment<P>> for CubicCurve<P> {
1218 fn extend<T: IntoIterator<Item = CubicSegment<P>>>(&mut self, iter: T) {
1219 self.segments.extend(iter);
1220 }
1221}
1222
1223impl<P: VectorSpace> IntoIterator for CubicCurve<P> {
1224 type IntoIter = <Vec<CubicSegment<P>> as IntoIterator>::IntoIter;
1225
1226 type Item = CubicSegment<P>;
1227
1228 fn into_iter(self) -> Self::IntoIter {
1229 self.segments.into_iter()
1230 }
1231}
1232
1233/// Implement this on cubic splines that can generate a rational cubic curve from their spline parameters.
1234pub trait RationalGenerator<P: VectorSpace> {
1235 /// An error type indicating why construction might fail.
1236 type Error;
1237
1238 /// Build a [`RationalCurve`] by computing the interpolation coefficients for each curve segment.
1239 fn to_curve(&self) -> Result<RationalCurve<P>, Self::Error>;
1240}
1241
1242/// A segment of a rational cubic curve, used to hold precomputed coefficients for fast interpolation.
1243/// It is a [`Curve`] with domain `[0, 1]`.
1244///
1245/// Note that the `knot_span` is used only by [compound curves] constructed by chaining these
1246/// together.
1247///
1248/// [compound curves]: RationalCurve
1249#[derive(Copy, Clone, Debug, Default, PartialEq)]
1250#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
1251#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Default))]
1252pub struct RationalSegment<P: VectorSpace> {
1253 /// The coefficients matrix of the cubic curve.
1254 pub coeff: [P; 4],
1255 /// The homogeneous weight coefficients.
1256 pub weight_coeff: [f32; 4],
1257 /// The width of the domain of this segment.
1258 pub knot_span: f32,
1259}
1260
1261impl<P: VectorSpace> RationalSegment<P> {
1262 /// Instantaneous position of a point at parametric value `t` in `[0, 1]`.
1263 #[inline]
1264 pub fn position(&self, t: f32) -> P {
1265 let [a, b, c, d] = self.coeff;
1266 let [x, y, z, w] = self.weight_coeff;
1267 // Compute a cubic polynomial for the control points
1268 let numerator = a + (b + (c + d * t) * t) * t;
1269 // Compute a cubic polynomial for the weights
1270 let denominator = x + (y + (z + w * t) * t) * t;
1271 numerator / denominator
1272 }
1273
1274 /// Instantaneous velocity of a point at parametric value `t` in `[0, 1]`.
1275 #[inline]
1276 pub fn velocity(&self, t: f32) -> P {
1277 // A derivation for the following equations can be found in "Matrix representation for NURBS
1278 // curves and surfaces" by Choi et al. See equation 19.
1279
1280 let [a, b, c, d] = self.coeff;
1281 let [x, y, z, w] = self.weight_coeff;
1282 // Compute a cubic polynomial for the control points
1283 let numerator = a + (b + (c + d * t) * t) * t;
1284 // Compute a cubic polynomial for the weights
1285 let denominator = x + (y + (z + w * t) * t) * t;
1286
1287 // Compute the derivative of the control point polynomial
1288 let numerator_derivative = b + (c * 2.0 + d * 3.0 * t) * t;
1289 // Compute the derivative of the weight polynomial
1290 let denominator_derivative = y + (z * 2.0 + w * 3.0 * t) * t;
1291
1292 // Velocity is the first derivative (wrt to the parameter `t`)
1293 // Position = N/D therefore
1294 // Velocity = (N/D)' = N'/D - N * D'/D^2 = (N' * D - N * D')/D^2
1295 numerator_derivative / denominator
1296 - numerator * (denominator_derivative / denominator.squared())
1297 }
1298
1299 /// Instantaneous acceleration of a point at parametric value `t` in `[0, 1]`.
1300 #[inline]
1301 pub fn acceleration(&self, t: f32) -> P {
1302 // A derivation for the following equations can be found in "Matrix representation for NURBS
1303 // curves and surfaces" by Choi et al. See equation 20. Note: In come copies of this paper, equation 20
1304 // is printed with the following two errors:
1305 // + The first term has incorrect sign.
1306 // + The second term uses R when it should use the first derivative.
1307
1308 let [a, b, c, d] = self.coeff;
1309 let [x, y, z, w] = self.weight_coeff;
1310 // Compute a cubic polynomial for the control points
1311 let numerator = a + (b + (c + d * t) * t) * t;
1312 // Compute a cubic polynomial for the weights
1313 let denominator = x + (y + (z + w * t) * t) * t;
1314
1315 // Compute the derivative of the control point polynomial
1316 let numerator_derivative = b + (c * 2.0 + d * 3.0 * t) * t;
1317 // Compute the derivative of the weight polynomial
1318 let denominator_derivative = y + (z * 2.0 + w * 3.0 * t) * t;
1319
1320 // Compute the second derivative of the control point polynomial
1321 let numerator_second_derivative = c * 2.0 + d * 6.0 * t;
1322 // Compute the second derivative of the weight polynomial
1323 let denominator_second_derivative = z * 2.0 + w * 6.0 * t;
1324
1325 // Velocity is the first derivative (wrt to the parameter `t`)
1326 // Position = N/D therefore
1327 // Velocity = (N/D)' = N'/D - N * D'/D^2 = (N' * D - N * D')/D^2
1328 // Acceleration = (N/D)'' = ((N' * D - N * D')/D^2)' = N''/D + N' * (-2D'/D^2) + N * (-D''/D^2 + 2D'^2/D^3)
1329 numerator_second_derivative / denominator
1330 + numerator_derivative * (-2.0 * denominator_derivative / denominator.squared())
1331 + numerator
1332 * (-denominator_second_derivative / denominator.squared()
1333 + 2.0 * denominator_derivative.squared() / denominator.cubed())
1334 }
1335
1336 /// Calculate polynomial coefficients for the cubic polynomials using a characteristic matrix.
1337 #[inline]
1338 fn coefficients(
1339 control_points: [P; 4],
1340 weights: [f32; 4],
1341 knot_span: f32,
1342 char_matrix: [[f32; 4]; 4],
1343 ) -> Self {
1344 // An explanation of this use can be found in "Matrix representation for NURBS curves and surfaces"
1345 // by Choi et al. See section "Evaluation of NURB Curves and Surfaces", and equation 16.
1346
1347 let [c0, c1, c2, c3] = char_matrix;
1348 let p = control_points;
1349 let w = weights;
1350 // These are the control point polynomial coefficients, computed by multiplying the characteristic
1351 // matrix by the point matrix.
1352 let coeff = [
1353 p[0] * c0[0] + p[1] * c0[1] + p[2] * c0[2] + p[3] * c0[3],
1354 p[0] * c1[0] + p[1] * c1[1] + p[2] * c1[2] + p[3] * c1[3],
1355 p[0] * c2[0] + p[1] * c2[1] + p[2] * c2[2] + p[3] * c2[3],
1356 p[0] * c3[0] + p[1] * c3[1] + p[2] * c3[2] + p[3] * c3[3],
1357 ];
1358 // These are the weight polynomial coefficients, computed by multiplying the characteristic
1359 // matrix by the weight matrix.
1360 let weight_coeff = [
1361 w[0] * c0[0] + w[1] * c0[1] + w[2] * c0[2] + w[3] * c0[3],
1362 w[0] * c1[0] + w[1] * c1[1] + w[2] * c1[2] + w[3] * c1[3],
1363 w[0] * c2[0] + w[1] * c2[1] + w[2] * c2[2] + w[3] * c2[3],
1364 w[0] * c3[0] + w[1] * c3[1] + w[2] * c3[2] + w[3] * c3[3],
1365 ];
1366 Self {
1367 coeff,
1368 weight_coeff,
1369 knot_span,
1370 }
1371 }
1372}
1373
1374#[cfg(feature = "curve")]
1375impl<P: VectorSpace> Curve<P> for RationalSegment<P> {
1376 #[inline]
1377 fn domain(&self) -> Interval {
1378 Interval::UNIT
1379 }
1380
1381 #[inline]
1382 fn sample_unchecked(&self, t: f32) -> P {
1383 self.position(t)
1384 }
1385}
1386
1387/// A collection of [`RationalSegment`]s chained into a single parametric curve. It is a [`Curve`]
1388/// with domain `[0, N]`, where `N` is the number of segments.
1389///
1390/// Use any struct that implements the [`RationalGenerator`] trait to create a new curve, such as
1391/// [`CubicNurbs`], or convert [`CubicCurve`] using `into/from`.
1392#[derive(Clone, Debug, PartialEq)]
1393#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
1394#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
1395pub struct RationalCurve<P: VectorSpace> {
1396 /// The segments comprising the curve. This must always be nonempty.
1397 segments: Vec<RationalSegment<P>>,
1398}
1399
1400impl<P: VectorSpace> RationalCurve<P> {
1401 /// Create a new curve from a collection of segments. If the collection of segments is empty,
1402 /// a curve cannot be built and `None` will be returned instead.
1403 pub fn from_segments(segments: impl Into<Vec<RationalSegment<P>>>) -> Option<Self> {
1404 let segments: Vec<_> = segments.into();
1405 if segments.is_empty() {
1406 None
1407 } else {
1408 Some(Self { segments })
1409 }
1410 }
1411
1412 /// Compute the position of a point on the curve at the parametric value `t`.
1413 ///
1414 /// Note that `t` varies from `0` to `self.length()`.
1415 #[inline]
1416 pub fn position(&self, t: f32) -> P {
1417 let (segment, t) = self.segment(t);
1418 segment.position(t)
1419 }
1420
1421 /// Compute the first derivative with respect to t at `t`. This is the instantaneous velocity of
1422 /// a point on the curve at `t`.
1423 ///
1424 /// Note that `t` varies from `0` to `self.length()`.
1425 #[inline]
1426 pub fn velocity(&self, t: f32) -> P {
1427 let (segment, t) = self.segment(t);
1428 segment.velocity(t)
1429 }
1430
1431 /// Compute the second derivative with respect to t at `t`. This is the instantaneous
1432 /// acceleration of a point on the curve at `t`.
1433 ///
1434 /// Note that `t` varies from `0` to `self.length()`.
1435 #[inline]
1436 pub fn acceleration(&self, t: f32) -> P {
1437 let (segment, t) = self.segment(t);
1438 segment.acceleration(t)
1439 }
1440
1441 /// A flexible iterator used to sample curves with arbitrary functions.
1442 ///
1443 /// This splits the curve into `subdivisions` of evenly spaced `t` values across the
1444 /// length of the curve from start (t = 0) to end (t = n), where `n = self.segment_count()`,
1445 /// returning an iterator evaluating the curve with the supplied `sample_function` at each `t`.
1446 ///
1447 /// For `subdivisions = 2`, this will split the curve into two lines, or three points, and
1448 /// return an iterator with 3 items, the three points, one at the start, middle, and end.
1449 #[inline]
1450 pub fn iter_samples<'a, 'b: 'a>(
1451 &'b self,
1452 subdivisions: usize,
1453 mut sample_function: impl FnMut(&Self, f32) -> P + 'a,
1454 ) -> impl Iterator<Item = P> + 'a {
1455 self.iter_uniformly(subdivisions)
1456 .map(move |t| sample_function(self, t))
1457 }
1458
1459 /// An iterator that returns values of `t` uniformly spaced over `0..=subdivisions`.
1460 #[inline]
1461 fn iter_uniformly(&self, subdivisions: usize) -> impl Iterator<Item = f32> {
1462 let length = self.length();
1463 let step = length / subdivisions as f32;
1464 (0..=subdivisions).map(move |i| i as f32 * step)
1465 }
1466
1467 /// The list of segments contained in this `RationalCurve`.
1468 ///
1469 /// This spline's global `t` value is equal to how many segments it has.
1470 ///
1471 /// All method accepting `t` on `RationalCurve` depends on the global `t`.
1472 /// When sampling over the entire curve, you should either use one of the
1473 /// `iter_*` methods or account for the segment count using `curve.segments().len()`.
1474 #[inline]
1475 pub fn segments(&self) -> &[RationalSegment<P>] {
1476 &self.segments
1477 }
1478
1479 /// Iterate over the curve split into `subdivisions`, sampling the position at each step.
1480 pub fn iter_positions(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
1481 self.iter_samples(subdivisions, Self::position)
1482 }
1483
1484 /// Iterate over the curve split into `subdivisions`, sampling the velocity at each step.
1485 pub fn iter_velocities(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
1486 self.iter_samples(subdivisions, Self::velocity)
1487 }
1488
1489 /// Iterate over the curve split into `subdivisions`, sampling the acceleration at each step.
1490 pub fn iter_accelerations(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
1491 self.iter_samples(subdivisions, Self::acceleration)
1492 }
1493
1494 /// Adds a segment to the curve.
1495 #[inline]
1496 pub fn push_segment(&mut self, segment: RationalSegment<P>) {
1497 self.segments.push(segment);
1498 }
1499
1500 /// Returns the [`RationalSegment`] and local `t` value given a spline's global `t` value.
1501 /// Input `t` will be clamped to the domain of the curve. Returned value will be in `[0, 1]`.
1502 #[inline]
1503 fn segment(&self, mut t: f32) -> (&RationalSegment<P>, f32) {
1504 if t <= 0.0 {
1505 (&self.segments[0], 0.0)
1506 } else if self.segments.len() == 1 {
1507 (&self.segments[0], t / self.segments[0].knot_span)
1508 } else {
1509 // Try to fit t into each segment domain
1510 for segment in self.segments.iter() {
1511 if t < segment.knot_span {
1512 // The division here makes t a normalized parameter in [0, 1] that can be properly
1513 // evaluated against a rational curve segment. See equations 6 & 16 from "Matrix representation
1514 // of NURBS curves and surfaces" by Choi et al. or equation 3 from "General Matrix
1515 // Representations for B-Splines" by Qin.
1516 return (segment, t / segment.knot_span);
1517 }
1518 t -= segment.knot_span;
1519 }
1520 return (self.segments.last().unwrap(), 1.0);
1521 }
1522 }
1523
1524 /// Returns the length of the domain of the parametric curve.
1525 #[inline]
1526 pub fn length(&self) -> f32 {
1527 self.segments.iter().map(|segment| segment.knot_span).sum()
1528 }
1529}
1530
1531#[cfg(feature = "curve")]
1532impl<P: VectorSpace> Curve<P> for RationalCurve<P> {
1533 #[inline]
1534 fn domain(&self) -> Interval {
1535 // The non-emptiness invariant guarantees the success of this.
1536 Interval::new(0.0, self.length())
1537 .expect("RationalCurve is invalid because it has zero length")
1538 }
1539
1540 #[inline]
1541 fn sample_unchecked(&self, t: f32) -> P {
1542 self.position(t)
1543 }
1544}
1545
1546impl<P: VectorSpace> Extend<RationalSegment<P>> for RationalCurve<P> {
1547 fn extend<T: IntoIterator<Item = RationalSegment<P>>>(&mut self, iter: T) {
1548 self.segments.extend(iter);
1549 }
1550}
1551
1552impl<P: VectorSpace> IntoIterator for RationalCurve<P> {
1553 type IntoIter = <Vec<RationalSegment<P>> as IntoIterator>::IntoIter;
1554
1555 type Item = RationalSegment<P>;
1556
1557 fn into_iter(self) -> Self::IntoIter {
1558 self.segments.into_iter()
1559 }
1560}
1561
1562impl<P: VectorSpace> From<CubicSegment<P>> for RationalSegment<P> {
1563 fn from(value: CubicSegment<P>) -> Self {
1564 Self {
1565 coeff: value.coeff,
1566 weight_coeff: [1.0, 0.0, 0.0, 0.0],
1567 knot_span: 1.0, // Cubic curves are uniform, so every segment has domain [0, 1).
1568 }
1569 }
1570}
1571
1572impl<P: VectorSpace> From<CubicCurve<P>> for RationalCurve<P> {
1573 fn from(value: CubicCurve<P>) -> Self {
1574 Self {
1575 segments: value.segments.into_iter().map(Into::into).collect(),
1576 }
1577 }
1578}
1579
1580#[cfg(test)]
1581mod tests {
1582 use glam::{vec2, Vec2};
1583
1584 use crate::{
1585 cubic_splines::{
1586 CubicBSpline, CubicBezier, CubicGenerator, CubicNurbs, CubicSegment, RationalCurve,
1587 RationalGenerator,
1588 },
1589 ops::{self, FloatPow},
1590 };
1591
1592 /// How close two floats can be and still be considered equal
1593 const FLOAT_EQ: f32 = 1e-5;
1594
1595 /// Sweep along the full length of a 3D cubic Bezier, and manually check the position.
1596 #[test]
1597 fn cubic() {
1598 const N_SAMPLES: usize = 1000;
1599 let points = [[
1600 vec2(-1.0, -20.0),
1601 vec2(3.0, 2.0),
1602 vec2(5.0, 3.0),
1603 vec2(9.0, 8.0),
1604 ]];
1605 let bezier = CubicBezier::new(points).to_curve().unwrap();
1606 for i in 0..=N_SAMPLES {
1607 let t = i as f32 / N_SAMPLES as f32; // Check along entire length
1608 assert!(bezier.position(t).distance(cubic_manual(t, points[0])) <= FLOAT_EQ);
1609 }
1610 }
1611
1612 /// Manual, hardcoded function for computing the position along a cubic bezier.
1613 fn cubic_manual(t: f32, points: [Vec2; 4]) -> Vec2 {
1614 let p = points;
1615 p[0] * (1.0 - t).cubed()
1616 + 3.0 * p[1] * t * (1.0 - t).squared()
1617 + 3.0 * p[2] * t.squared() * (1.0 - t)
1618 + p[3] * t.cubed()
1619 }
1620
1621 /// Basic cubic Bezier easing test to verify the shape of the curve.
1622 #[test]
1623 fn easing_simple() {
1624 // A curve similar to ease-in-out, but symmetric
1625 let bezier = CubicSegment::new_bezier([1.0, 0.0], [0.0, 1.0]);
1626 assert_eq!(bezier.ease(0.0), 0.0);
1627 assert!(bezier.ease(0.2) < 0.2); // tests curve
1628 assert_eq!(bezier.ease(0.5), 0.5); // true due to symmetry
1629 assert!(bezier.ease(0.8) > 0.8); // tests curve
1630 assert_eq!(bezier.ease(1.0), 1.0);
1631 }
1632
1633 /// A curve that forms an upside-down "U", that should extend below 0.0. Useful for animations
1634 /// that go beyond the start and end positions, e.g. bouncing.
1635 #[test]
1636 fn easing_overshoot() {
1637 // A curve that forms an upside-down "U", that should extend above 1.0
1638 let bezier = CubicSegment::new_bezier([0.0, 2.0], [1.0, 2.0]);
1639 assert_eq!(bezier.ease(0.0), 0.0);
1640 assert!(bezier.ease(0.5) > 1.5);
1641 assert_eq!(bezier.ease(1.0), 1.0);
1642 }
1643
1644 /// A curve that forms a "U", that should extend below 0.0. Useful for animations that go beyond
1645 /// the start and end positions, e.g. bouncing.
1646 #[test]
1647 fn easing_undershoot() {
1648 let bezier = CubicSegment::new_bezier([0.0, -2.0], [1.0, -2.0]);
1649 assert_eq!(bezier.ease(0.0), 0.0);
1650 assert!(bezier.ease(0.5) < -0.5);
1651 assert_eq!(bezier.ease(1.0), 1.0);
1652 }
1653
1654 /// Test that a simple cardinal spline passes through all of its control points with
1655 /// the correct tangents.
1656 #[test]
1657 fn cardinal_control_pts() {
1658 use super::CubicCardinalSpline;
1659
1660 let tension = 0.2;
1661 let [p0, p1, p2, p3] = [vec2(-1., -2.), vec2(0., 1.), vec2(1., 2.), vec2(-2., 1.)];
1662 let curve = CubicCardinalSpline::new(tension, [p0, p1, p2, p3])
1663 .to_curve()
1664 .unwrap();
1665
1666 // Positions at segment endpoints
1667 assert!(curve.position(0.).abs_diff_eq(p0, FLOAT_EQ));
1668 assert!(curve.position(1.).abs_diff_eq(p1, FLOAT_EQ));
1669 assert!(curve.position(2.).abs_diff_eq(p2, FLOAT_EQ));
1670 assert!(curve.position(3.).abs_diff_eq(p3, FLOAT_EQ));
1671
1672 // Tangents at segment endpoints
1673 assert!(curve
1674 .velocity(0.)
1675 .abs_diff_eq((p1 - p0) * tension * 2., FLOAT_EQ));
1676 assert!(curve
1677 .velocity(1.)
1678 .abs_diff_eq((p2 - p0) * tension, FLOAT_EQ));
1679 assert!(curve
1680 .velocity(2.)
1681 .abs_diff_eq((p3 - p1) * tension, FLOAT_EQ));
1682 assert!(curve
1683 .velocity(3.)
1684 .abs_diff_eq((p3 - p2) * tension * 2., FLOAT_EQ));
1685 }
1686
1687 /// Test that [`RationalCurve`] properly generalizes [`CubicCurve`]. A Cubic upgraded to a rational
1688 /// should produce pretty much the same output.
1689 #[test]
1690 fn cubic_to_rational() {
1691 const EPSILON: f32 = 0.00001;
1692
1693 let points = [
1694 vec2(0.0, 0.0),
1695 vec2(1.0, 1.0),
1696 vec2(1.0, 1.0),
1697 vec2(2.0, -1.0),
1698 vec2(3.0, 1.0),
1699 vec2(0.0, 0.0),
1700 ];
1701
1702 let b_spline = CubicBSpline::new(points).to_curve().unwrap();
1703 let rational_b_spline = RationalCurve::from(b_spline.clone());
1704
1705 /// Tests if two vectors of points are approximately the same
1706 fn compare_vectors(cubic_curve: Vec<Vec2>, rational_curve: Vec<Vec2>, name: &str) {
1707 assert_eq!(
1708 cubic_curve.len(),
1709 rational_curve.len(),
1710 "{name} vector lengths mismatch"
1711 );
1712 for (i, (a, b)) in cubic_curve.iter().zip(rational_curve.iter()).enumerate() {
1713 assert!(
1714 a.distance(*b) < EPSILON,
1715 "Mismatch at {name} value {i}. CubicCurve: {} Converted RationalCurve: {}",
1716 a,
1717 b
1718 );
1719 }
1720 }
1721
1722 // Both curves should yield the same values
1723 let cubic_positions: Vec<_> = b_spline.iter_positions(10).collect();
1724 let rational_positions: Vec<_> = rational_b_spline.iter_positions(10).collect();
1725 compare_vectors(cubic_positions, rational_positions, "position");
1726
1727 let cubic_velocities: Vec<_> = b_spline.iter_velocities(10).collect();
1728 let rational_velocities: Vec<_> = rational_b_spline.iter_velocities(10).collect();
1729 compare_vectors(cubic_velocities, rational_velocities, "velocity");
1730
1731 let cubic_accelerations: Vec<_> = b_spline.iter_accelerations(10).collect();
1732 let rational_accelerations: Vec<_> = rational_b_spline.iter_accelerations(10).collect();
1733 compare_vectors(cubic_accelerations, rational_accelerations, "acceleration");
1734 }
1735
1736 /// Test that a nurbs curve can approximate a portion of a circle.
1737 #[test]
1738 fn nurbs_circular_arc() {
1739 use core::f32::consts::FRAC_PI_2;
1740 const EPSILON: f32 = 0.0000001;
1741
1742 // The following NURBS parameters were determined by constraining the first two
1743 // points to the line y=1, the second two points to the line x=1, and the distance
1744 // between each pair of points to be equal. One can solve the weights by assuming the
1745 // first and last weights to be one, the intermediate weights to be equal, and
1746 // subjecting ones self to a lot of tedious matrix algebra.
1747
1748 let alpha = FRAC_PI_2;
1749 let leg = 2.0 * ops::sin(alpha / 2.0) / (1.0 + 2.0 * ops::cos(alpha / 2.0));
1750 let weight = (1.0 + 2.0 * ops::cos(alpha / 2.0)) / 3.0;
1751 let points = [
1752 vec2(1.0, 0.0),
1753 vec2(1.0, leg),
1754 vec2(leg, 1.0),
1755 vec2(0.0, 1.0),
1756 ];
1757 let weights = [1.0, weight, weight, 1.0];
1758 let knots = [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0];
1759 let spline = CubicNurbs::new(points, Some(weights), Some(knots)).unwrap();
1760 let curve = spline.to_curve().unwrap();
1761 for (i, point) in curve.iter_positions(10).enumerate() {
1762 assert!(
1763 f32::abs(point.length() - 1.0) < EPSILON,
1764 "Point {i} is not on the unit circle: {point:?} has length {}",
1765 point.length()
1766 );
1767 }
1768 }
1769}