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