Path: blob/main/crates/bevy_math/src/cubic_splines/mod.rs
6596 views
//! Provides types for building cubic splines for rendering curves and use with animation easing.12#[cfg(feature = "curve")]3mod curve_impls;4use crate::{5ops::{self, FloatPow},6Vec2, VectorSpace,7};8#[cfg(feature = "bevy_reflect")]9use bevy_reflect::{std_traits::ReflectDefault, Reflect};10use thiserror::Error;11#[cfg(feature = "alloc")]12use {alloc::vec, alloc::vec::Vec, core::iter::once, itertools::Itertools};1314/// A spline composed of a single cubic Bezier curve.15///16/// Useful for user-drawn curves with local control, or animation easing. See17/// [`CubicSegment::new_bezier_easing`] for use in easing.18///19/// ### Interpolation20///21/// The curve only passes through the first and last control point in each set of four points. The curve22/// is divided into "segments" by every fourth control point.23///24/// ### Tangency25///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 intermediate28/// control points as the anchors displaced along their tangent vectors29///30/// ### Continuity31///32/// A Bezier curve is at minimum C0 continuous, meaning it has no holes or jumps. Each curve segment is33/// C2, meaning the tangent vector changes smoothly between each set of four control points, but this34/// doesn't hold at the control points between segments. Making the whole curve C1 or C2 requires moving35/// the intermediate control points to align the tangent vectors between segments, and can result in a36/// loss of local control.37///38/// ### Usage39///40/// ```41/// # use bevy_math::{*, prelude::*};42/// let points = [[43/// vec2(-1.0, -20.0),44/// vec2(3.0, 2.0),45/// vec2(5.0, 3.0),46/// vec2(9.0, 8.0),47/// ]];48/// let bezier = CubicBezier::new(points).to_curve().unwrap();49/// let positions: Vec<_> = bezier.iter_positions(100).collect();50/// ```51#[derive(Clone, Debug)]52#[cfg(feature = "alloc")]53#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]54pub struct CubicBezier<P: VectorSpace> {55/// The control points of the Bezier curve.56pub control_points: Vec<[P; 4]>,57}5859#[cfg(feature = "alloc")]60impl<P: VectorSpace> CubicBezier<P> {61/// Create a new cubic Bezier curve from sets of control points.62pub fn new(control_points: impl IntoIterator<Item = [P; 4]>) -> Self {63Self {64control_points: control_points.into_iter().collect(),65}66}67}6869#[cfg(feature = "alloc")]70impl<P: VectorSpace<Scalar = f32>> CubicGenerator<P> for CubicBezier<P> {71type Error = CubicBezierError;7273#[inline]74fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error> {75let segments = self76.control_points77.iter()78.map(|p| CubicSegment::new_bezier(*p))79.collect_vec();8081if segments.is_empty() {82Err(CubicBezierError)83} else {84Ok(CubicCurve { segments })85}86}87}88/// An error returned during cubic curve generation for cubic Bezier curves indicating that a89/// segment of control points was not present.90#[derive(Clone, Debug, Error)]91#[error("Unable to generate cubic curve: at least one set of control points is required")]92pub struct CubicBezierError;9394/// A spline interpolated continuously between the nearest two control points, with the position and95/// velocity of the curve specified at both control points. This curve passes through all control96/// points, with the specified velocity which includes direction and parametric speed.97///98/// Useful for smooth interpolation when you know the position and velocity at two points in time,99/// such as network prediction.100///101/// ### Interpolation102///103/// The curve passes through every control point.104///105/// ### Tangency106///107/// Tangents are explicitly defined at each control point.108///109/// ### Continuity110///111/// The curve is at minimum C1 continuous, meaning that it has no holes or jumps and the tangent vector also112/// has no sudden jumps.113///114/// ### Parametrization115///116/// The first segment of the curve connects the first two control points, the second connects the second and117/// third, and so on. This remains true when a cyclic curve is formed with [`to_curve_cyclic`], in which case118/// the final curve segment connects the last control point to the first.119///120/// ### Usage121///122/// ```123/// # use bevy_math::{*, prelude::*};124/// let points = [125/// vec2(-1.0, -20.0),126/// vec2(3.0, 2.0),127/// vec2(5.0, 3.0),128/// vec2(9.0, 8.0),129/// ];130/// let tangents = [131/// vec2(0.0, 1.0),132/// vec2(0.0, 1.0),133/// vec2(0.0, 1.0),134/// vec2(0.0, 1.0),135/// ];136/// let hermite = CubicHermite::new(points, tangents).to_curve().unwrap();137/// let positions: Vec<_> = hermite.iter_positions(100).collect();138/// ```139///140/// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic141#[derive(Clone, Debug)]142#[cfg(feature = "alloc")]143#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]144pub struct CubicHermite<P: VectorSpace> {145/// The control points of the Hermite curve.146pub control_points: Vec<(P, P)>,147}148149#[cfg(feature = "alloc")]150impl<P: VectorSpace> CubicHermite<P> {151/// Create a new Hermite curve from sets of control points.152pub fn new(153control_points: impl IntoIterator<Item = P>,154tangents: impl IntoIterator<Item = P>,155) -> Self {156Self {157control_points: control_points.into_iter().zip(tangents).collect(),158}159}160161/// The characteristic matrix for this spline construction.162///163/// Each row of this matrix expresses the coefficients of a [`CubicSegment`] as a linear164/// combination of `p_i`, `v_i`, `p_{i+1}`, and `v_{i+1}`, where `(p_i, v_i)` and165/// `(p_{i+1}, v_{i+1})` are consecutive control points with tangents.166#[inline]167fn char_matrix(&self) -> [[f32; 4]; 4] {168[169[1., 0., 0., 0.],170[0., 1., 0., 0.],171[-3., -2., 3., -1.],172[2., 1., -2., 1.],173]174}175}176177#[cfg(feature = "alloc")]178impl<P: VectorSpace<Scalar = f32>> CubicGenerator<P> for CubicHermite<P> {179type Error = InsufficientDataError;180181#[inline]182fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error> {183let segments = self184.control_points185.windows(2)186.map(|p| {187let (p0, v0, p1, v1) = (p[0].0, p[0].1, p[1].0, p[1].1);188CubicSegment::coefficients([p0, v0, p1, v1], self.char_matrix())189})190.collect_vec();191192if segments.is_empty() {193Err(InsufficientDataError {194expected: 2,195given: self.control_points.len(),196})197} else {198Ok(CubicCurve { segments })199}200}201}202203#[cfg(feature = "alloc")]204impl<P: VectorSpace<Scalar = f32>> CyclicCubicGenerator<P> for CubicHermite<P> {205type Error = InsufficientDataError;206207#[inline]208fn to_curve_cyclic(&self) -> Result<CubicCurve<P>, Self::Error> {209let segments = self210.control_points211.iter()212.circular_tuple_windows()213.map(|(&j0, &j1)| {214let (p0, v0, p1, v1) = (j0.0, j0.1, j1.0, j1.1);215CubicSegment::coefficients([p0, v0, p1, v1], self.char_matrix())216})217.collect_vec();218219if segments.is_empty() {220Err(InsufficientDataError {221expected: 2,222given: self.control_points.len(),223})224} else {225Ok(CubicCurve { segments })226}227}228}229230/// A spline interpolated continuously across the nearest four control points, with the position of231/// the curve specified at every control point and the tangents computed automatically. The associated [`CubicCurve`]232/// has one segment between each pair of adjacent control points.233///234/// **Note** the Catmull-Rom spline is a special case of Cardinal spline where the tension is 0.5.235///236/// ### Interpolation237///238/// The curve passes through every control point.239///240/// ### Tangency241///242/// Tangents are automatically computed based on the positions of control points.243///244/// ### Continuity245///246/// The curve is at minimum C1, meaning that it is continuous (it has no holes or jumps), and its tangent247/// vector is also well-defined everywhere, without sudden jumps.248///249/// ### Parametrization250///251/// The first segment of the curve connects the first two control points, the second connects the second and252/// third, and so on. This remains true when a cyclic curve is formed with [`to_curve_cyclic`], in which case253/// the final curve segment connects the last control point to the first.254///255/// ### Usage256///257/// ```258/// # use bevy_math::{*, prelude::*};259/// let points = [260/// vec2(-1.0, -20.0),261/// vec2(3.0, 2.0),262/// vec2(5.0, 3.0),263/// vec2(9.0, 8.0),264/// ];265/// let cardinal = CubicCardinalSpline::new(0.3, points).to_curve().unwrap();266/// let positions: Vec<_> = cardinal.iter_positions(100).collect();267/// ```268///269/// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic270#[derive(Clone, Debug)]271#[cfg(feature = "alloc")]272#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]273pub struct CubicCardinalSpline<P: VectorSpace> {274/// Tension275pub tension: f32,276/// The control points of the Cardinal spline277pub control_points: Vec<P>,278}279280#[cfg(feature = "alloc")]281impl<P: VectorSpace> CubicCardinalSpline<P> {282/// Build a new Cardinal spline.283pub fn new(tension: f32, control_points: impl IntoIterator<Item = P>) -> Self {284Self {285tension,286control_points: control_points.into_iter().collect(),287}288}289290/// Build a new Catmull-Rom spline, the special case of a Cardinal spline where tension = 1/2.291pub fn new_catmull_rom(control_points: impl IntoIterator<Item = P>) -> Self {292Self {293tension: 0.5,294control_points: control_points.into_iter().collect(),295}296}297298/// The characteristic matrix for this spline construction.299///300/// Each row of this matrix expresses the coefficients of a [`CubicSegment`] as a linear301/// combination of four consecutive control points.302#[inline]303fn char_matrix(&self) -> [[f32; 4]; 4] {304let s = self.tension;305[306[0., 1., 0., 0.],307[-s, 0., s, 0.],308[2. * s, s - 3., 3. - 2. * s, -s],309[-s, 2. - s, s - 2., s],310]311}312}313314#[cfg(feature = "alloc")]315impl<P: VectorSpace<Scalar = f32>> CubicGenerator<P> for CubicCardinalSpline<P> {316type Error = InsufficientDataError;317318#[inline]319fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error> {320let length = self.control_points.len();321322// Early return to avoid accessing an invalid index323if length < 2 {324return Err(InsufficientDataError {325expected: 2,326given: self.control_points.len(),327});328}329330// Extend the list of control points by mirroring the last second-to-last control points on each end;331// this allows tangents for the endpoints to be provided, and the overall effect is that the tangent332// at an endpoint is proportional to twice the vector between it and its adjacent control point.333//334// The expression used here is P_{-1} := P_0 - (P_1 - P_0) = 2P_0 - P_1. (Analogously at the other end.)335let mirrored_first = self.control_points[0] * 2. - self.control_points[1];336let mirrored_last = self.control_points[length - 1] * 2. - self.control_points[length - 2];337let extended_control_points = once(&mirrored_first)338.chain(self.control_points.iter())339.chain(once(&mirrored_last));340341let segments = extended_control_points342.tuple_windows()343.map(|(&p0, &p1, &p2, &p3)| {344CubicSegment::coefficients([p0, p1, p2, p3], self.char_matrix())345})346.collect_vec();347348Ok(CubicCurve { segments })349}350}351352#[cfg(feature = "alloc")]353impl<P: VectorSpace<Scalar = f32>> CyclicCubicGenerator<P> for CubicCardinalSpline<P> {354type Error = InsufficientDataError;355356#[inline]357fn to_curve_cyclic(&self) -> Result<CubicCurve<P>, Self::Error> {358let len = self.control_points.len();359360if len < 2 {361return Err(InsufficientDataError {362expected: 2,363given: self.control_points.len(),364});365}366367// This would ordinarily be the last segment, but we pick it out so that we can make it first368// in order to get a desirable parametrization where the first segment connects the first two369// control points instead of the second and third.370let first_segment = {371// We take the indices mod `len` in case `len` is very small.372let p0 = self.control_points[len - 1];373let p1 = self.control_points[0];374let p2 = self.control_points[1 % len];375let p3 = self.control_points[2 % len];376CubicSegment::coefficients([p0, p1, p2, p3], self.char_matrix())377};378379let later_segments = self380.control_points381.iter()382.circular_tuple_windows()383.map(|(&p0, &p1, &p2, &p3)| {384CubicSegment::coefficients([p0, p1, p2, p3], self.char_matrix())385})386.take(len - 1);387388let mut segments = Vec::with_capacity(len);389segments.push(first_segment);390segments.extend(later_segments);391392Ok(CubicCurve { segments })393}394}395396/// A spline interpolated continuously across the nearest four control points. The curve does not397/// necessarily pass through any of the control points.398///399/// ### Interpolation400///401/// The curve does not necessarily pass through its control points.402///403/// ### Tangency404/// Tangents are automatically computed based on the positions of control points.405///406/// ### Continuity407///408/// The curve is C2 continuous, meaning it has no holes or jumps, the tangent vector changes smoothly along409/// the entire curve, and the acceleration also varies continuously. The acceleration continuity of this410/// spline makes it useful for camera paths.411///412/// ### Parametrization413///414/// Each curve segment is defined by a window of four control points taken in sequence. When [`to_curve_cyclic`]415/// is used to form a cyclic curve, the three additional segments used to close the curve come last.416///417/// ### Usage418///419/// ```420/// # use bevy_math::{*, prelude::*};421/// let points = [422/// vec2(-1.0, -20.0),423/// vec2(3.0, 2.0),424/// vec2(5.0, 3.0),425/// vec2(9.0, 8.0),426/// ];427/// let b_spline = CubicBSpline::new(points).to_curve().unwrap();428/// let positions: Vec<_> = b_spline.iter_positions(100).collect();429/// ```430///431/// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic432#[derive(Clone, Debug)]433#[cfg(feature = "alloc")]434#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]435pub struct CubicBSpline<P: VectorSpace> {436/// The control points of the spline437pub control_points: Vec<P>,438}439#[cfg(feature = "alloc")]440impl<P: VectorSpace> CubicBSpline<P> {441/// Build a new B-Spline.442pub fn new(control_points: impl IntoIterator<Item = P>) -> Self {443Self {444control_points: control_points.into_iter().collect(),445}446}447448/// The characteristic matrix for this spline construction.449///450/// Each row of this matrix expresses the coefficients of a [`CubicSegment`] as a linear451/// combination of four consecutive control points.452#[inline]453fn char_matrix(&self) -> [[f32; 4]; 4] {454// A derivation for this matrix can be found in "General Matrix Representations for B-splines" by Kaihuai Qin.455// <https://xiaoxingchen.github.io/2020/03/02/bspline_in_so3/general_matrix_representation_for_bsplines.pdf>456// See section 4.1 and equations 7 and 8.457let mut char_matrix = [458[1.0, 4.0, 1.0, 0.0],459[-3.0, 0.0, 3.0, 0.0],460[3.0, -6.0, 3.0, 0.0],461[-1.0, 3.0, -3.0, 1.0],462];463464char_matrix465.iter_mut()466.for_each(|r| r.iter_mut().for_each(|c| *c /= 6.0));467468char_matrix469}470}471472#[cfg(feature = "alloc")]473impl<P: VectorSpace<Scalar = f32>> CubicGenerator<P> for CubicBSpline<P> {474type Error = InsufficientDataError;475476#[inline]477fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error> {478let segments = self479.control_points480.windows(4)481.map(|p| CubicSegment::coefficients([p[0], p[1], p[2], p[3]], self.char_matrix()))482.collect_vec();483484if segments.is_empty() {485Err(InsufficientDataError {486expected: 4,487given: self.control_points.len(),488})489} else {490Ok(CubicCurve { segments })491}492}493}494495#[cfg(feature = "alloc")]496impl<P: VectorSpace<Scalar = f32>> CyclicCubicGenerator<P> for CubicBSpline<P> {497type Error = InsufficientDataError;498499#[inline]500fn to_curve_cyclic(&self) -> Result<CubicCurve<P>, Self::Error> {501let segments = self502.control_points503.iter()504.circular_tuple_windows()505.map(|(&a, &b, &c, &d)| CubicSegment::coefficients([a, b, c, d], self.char_matrix()))506.collect_vec();507508// Note that the parametrization is consistent with the one for `to_curve` but with509// the extra curve segments all tacked on at the end. This might be slightly counter-intuitive,510// since it means the first segment doesn't go "between" the first two control points, but511// between the second and third instead.512513if segments.is_empty() {514Err(InsufficientDataError {515expected: 2,516given: self.control_points.len(),517})518} else {519Ok(CubicCurve { segments })520}521}522}523524/// Error during construction of [`CubicNurbs`]525#[derive(Clone, Debug, Error)]526pub enum CubicNurbsError {527/// Provided the wrong number of knots.528#[error("Wrong number of knots: expected {expected}, provided {provided}")]529KnotsNumberMismatch {530/// Expected number of knots531expected: usize,532/// Provided number of knots533provided: usize,534},535/// The provided knots had a descending knot pair. Subsequent knots must536/// either increase or stay the same.537#[error("Invalid knots: contains descending knot pair")]538DescendingKnots,539/// The provided knots were all equal. Knots must contain at least one increasing pair.540#[error("Invalid knots: all knots are equal")]541ConstantKnots,542/// Provided a different number of weights and control points.543#[error("Incorrect number of weights: expected {expected}, provided {provided}")]544WeightsNumberMismatch {545/// Expected number of weights546expected: usize,547/// Provided number of weights548provided: usize,549},550/// The number of control points provided is less than 4.551#[error("Not enough control points, at least 4 are required, {provided} were provided")]552NotEnoughControlPoints {553/// The number of control points provided554provided: usize,555},556}557558/// Non-uniform Rational B-Splines (NURBS) are a powerful generalization of the [`CubicBSpline`] which can559/// represent a much more diverse class of curves (like perfect circles and ellipses).560///561/// ### Non-uniformity562///563/// The 'NU' part of NURBS stands for "Non-Uniform". This has to do with a parameter called 'knots'.564/// The knots are a non-decreasing sequence of floating point numbers. The first and last three pairs of565/// knots control the behavior of the curve as it approaches its endpoints. The intermediate pairs566/// each control the length of one segment of the curve. Multiple repeated knot values are called567/// "knot multiplicity". Knot multiplicity in the intermediate knots causes a "zero-length" segment,568/// and can create sharp corners.569///570/// ### Rationality571///572/// The 'R' part of NURBS stands for "Rational". This has to do with NURBS allowing each control point to573/// be assigned a weighting, which controls how much it affects the curve compared to the other points.574///575/// ### Interpolation576///577/// The curve will not pass through the control points except where a knot has multiplicity four.578///579/// ### Tangency580///581/// Tangents are automatically computed based on the position of control points.582///583/// ### Continuity584///585/// When there is no knot multiplicity, the curve is C2 continuous, meaning it has no holes or jumps and the586/// tangent vector changes smoothly along the entire curve length. Like the [`CubicBSpline`], the acceleration587/// continuity makes it useful for camera paths. Knot multiplicity of 2 in intermediate knots reduces the588/// continuity to C1, and knot multiplicity of 3 reduces the continuity to C0. The curve is always at least589/// C0, meaning it has no jumps or holes.590///591/// ### Usage592///593/// ```594/// # use bevy_math::{*, prelude::*};595/// let points = [596/// vec2(-1.0, -20.0),597/// vec2(3.0, 2.0),598/// vec2(5.0, 3.0),599/// vec2(9.0, 8.0),600/// ];601/// let weights = [1.0, 1.0, 2.0, 1.0];602/// let knots = [0.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 5.0];603/// let nurbs = CubicNurbs::new(points, Some(weights), Some(knots))604/// .expect("NURBS construction failed!")605/// .to_curve()606/// .unwrap();607/// let positions: Vec<_> = nurbs.iter_positions(100).collect();608/// ```609#[derive(Clone, Debug)]610#[cfg(feature = "alloc")]611#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]612pub struct CubicNurbs<P: VectorSpace> {613/// The control points of the NURBS614pub control_points: Vec<P>,615/// Weights616pub weights: Vec<f32>,617/// Knots618pub knots: Vec<f32>,619}620621#[cfg(feature = "alloc")]622impl<P: VectorSpace<Scalar = f32>> CubicNurbs<P> {623/// Build a Non-Uniform Rational B-Spline.624///625/// If provided, weights must be the same length as the control points. Defaults to equal weights.626///627/// If provided, the number of knots must be n + 4 elements, where n is the amount of control628/// points. Defaults to open uniform knots: [`Self::open_uniform_knots`]. Knots cannot629/// all be equal.630///631/// At least 4 points must be provided, otherwise an error will be returned.632pub fn new(633control_points: impl IntoIterator<Item = P>,634weights: Option<impl IntoIterator<Item = f32>>,635knots: Option<impl IntoIterator<Item = f32>>,636) -> Result<Self, CubicNurbsError> {637let mut control_points: Vec<P> = control_points.into_iter().collect();638let control_points_len = control_points.len();639640if control_points_len < 4 {641return Err(CubicNurbsError::NotEnoughControlPoints {642provided: control_points_len,643});644}645646let weights: Vec<f32> = weights647.map(|ws| ws.into_iter().collect())648.unwrap_or_else(|| vec![1.0; control_points_len]);649650let mut knots: Vec<f32> = knots.map(|ks| ks.into_iter().collect()).unwrap_or_else(|| {651Self::open_uniform_knots(control_points_len)652.expect("The amount of control points was checked")653});654655let expected_knots_len = Self::knots_len(control_points_len);656657// Check the number of knots is correct658if knots.len() != expected_knots_len {659return Err(CubicNurbsError::KnotsNumberMismatch {660expected: expected_knots_len,661provided: knots.len(),662});663}664665// Ensure the knots are non-descending (previous element is less than or equal666// to the next)667if knots.windows(2).any(|win| win[0] > win[1]) {668return Err(CubicNurbsError::DescendingKnots);669}670671// Ensure the knots are non-constant672if knots.windows(2).all(|win| win[0] == win[1]) {673return Err(CubicNurbsError::ConstantKnots);674}675676// Check that the number of weights equals the number of control points677if weights.len() != control_points_len {678return Err(CubicNurbsError::WeightsNumberMismatch {679expected: control_points_len,680provided: weights.len(),681});682}683684// To align the evaluation behavior of nurbs with the other splines,685// make the intervals between knots form an exact cover of [0, N], where N is686// the number of segments of the final curve.687let curve_length = (control_points.len() - 3) as f32;688let min = *knots.first().unwrap();689let max = *knots.last().unwrap();690let knot_delta = max - min;691knots = knots692.into_iter()693.map(|k| k - min)694.map(|k| k * curve_length / knot_delta)695.collect();696697control_points698.iter_mut()699.zip(weights.iter())700.for_each(|(p, w)| *p = *p * *w);701702Ok(Self {703control_points,704weights,705knots,706})707}708709/// Generates uniform knots that will generate the same curve as [`CubicBSpline`].710///711/// "Uniform" means that the difference between two subsequent knots is the same.712///713/// Will return `None` if there are less than 4 control points.714pub fn uniform_knots(control_points: usize) -> Option<Vec<f32>> {715if control_points < 4 {716return None;717}718Some(719(0..Self::knots_len(control_points))720.map(|v| v as f32)721.collect(),722)723}724725/// Generates open uniform knots, which makes the ends of the curve pass through the726/// start and end points.727///728/// The start and end knots have multiplicity 4, and intermediate knots have multiplicity 0 and729/// difference of 1.730///731/// Will return `None` if there are less than 4 control points.732pub fn open_uniform_knots(control_points: usize) -> Option<Vec<f32>> {733if control_points < 4 {734return None;735}736let last_knots_value = control_points - 3;737Some(738core::iter::repeat_n(0.0, 4)739.chain((1..last_knots_value).map(|v| v as f32))740.chain(core::iter::repeat_n(last_knots_value as f32, 4))741.collect(),742)743}744745#[inline(always)]746const fn knots_len(control_points_len: usize) -> usize {747control_points_len + 4748}749750/// Generates a non-uniform B-spline characteristic matrix from a sequence of six knots. Each six751/// knots describe the relationship between four successive control points. For padding reasons,752/// this takes a vector of 8 knots, but only six are actually used.753fn generate_matrix(knots: &[f32; 8]) -> [[f32; 4]; 4] {754// A derivation for this matrix can be found in "General Matrix Representations for B-splines" by Kaihuai Qin.755// <https://xiaoxingchen.github.io/2020/03/02/bspline_in_so3/general_matrix_representation_for_bsplines.pdf>756// See section 3.1.757758let t = knots;759// In the notation of the paper:760// t[1] := t_i-2761// t[2] := t_i-1762// t[3] := t_i (the lower extent of the current knot span)763// t[4] := t_i+1 (the upper extent of the current knot span)764// t[5] := t_i+2765// t[6] := t_i+3766767let m00 = (t[4] - t[3]).squared() / ((t[4] - t[2]) * (t[4] - t[1]));768let m02 = (t[3] - t[2]).squared() / ((t[5] - t[2]) * (t[4] - t[2]));769let m12 = (3.0 * (t[4] - t[3]) * (t[3] - t[2])) / ((t[5] - t[2]) * (t[4] - t[2]));770let m22 = 3.0 * (t[4] - t[3]).squared() / ((t[5] - t[2]) * (t[4] - t[2]));771let m33 = (t[4] - t[3]).squared() / ((t[6] - t[3]) * (t[5] - t[3]));772let m32 = -m22 / 3.0 - m33 - (t[4] - t[3]).squared() / ((t[5] - t[3]) * (t[5] - t[2]));773[774[m00, 1.0 - m00 - m02, m02, 0.0],775[-3.0 * m00, 3.0 * m00 - m12, m12, 0.0],776[3.0 * m00, -3.0 * m00 - m22, m22, 0.0],777[-m00, m00 - m32 - m33, m32, m33],778]779}780}781782#[cfg(feature = "alloc")]783impl<P: VectorSpace<Scalar = f32>> RationalGenerator<P> for CubicNurbs<P> {784type Error = InsufficientDataError;785786#[inline]787fn to_curve(&self) -> Result<RationalCurve<P>, Self::Error> {788let segments = self789.control_points790.windows(4)791.zip(self.weights.windows(4))792.zip(self.knots.windows(8))793.filter(|(_, knots)| knots[4] - knots[3] > 0.0)794.map(|((points, weights), knots)| {795// This is curve segment i. It uses control points P_i, P_i+2, P_i+2 and P_i+3,796// It is associated with knot span i+3 (which is the interval between knots i+3797// and i+4) and its characteristic matrix uses knots i+1 through i+6 (because798// those define the two knot spans on either side).799let span = knots[4] - knots[3];800let coefficient_knots = knots.try_into().expect("Knot windows are of length 6");801let matrix = Self::generate_matrix(coefficient_knots);802RationalSegment::coefficients(803points.try_into().expect("Point windows are of length 4"),804weights.try_into().expect("Weight windows are of length 4"),805span,806matrix,807)808})809.collect_vec();810if segments.is_empty() {811Err(InsufficientDataError {812expected: 4,813given: self.control_points.len(),814})815} else {816Ok(RationalCurve { segments })817}818}819}820821/// A spline interpolated linearly between the nearest 2 points.822///823/// ### Interpolation824///825/// The curve passes through every control point.826///827/// ### Tangency828///829/// The curve is not generally differentiable at control points.830///831/// ### Continuity832///833/// The curve is C0 continuous, meaning it has no holes or jumps.834///835/// ### Parametrization836///837/// Each curve segment connects two adjacent control points in sequence. When a cyclic curve is838/// formed with [`to_curve_cyclic`], the final segment connects the last control point with the first.839///840/// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic841#[derive(Clone, Debug)]842#[cfg(feature = "alloc")]843#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]844pub struct LinearSpline<P: VectorSpace> {845/// The control points of the linear spline.846pub points: Vec<P>,847}848849#[cfg(feature = "alloc")]850impl<P: VectorSpace> LinearSpline<P> {851/// Create a new linear spline from a list of points to be interpolated.852pub fn new(points: impl IntoIterator<Item = P>) -> Self {853Self {854points: points.into_iter().collect(),855}856}857}858859#[cfg(feature = "alloc")]860impl<P: VectorSpace> CubicGenerator<P> for LinearSpline<P> {861type Error = InsufficientDataError;862863#[inline]864fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error> {865let segments = self866.points867.windows(2)868.map(|points| {869let a = points[0];870let b = points[1];871CubicSegment {872coeff: [a, b - a, P::default(), P::default()],873}874})875.collect_vec();876877if segments.is_empty() {878Err(InsufficientDataError {879expected: 2,880given: self.points.len(),881})882} else {883Ok(CubicCurve { segments })884}885}886}887888#[cfg(feature = "alloc")]889impl<P: VectorSpace> CyclicCubicGenerator<P> for LinearSpline<P> {890type Error = InsufficientDataError;891892#[inline]893fn to_curve_cyclic(&self) -> Result<CubicCurve<P>, Self::Error> {894let segments = self895.points896.iter()897.circular_tuple_windows()898.map(|(&a, &b)| CubicSegment {899coeff: [a, b - a, P::default(), P::default()],900})901.collect_vec();902903if segments.is_empty() {904Err(InsufficientDataError {905expected: 2,906given: self.points.len(),907})908} else {909Ok(CubicCurve { segments })910}911}912}913914/// An error indicating that a spline construction didn't have enough control points to generate a curve.915#[derive(Clone, Debug, Error)]916#[error("Not enough data to build curve: needed at least {expected} control points but was only given {given}")]917pub struct InsufficientDataError {918expected: usize,919given: usize,920}921922/// Implement this on cubic splines that can generate a cubic curve from their spline parameters.923#[cfg(feature = "alloc")]924pub trait CubicGenerator<P: VectorSpace> {925/// An error type indicating why construction might fail.926type Error;927928/// Build a [`CubicCurve`] by computing the interpolation coefficients for each curve segment.929fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error>;930}931932/// Implement this on cubic splines that can generate a cyclic cubic curve from their spline parameters.933///934/// This makes sense only when the control data can be interpreted cyclically.935#[cfg(feature = "alloc")]936pub trait CyclicCubicGenerator<P: VectorSpace> {937/// An error type indicating why construction might fail.938type Error;939940/// Build a cyclic [`CubicCurve`] by computing the interpolation coefficients for each curve segment,941/// treating the control data as cyclic so that the result is a closed curve.942fn to_curve_cyclic(&self) -> Result<CubicCurve<P>, Self::Error>;943}944945/// A segment of a cubic curve, used to hold precomputed coefficients for fast interpolation.946/// It is a [`Curve`] with domain `[0, 1]`.947///948/// Segments can be chained together to form a longer [compound curve].949///950/// [compound curve]: CubicCurve951/// [`Curve`]: crate::curve::Curve952#[derive(Copy, Clone, Debug, Default, PartialEq)]953#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]954#[cfg_attr(955feature = "bevy_reflect",956derive(Reflect),957reflect(Debug, Default, Clone)958)]959pub struct CubicSegment<P: VectorSpace> {960/// Polynomial coefficients for the segment.961pub coeff: [P; 4],962}963964impl<P: VectorSpace<Scalar = f32>> CubicSegment<P> {965/// Instantaneous position of a point at parametric value `t`.966#[inline]967pub fn position(&self, t: f32) -> P {968let [a, b, c, d] = self.coeff;969// Evaluate `a + bt + ct^2 + dt^3`, avoiding exponentiation970a + (b + (c + d * t) * t) * t971}972973/// Instantaneous velocity of a point at parametric value `t`.974#[inline]975pub fn velocity(&self, t: f32) -> P {976let [_, b, c, d] = self.coeff;977// Evaluate the derivative, which is `b + 2ct + 3dt^2`, avoiding exponentiation978b + (c * 2.0 + d * 3.0 * t) * t979}980981/// Instantaneous acceleration of a point at parametric value `t`.982#[inline]983pub fn acceleration(&self, t: f32) -> P {984let [_, _, c, d] = self.coeff;985// Evaluate the second derivative, which is `2c + 6dt`986c * 2.0 + d * 6.0 * t987}988989/// Creates a cubic segment from four points, representing a Bezier curve.990pub fn new_bezier(points: [P; 4]) -> Self {991// A derivation for this matrix can be found in "General Matrix Representations for B-splines" by Kaihuai Qin.992// <https://xiaoxingchen.github.io/2020/03/02/bspline_in_so3/general_matrix_representation_for_bsplines.pdf>993// See section 4.2 and equation 11.994let char_matrix = [995[1., 0., 0., 0.],996[-3., 3., 0., 0.],997[3., -6., 3., 0.],998[-1., 3., -3., 1.],999];1000Self::coefficients(points, char_matrix)1001}10021003/// Calculate polynomial coefficients for the cubic curve using a characteristic matrix.1004#[inline]1005fn coefficients(p: [P; 4], char_matrix: [[f32; 4]; 4]) -> Self {1006let [c0, c1, c2, c3] = char_matrix;1007// These are the polynomial coefficients, computed by multiplying the characteristic1008// matrix by the point matrix.1009let coeff = [1010p[0] * c0[0] + p[1] * c0[1] + p[2] * c0[2] + p[3] * c0[3],1011p[0] * c1[0] + p[1] * c1[1] + p[2] * c1[2] + p[3] * c1[3],1012p[0] * c2[0] + p[1] * c2[1] + p[2] * c2[2] + p[3] * c2[3],1013p[0] * c3[0] + p[1] * c3[1] + p[2] * c3[2] + p[3] * c3[3],1014];1015Self { coeff }1016}10171018/// A flexible iterator used to sample curves with arbitrary functions.1019///1020/// This splits the curve into `subdivisions` of evenly spaced `t` values across the1021/// length of the curve from start (t = 0) to end (t = n), where `n = self.segment_count()`,1022/// returning an iterator evaluating the curve with the supplied `sample_function` at each `t`.1023///1024/// For `subdivisions = 2`, this will split the curve into two lines, or three points, and1025/// return an iterator with 3 items, the three points, one at the start, middle, and end.1026#[inline]1027pub fn iter_samples<'a, 'b: 'a>(1028&'b self,1029subdivisions: usize,1030mut sample_function: impl FnMut(&Self, f32) -> P + 'a,1031) -> impl Iterator<Item = P> + 'a {1032self.iter_uniformly(subdivisions)1033.map(move |t| sample_function(self, t))1034}10351036/// An iterator that returns values of `t` uniformly spaced over `0..=subdivisions`.1037#[inline]1038pub fn iter_uniformly(&self, subdivisions: usize) -> impl Iterator<Item = f32> {1039let step = 1.0 / subdivisions as f32;1040(0..=subdivisions).map(move |i| i as f32 * step)1041}10421043/// Iterate over the curve split into `subdivisions`, sampling the position at each step.1044pub fn iter_positions(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {1045self.iter_samples(subdivisions, Self::position)1046}10471048/// Iterate over the curve split into `subdivisions`, sampling the velocity at each step.1049pub fn iter_velocities(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {1050self.iter_samples(subdivisions, Self::velocity)1051}10521053/// Iterate over the curve split into `subdivisions`, sampling the acceleration at each step.1054pub fn iter_accelerations(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {1055self.iter_samples(subdivisions, Self::acceleration)1056}1057}10581059/// The `CubicSegment<Vec2>` can be used as a 2-dimensional easing curve for animation.1060///1061/// The x-axis of the curve is time, and the y-axis is the output value. This struct provides1062/// methods for extremely fast solves for y given x.1063impl CubicSegment<Vec2> {1064/// Construct a cubic Bezier curve for animation easing, with control points `p1` and `p2`. A1065/// cubic Bezier easing curve has control point `p0` at (0, 0) and `p3` at (1, 1), leaving only1066/// `p1` and `p2` as the remaining degrees of freedom. The first and last control points are1067/// fixed to ensure the animation begins at 0, and ends at 1.1068///1069/// This is a very common tool for UI animations that accelerate and decelerate smoothly. For1070/// example, the ubiquitous "ease-in-out" is defined as `(0.25, 0.1), (0.25, 1.0)`.1071#[cfg(feature = "alloc")]1072pub fn new_bezier_easing(p1: impl Into<Vec2>, p2: impl Into<Vec2>) -> Self {1073let (p0, p3) = (Vec2::ZERO, Vec2::ONE);1074Self::new_bezier([p0, p1.into(), p2.into(), p3])1075}10761077/// Maximum allowable error for iterative Bezier solve1078const MAX_ERROR: f32 = 1e-5;10791080/// Maximum number of iterations during Bezier solve1081const MAX_ITERS: u8 = 8;10821083/// Given a `time` within `0..=1`, returns an eased value that follows the cubic curve instead1084/// of a straight line. This eased result may be outside the range `0..=1`, however it will1085/// always start at 0 and end at 1: `ease(0) = 0` and `ease(1) = 1`.1086///1087/// ```1088/// # use bevy_math::prelude::*;1089/// # #[cfg(feature = "alloc")]1090/// # {1091/// let cubic_bezier = CubicSegment::new_bezier_easing((0.25, 0.1), (0.25, 1.0));1092/// assert_eq!(cubic_bezier.ease(0.0), 0.0);1093/// assert_eq!(cubic_bezier.ease(1.0), 1.0);1094/// # }1095/// ```1096///1097/// # How cubic easing works1098///1099/// Easing is generally accomplished with the help of "shaping functions". These are curves that1100/// start at (0,0) and end at (1,1). The x-axis of this plot is the current `time` of the1101/// animation, from 0 to 1. The y-axis is how far along the animation is, also from 0 to 1. You1102/// can imagine that if the shaping function is a straight line, there is a 1:1 mapping between1103/// the `time` and how far along your animation is. If the `time` = 0.5, the animation is1104/// halfway through. This is known as linear interpolation, and results in objects animating1105/// with a constant velocity, and no smooth acceleration or deceleration at the start or end.1106///1107/// ```text1108/// y1109/// │ ●1110/// │ ⬈1111/// │ ⬈1112/// │ ⬈1113/// │ ⬈1114/// ●─────────── x (time)1115/// ```1116///1117/// Using cubic Beziers, we have a curve that starts at (0,0), ends at (1,1), and follows a path1118/// determined by the two remaining control points (handles). These handles allow us to define a1119/// smooth curve. As `time` (x-axis) progresses, we now follow the curve, and use the `y` value1120/// to determine how far along the animation is.1121///1122/// ```text1123/// y1124/// ⬈➔●1125/// │ ⬈1126/// │ ↑1127/// │ ↑1128/// │ ⬈1129/// ●➔⬈───────── x (time)1130/// ```1131///1132/// To accomplish this, we need to be able to find the position `y` on a curve, given the `x`1133/// value. Cubic curves are implicit parametric functions like B(t) = (x,y). To find `y`, we1134/// first solve for `t` that corresponds to the given `x` (`time`). We use the Newton-Raphson1135/// root-finding method to quickly find a value of `t` that is very near the desired value of1136/// `x`. Once we have this we can easily plug that `t` into our curve's `position` function, to1137/// find the `y` component, which is how far along our animation should be. In other words:1138///1139/// > Given `time` in `0..=1`1140///1141/// > Use Newton's method to find a value of `t` that results in B(t) = (x,y) where `x == time`1142///1143/// > Once a solution is found, use the resulting `y` value as the final result1144#[inline]1145pub fn ease(&self, time: f32) -> f32 {1146let x = time.clamp(0.0, 1.0);1147self.find_y_given_x(x)1148}11491150/// Find the `y` value of the curve at the given `x` value using the Newton-Raphson method.1151#[inline]1152fn find_y_given_x(&self, x: f32) -> f32 {1153let mut t_guess = x;1154let mut pos_guess = Vec2::ZERO;1155for _ in 0..Self::MAX_ITERS {1156pos_guess = self.position(t_guess);1157let error = pos_guess.x - x;1158if ops::abs(error) <= Self::MAX_ERROR {1159break;1160}1161// Using Newton's method, use the tangent line to estimate a better guess value.1162let slope = self.velocity(t_guess).x; // dx/dt1163t_guess -= error / slope;1164}1165pos_guess.y1166}1167}11681169/// A collection of [`CubicSegment`]s chained into a single parametric curve. It is a [`Curve`]1170/// with domain `[0, N]`, where `N` is its number of segments.1171///1172/// Use any struct that implements the [`CubicGenerator`] trait to create a new curve, such as1173/// [`CubicBezier`].1174///1175/// [`Curve`]: crate::curve::Curve1176#[derive(Clone, Debug, PartialEq)]1177#[cfg(feature = "alloc")]1178#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]1179#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]1180pub struct CubicCurve<P: VectorSpace> {1181/// The segments comprising the curve. This must always be nonempty.1182segments: Vec<CubicSegment<P>>,1183}11841185#[cfg(feature = "alloc")]1186impl<P: VectorSpace<Scalar = f32>> CubicCurve<P> {1187/// Create a new curve from a collection of segments. If the collection of segments is empty,1188/// a curve cannot be built and `None` will be returned instead.1189pub fn from_segments(segments: impl IntoIterator<Item = CubicSegment<P>>) -> Option<Self> {1190let segments: Vec<_> = segments.into_iter().collect();1191if segments.is_empty() {1192None1193} else {1194Some(Self { segments })1195}1196}11971198/// Compute the position of a point on the cubic curve at the parametric value `t`.1199///1200/// Note that `t` varies from `0..=(n_points - 3)`.1201#[inline]1202pub fn position(&self, t: f32) -> P {1203let (segment, t) = self.segment(t);1204segment.position(t)1205}12061207/// Compute the first derivative with respect to t at `t`. This is the instantaneous velocity of1208/// a point on the cubic curve at `t`.1209///1210/// Note that `t` varies from `0..=(n_points - 3)`.1211#[inline]1212pub fn velocity(&self, t: f32) -> P {1213let (segment, t) = self.segment(t);1214segment.velocity(t)1215}12161217/// Compute the second derivative with respect to t at `t`. This is the instantaneous1218/// acceleration of a point on the cubic curve at `t`.1219///1220/// Note that `t` varies from `0..=(n_points - 3)`.1221#[inline]1222pub fn acceleration(&self, t: f32) -> P {1223let (segment, t) = self.segment(t);1224segment.acceleration(t)1225}12261227/// A flexible iterator used to sample curves with arbitrary functions.1228///1229/// This splits the curve into `subdivisions` of evenly spaced `t` values across the1230/// length of the curve from start (t = 0) to end (t = n), where `n = self.segment_count()`,1231/// returning an iterator evaluating the curve with the supplied `sample_function` at each `t`.1232///1233/// For `subdivisions = 2`, this will split the curve into two lines, or three points, and1234/// return an iterator with 3 items, the three points, one at the start, middle, and end.1235#[inline]1236pub fn iter_samples<'a, 'b: 'a>(1237&'b self,1238subdivisions: usize,1239mut sample_function: impl FnMut(&Self, f32) -> P + 'a,1240) -> impl Iterator<Item = P> + 'a {1241self.iter_uniformly(subdivisions)1242.map(move |t| sample_function(self, t))1243}12441245/// An iterator that returns values of `t` uniformly spaced over `0..=subdivisions`.1246#[inline]1247fn iter_uniformly(&self, subdivisions: usize) -> impl Iterator<Item = f32> {1248let segments = self.segments.len() as f32;1249let step = segments / subdivisions as f32;1250(0..=subdivisions).map(move |i| i as f32 * step)1251}12521253/// The list of segments contained in this `CubicCurve`.1254///1255/// This spline's global `t` value is equal to how many segments it has.1256///1257/// All method accepting `t` on `CubicCurve` depends on the global `t`.1258/// When sampling over the entire curve, you should either use one of the1259/// `iter_*` methods or account for the segment count using `curve.segments().len()`.1260#[inline]1261pub fn segments(&self) -> &[CubicSegment<P>] {1262&self.segments1263}12641265/// Iterate over the curve split into `subdivisions`, sampling the position at each step.1266pub fn iter_positions(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {1267self.iter_samples(subdivisions, Self::position)1268}12691270/// Iterate over the curve split into `subdivisions`, sampling the velocity at each step.1271pub fn iter_velocities(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {1272self.iter_samples(subdivisions, Self::velocity)1273}12741275/// Iterate over the curve split into `subdivisions`, sampling the acceleration at each step.1276pub fn iter_accelerations(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {1277self.iter_samples(subdivisions, Self::acceleration)1278}12791280#[inline]1281/// Adds a segment to the curve1282pub fn push_segment(&mut self, segment: CubicSegment<P>) {1283self.segments.push(segment);1284}12851286/// Returns the [`CubicSegment`] and local `t` value given a spline's global `t` value.1287#[inline]1288fn segment(&self, t: f32) -> (&CubicSegment<P>, f32) {1289if self.segments.len() == 1 {1290(&self.segments[0], t)1291} else {1292let i = (ops::floor(t) as usize).clamp(0, self.segments.len() - 1);1293(&self.segments[i], t - i as f32)1294}1295}1296}12971298#[cfg(feature = "alloc")]1299impl<P: VectorSpace> Extend<CubicSegment<P>> for CubicCurve<P> {1300fn extend<T: IntoIterator<Item = CubicSegment<P>>>(&mut self, iter: T) {1301self.segments.extend(iter);1302}1303}13041305#[cfg(feature = "alloc")]1306impl<P: VectorSpace> IntoIterator for CubicCurve<P> {1307type IntoIter = <Vec<CubicSegment<P>> as IntoIterator>::IntoIter;13081309type Item = CubicSegment<P>;13101311fn into_iter(self) -> Self::IntoIter {1312self.segments.into_iter()1313}1314}13151316/// Implement this on cubic splines that can generate a rational cubic curve from their spline parameters.1317#[cfg(feature = "alloc")]1318pub trait RationalGenerator<P: VectorSpace> {1319/// An error type indicating why construction might fail.1320type Error;13211322/// Build a [`RationalCurve`] by computing the interpolation coefficients for each curve segment.1323fn to_curve(&self) -> Result<RationalCurve<P>, Self::Error>;1324}13251326/// A segment of a rational cubic curve, used to hold precomputed coefficients for fast interpolation.1327/// It is a [`Curve`] with domain `[0, 1]`.1328///1329/// Note that the `knot_span` is used only by [compound curves] constructed by chaining these1330/// together.1331///1332/// [compound curves]: RationalCurve1333/// [`Curve`]: crate::curve::Curve1334#[derive(Copy, Clone, Debug, Default, PartialEq)]1335#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]1336#[cfg_attr(1337feature = "bevy_reflect",1338derive(Reflect),1339reflect(Debug, Default, Clone)1340)]1341pub struct RationalSegment<P: VectorSpace> {1342/// The coefficients matrix of the cubic curve.1343pub coeff: [P; 4],1344/// The homogeneous weight coefficients.1345pub weight_coeff: [f32; 4],1346/// The width of the domain of this segment.1347pub knot_span: f32,1348}13491350impl<P: VectorSpace<Scalar = f32>> RationalSegment<P> {1351/// Instantaneous position of a point at parametric value `t` in `[0, 1]`.1352#[inline]1353pub fn position(&self, t: f32) -> P {1354let [a, b, c, d] = self.coeff;1355let [x, y, z, w] = self.weight_coeff;1356// Compute a cubic polynomial for the control points1357let numerator = a + (b + (c + d * t) * t) * t;1358// Compute a cubic polynomial for the weights1359let denominator = x + (y + (z + w * t) * t) * t;1360numerator / denominator1361}13621363/// Instantaneous velocity of a point at parametric value `t` in `[0, 1]`.1364#[inline]1365pub fn velocity(&self, t: f32) -> P {1366// A derivation for the following equations can be found in "Matrix representation for NURBS1367// curves and surfaces" by Choi et al. See equation 19.13681369let [a, b, c, d] = self.coeff;1370let [x, y, z, w] = self.weight_coeff;1371// Compute a cubic polynomial for the control points1372let numerator = a + (b + (c + d * t) * t) * t;1373// Compute a cubic polynomial for the weights1374let denominator = x + (y + (z + w * t) * t) * t;13751376// Compute the derivative of the control point polynomial1377let numerator_derivative = b + (c * 2.0 + d * 3.0 * t) * t;1378// Compute the derivative of the weight polynomial1379let denominator_derivative = y + (z * 2.0 + w * 3.0 * t) * t;13801381// Velocity is the first derivative (wrt to the parameter `t`)1382// Position = N/D therefore1383// Velocity = (N/D)' = N'/D - N * D'/D^2 = (N' * D - N * D')/D^21384numerator_derivative / denominator1385- numerator * (denominator_derivative / denominator.squared())1386}13871388/// Instantaneous acceleration of a point at parametric value `t` in `[0, 1]`.1389#[inline]1390pub fn acceleration(&self, t: f32) -> P {1391// A derivation for the following equations can be found in "Matrix representation for NURBS1392// curves and surfaces" by Choi et al. See equation 20. Note: In come copies of this paper, equation 201393// is printed with the following two errors:1394// + The first term has incorrect sign.1395// + The second term uses R when it should use the first derivative.13961397let [a, b, c, d] = self.coeff;1398let [x, y, z, w] = self.weight_coeff;1399// Compute a cubic polynomial for the control points1400let numerator = a + (b + (c + d * t) * t) * t;1401// Compute a cubic polynomial for the weights1402let denominator = x + (y + (z + w * t) * t) * t;14031404// Compute the derivative of the control point polynomial1405let numerator_derivative = b + (c * 2.0 + d * 3.0 * t) * t;1406// Compute the derivative of the weight polynomial1407let denominator_derivative = y + (z * 2.0 + w * 3.0 * t) * t;14081409// Compute the second derivative of the control point polynomial1410let numerator_second_derivative = c * 2.0 + d * 6.0 * t;1411// Compute the second derivative of the weight polynomial1412let denominator_second_derivative = z * 2.0 + w * 6.0 * t;14131414// Velocity is the first derivative (wrt to the parameter `t`)1415// Position = N/D therefore1416// Velocity = (N/D)' = N'/D - N * D'/D^2 = (N' * D - N * D')/D^21417// Acceleration = (N/D)'' = ((N' * D - N * D')/D^2)' = N''/D + N' * (-2D'/D^2) + N * (-D''/D^2 + 2D'^2/D^3)1418numerator_second_derivative / denominator1419+ numerator_derivative * (-2.0 * denominator_derivative / denominator.squared())1420+ numerator1421* (-denominator_second_derivative / denominator.squared()1422+ 2.0 * denominator_derivative.squared() / denominator.cubed())1423}14241425/// Calculate polynomial coefficients for the cubic polynomials using a characteristic matrix.1426#[cfg_attr(1427not(feature = "alloc"),1428expect(1429dead_code,1430reason = "Method only used when `alloc` feature is enabled."1431)1432)]1433#[inline]1434fn coefficients(1435control_points: [P; 4],1436weights: [f32; 4],1437knot_span: f32,1438char_matrix: [[f32; 4]; 4],1439) -> Self {1440// An explanation of this use can be found in "Matrix representation for NURBS curves and surfaces"1441// by Choi et al. See section "Evaluation of NURB Curves and Surfaces", and equation 16.14421443let [c0, c1, c2, c3] = char_matrix;1444let p = control_points;1445let w = weights;1446// These are the control point polynomial coefficients, computed by multiplying the characteristic1447// matrix by the point matrix.1448let coeff = [1449p[0] * c0[0] + p[1] * c0[1] + p[2] * c0[2] + p[3] * c0[3],1450p[0] * c1[0] + p[1] * c1[1] + p[2] * c1[2] + p[3] * c1[3],1451p[0] * c2[0] + p[1] * c2[1] + p[2] * c2[2] + p[3] * c2[3],1452p[0] * c3[0] + p[1] * c3[1] + p[2] * c3[2] + p[3] * c3[3],1453];1454// These are the weight polynomial coefficients, computed by multiplying the characteristic1455// matrix by the weight matrix.1456let weight_coeff = [1457w[0] * c0[0] + w[1] * c0[1] + w[2] * c0[2] + w[3] * c0[3],1458w[0] * c1[0] + w[1] * c1[1] + w[2] * c1[2] + w[3] * c1[3],1459w[0] * c2[0] + w[1] * c2[1] + w[2] * c2[2] + w[3] * c2[3],1460w[0] * c3[0] + w[1] * c3[1] + w[2] * c3[2] + w[3] * c3[3],1461];1462Self {1463coeff,1464weight_coeff,1465knot_span,1466}1467}1468}14691470/// A collection of [`RationalSegment`]s chained into a single parametric curve. It is a [`Curve`]1471/// with domain `[0, N]`, where `N` is the number of segments.1472///1473/// Use any struct that implements the [`RationalGenerator`] trait to create a new curve, such as1474/// [`CubicNurbs`], or convert [`CubicCurve`] using `into/from`.1475///1476/// [`Curve`]: crate::curve::Curve1477#[derive(Clone, Debug, PartialEq)]1478#[cfg(feature = "alloc")]1479#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]1480#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]1481pub struct RationalCurve<P: VectorSpace> {1482/// The segments comprising the curve. This must always be nonempty.1483segments: Vec<RationalSegment<P>>,1484}14851486#[cfg(feature = "alloc")]1487impl<P: VectorSpace<Scalar = f32>> RationalCurve<P> {1488/// Create a new curve from a collection of segments. If the collection of segments is empty,1489/// a curve cannot be built and `None` will be returned instead.1490pub fn from_segments(segments: impl IntoIterator<Item = RationalSegment<P>>) -> Option<Self> {1491let segments: Vec<_> = segments.into_iter().collect();1492if segments.is_empty() {1493None1494} else {1495Some(Self { segments })1496}1497}14981499/// Compute the position of a point on the curve at the parametric value `t`.1500///1501/// Note that `t` varies from `0` to `self.length()`.1502#[inline]1503pub fn position(&self, t: f32) -> P {1504let (segment, t) = self.segment(t);1505segment.position(t)1506}15071508/// Compute the first derivative with respect to t at `t`. This is the instantaneous velocity of1509/// a point on the curve at `t`.1510///1511/// Note that `t` varies from `0` to `self.length()`.1512#[inline]1513pub fn velocity(&self, t: f32) -> P {1514let (segment, t) = self.segment(t);1515segment.velocity(t)1516}15171518/// Compute the second derivative with respect to t at `t`. This is the instantaneous1519/// acceleration of a point on the curve at `t`.1520///1521/// Note that `t` varies from `0` to `self.length()`.1522#[inline]1523pub fn acceleration(&self, t: f32) -> P {1524let (segment, t) = self.segment(t);1525segment.acceleration(t)1526}15271528/// A flexible iterator used to sample curves with arbitrary functions.1529///1530/// This splits the curve into `subdivisions` of evenly spaced `t` values across the1531/// length of the curve from start (t = 0) to end (t = n), where `n = self.segment_count()`,1532/// returning an iterator evaluating the curve with the supplied `sample_function` at each `t`.1533///1534/// For `subdivisions = 2`, this will split the curve into two lines, or three points, and1535/// return an iterator with 3 items, the three points, one at the start, middle, and end.1536#[inline]1537pub fn iter_samples<'a, 'b: 'a>(1538&'b self,1539subdivisions: usize,1540mut sample_function: impl FnMut(&Self, f32) -> P + 'a,1541) -> impl Iterator<Item = P> + 'a {1542self.iter_uniformly(subdivisions)1543.map(move |t| sample_function(self, t))1544}15451546/// An iterator that returns values of `t` uniformly spaced over `0..=subdivisions`.1547#[inline]1548fn iter_uniformly(&self, subdivisions: usize) -> impl Iterator<Item = f32> {1549let length = self.length();1550let step = length / subdivisions as f32;1551(0..=subdivisions).map(move |i| i as f32 * step)1552}15531554/// The list of segments contained in this `RationalCurve`.1555///1556/// This spline's global `t` value is equal to how many segments it has.1557///1558/// All method accepting `t` on `RationalCurve` depends on the global `t`.1559/// When sampling over the entire curve, you should either use one of the1560/// `iter_*` methods or account for the segment count using `curve.segments().len()`.1561#[inline]1562pub fn segments(&self) -> &[RationalSegment<P>] {1563&self.segments1564}15651566/// Iterate over the curve split into `subdivisions`, sampling the position at each step.1567pub fn iter_positions(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {1568self.iter_samples(subdivisions, Self::position)1569}15701571/// Iterate over the curve split into `subdivisions`, sampling the velocity at each step.1572pub fn iter_velocities(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {1573self.iter_samples(subdivisions, Self::velocity)1574}15751576/// Iterate over the curve split into `subdivisions`, sampling the acceleration at each step.1577pub fn iter_accelerations(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {1578self.iter_samples(subdivisions, Self::acceleration)1579}15801581/// Adds a segment to the curve.1582#[inline]1583pub fn push_segment(&mut self, segment: RationalSegment<P>) {1584self.segments.push(segment);1585}15861587/// Returns the [`RationalSegment`] and local `t` value given a spline's global `t` value.1588/// Input `t` will be clamped to the domain of the curve. Returned value will be in `[0, 1]`.1589#[inline]1590fn segment(&self, mut t: f32) -> (&RationalSegment<P>, f32) {1591if t <= 0.0 {1592(&self.segments[0], 0.0)1593} else if self.segments.len() == 1 {1594(&self.segments[0], t / self.segments[0].knot_span)1595} else {1596// Try to fit t into each segment domain1597for segment in self.segments.iter() {1598if t < segment.knot_span {1599// The division here makes t a normalized parameter in [0, 1] that can be properly1600// evaluated against a rational curve segment. See equations 6 & 16 from "Matrix representation1601// of NURBS curves and surfaces" by Choi et al. or equation 3 from "General Matrix1602// Representations for B-Splines" by Qin.1603return (segment, t / segment.knot_span);1604}1605t -= segment.knot_span;1606}1607(self.segments.last().unwrap(), 1.0)1608}1609}16101611/// Returns the length of the domain of the parametric curve.1612#[inline]1613pub fn length(&self) -> f32 {1614self.segments.iter().map(|segment| segment.knot_span).sum()1615}1616}16171618#[cfg(feature = "alloc")]1619impl<P: VectorSpace> Extend<RationalSegment<P>> for RationalCurve<P> {1620fn extend<T: IntoIterator<Item = RationalSegment<P>>>(&mut self, iter: T) {1621self.segments.extend(iter);1622}1623}16241625#[cfg(feature = "alloc")]1626impl<P: VectorSpace> IntoIterator for RationalCurve<P> {1627type IntoIter = <Vec<RationalSegment<P>> as IntoIterator>::IntoIter;16281629type Item = RationalSegment<P>;16301631fn into_iter(self) -> Self::IntoIter {1632self.segments.into_iter()1633}1634}16351636impl<P: VectorSpace> From<CubicSegment<P>> for RationalSegment<P> {1637fn from(value: CubicSegment<P>) -> Self {1638Self {1639coeff: value.coeff,1640weight_coeff: [1.0, 0.0, 0.0, 0.0],1641knot_span: 1.0, // Cubic curves are uniform, so every segment has domain [0, 1).1642}1643}1644}16451646#[cfg(feature = "alloc")]1647impl<P: VectorSpace> From<CubicCurve<P>> for RationalCurve<P> {1648fn from(value: CubicCurve<P>) -> Self {1649Self {1650segments: value.segments.into_iter().map(Into::into).collect(),1651}1652}1653}16541655#[cfg(feature = "alloc")]1656#[cfg(test)]1657mod tests {1658use crate::{1659cubic_splines::{1660CubicBSpline, CubicBezier, CubicGenerator, CubicNurbs, CubicSegment, RationalCurve,1661RationalGenerator,1662},1663ops::{self, FloatPow},1664};1665use alloc::vec::Vec;1666use glam::{vec2, Vec2};16671668/// How close two floats can be and still be considered equal1669const FLOAT_EQ: f32 = 1e-5;16701671/// Sweep along the full length of a 3D cubic Bezier, and manually check the position.1672#[test]1673fn cubic() {1674const N_SAMPLES: usize = 1000;1675let points = [[1676vec2(-1.0, -20.0),1677vec2(3.0, 2.0),1678vec2(5.0, 3.0),1679vec2(9.0, 8.0),1680]];1681let bezier = CubicBezier::new(points).to_curve().unwrap();1682for i in 0..=N_SAMPLES {1683let t = i as f32 / N_SAMPLES as f32; // Check along entire length1684assert!(bezier.position(t).distance(cubic_manual(t, points[0])) <= FLOAT_EQ);1685}1686}16871688/// Manual, hardcoded function for computing the position along a cubic bezier.1689fn cubic_manual(t: f32, points: [Vec2; 4]) -> Vec2 {1690let p = points;1691p[0] * (1.0 - t).cubed()1692+ 3.0 * p[1] * t * (1.0 - t).squared()1693+ 3.0 * p[2] * t.squared() * (1.0 - t)1694+ p[3] * t.cubed()1695}16961697/// Basic cubic Bezier easing test to verify the shape of the curve.1698#[test]1699fn easing_simple() {1700// A curve similar to ease-in-out, but symmetric1701let bezier = CubicSegment::new_bezier_easing([1.0, 0.0], [0.0, 1.0]);1702assert_eq!(bezier.ease(0.0), 0.0);1703assert!(bezier.ease(0.2) < 0.2); // tests curve1704assert_eq!(bezier.ease(0.5), 0.5); // true due to symmetry1705assert!(bezier.ease(0.8) > 0.8); // tests curve1706assert_eq!(bezier.ease(1.0), 1.0);1707}17081709/// A curve that forms an upside-down "U", that should extend below 0.0. Useful for animations1710/// that go beyond the start and end positions, e.g. bouncing.1711#[test]1712fn easing_overshoot() {1713// A curve that forms an upside-down "U", that should extend above 1.01714let bezier = CubicSegment::new_bezier_easing([0.0, 2.0], [1.0, 2.0]);1715assert_eq!(bezier.ease(0.0), 0.0);1716assert!(bezier.ease(0.5) > 1.5);1717assert_eq!(bezier.ease(1.0), 1.0);1718}17191720/// A curve that forms a "U", that should extend below 0.0. Useful for animations that go beyond1721/// the start and end positions, e.g. bouncing.1722#[test]1723fn easing_undershoot() {1724let bezier = CubicSegment::new_bezier_easing([0.0, -2.0], [1.0, -2.0]);1725assert_eq!(bezier.ease(0.0), 0.0);1726assert!(bezier.ease(0.5) < -0.5);1727assert_eq!(bezier.ease(1.0), 1.0);1728}17291730/// Test that a simple cardinal spline passes through all of its control points with1731/// the correct tangents.1732#[test]1733fn cardinal_control_pts() {1734use super::CubicCardinalSpline;17351736let tension = 0.2;1737let [p0, p1, p2, p3] = [vec2(-1., -2.), vec2(0., 1.), vec2(1., 2.), vec2(-2., 1.)];1738let curve = CubicCardinalSpline::new(tension, [p0, p1, p2, p3])1739.to_curve()1740.unwrap();17411742// Positions at segment endpoints1743assert!(curve.position(0.).abs_diff_eq(p0, FLOAT_EQ));1744assert!(curve.position(1.).abs_diff_eq(p1, FLOAT_EQ));1745assert!(curve.position(2.).abs_diff_eq(p2, FLOAT_EQ));1746assert!(curve.position(3.).abs_diff_eq(p3, FLOAT_EQ));17471748// Tangents at segment endpoints1749assert!(curve1750.velocity(0.)1751.abs_diff_eq((p1 - p0) * tension * 2., FLOAT_EQ));1752assert!(curve1753.velocity(1.)1754.abs_diff_eq((p2 - p0) * tension, FLOAT_EQ));1755assert!(curve1756.velocity(2.)1757.abs_diff_eq((p3 - p1) * tension, FLOAT_EQ));1758assert!(curve1759.velocity(3.)1760.abs_diff_eq((p3 - p2) * tension * 2., FLOAT_EQ));1761}17621763/// Test that [`RationalCurve`] properly generalizes [`CubicCurve`]. A Cubic upgraded to a rational1764/// should produce pretty much the same output.1765#[test]1766fn cubic_to_rational() {1767const EPSILON: f32 = 0.00001;17681769let points = [1770vec2(0.0, 0.0),1771vec2(1.0, 1.0),1772vec2(1.0, 1.0),1773vec2(2.0, -1.0),1774vec2(3.0, 1.0),1775vec2(0.0, 0.0),1776];17771778let b_spline = CubicBSpline::new(points).to_curve().unwrap();1779let rational_b_spline = RationalCurve::from(b_spline.clone());17801781/// Tests if two vectors of points are approximately the same1782fn compare_vectors(cubic_curve: Vec<Vec2>, rational_curve: Vec<Vec2>, name: &str) {1783assert_eq!(1784cubic_curve.len(),1785rational_curve.len(),1786"{name} vector lengths mismatch"1787);1788for (i, (a, b)) in cubic_curve.iter().zip(rational_curve.iter()).enumerate() {1789assert!(1790a.distance(*b) < EPSILON,1791"Mismatch at {name} value {i}. CubicCurve: {a} Converted RationalCurve: {b}",1792);1793}1794}17951796// Both curves should yield the same values1797let cubic_positions: Vec<_> = b_spline.iter_positions(10).collect();1798let rational_positions: Vec<_> = rational_b_spline.iter_positions(10).collect();1799compare_vectors(cubic_positions, rational_positions, "position");18001801let cubic_velocities: Vec<_> = b_spline.iter_velocities(10).collect();1802let rational_velocities: Vec<_> = rational_b_spline.iter_velocities(10).collect();1803compare_vectors(cubic_velocities, rational_velocities, "velocity");18041805let cubic_accelerations: Vec<_> = b_spline.iter_accelerations(10).collect();1806let rational_accelerations: Vec<_> = rational_b_spline.iter_accelerations(10).collect();1807compare_vectors(cubic_accelerations, rational_accelerations, "acceleration");1808}18091810/// Test that a nurbs curve can approximate a portion of a circle.1811#[test]1812fn nurbs_circular_arc() {1813use core::f32::consts::FRAC_PI_2;1814const EPSILON: f32 = 0.0000001;18151816// The following NURBS parameters were determined by constraining the first two1817// points to the line y=1, the second two points to the line x=1, and the distance1818// between each pair of points to be equal. One can solve the weights by assuming the1819// first and last weights to be one, the intermediate weights to be equal, and1820// subjecting ones self to a lot of tedious matrix algebra.18211822let alpha = FRAC_PI_2;1823let leg = 2.0 * ops::sin(alpha / 2.0) / (1.0 + 2.0 * ops::cos(alpha / 2.0));1824let weight = (1.0 + 2.0 * ops::cos(alpha / 2.0)) / 3.0;1825let points = [1826vec2(1.0, 0.0),1827vec2(1.0, leg),1828vec2(leg, 1.0),1829vec2(0.0, 1.0),1830];1831let weights = [1.0, weight, weight, 1.0];1832let knots = [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0];1833let spline = CubicNurbs::new(points, Some(weights), Some(knots)).unwrap();1834let curve = spline.to_curve().unwrap();1835for (i, point) in curve.iter_positions(10).enumerate() {1836assert!(1837ops::abs(point.length() - 1.0) < EPSILON,1838"Point {i} is not on the unit circle: {point:?} has length {}",1839point.length()1840);1841}1842}1843}184418451846