From c60dcea2310aa95ffb556e750a75dd62018e648f Mon Sep 17 00:00:00 2001 From: Matty Weatherley Date: Tue, 10 Dec 2024 15:27:37 -0500 Subject: [PATCH] Derivative access patterns for curves (#16503) # Objective - For curves that also include derivatives, make accessing derivative information via the `Curve` API ergonomic: that is, provide access to a curve that also samples derivative information. - Implement this functionality for cubic spline curves provided by `bevy_math`. Ultimately, this is to serve the purpose of doing more geometric operations on curves, like reparametrization by arclength and the construction of moving frames. ## Solution This has several parts, some of which may seem redundant. However, care has been put into this to satisfy the following constraints: - Accessing a `Curve` that samples derivative information should be not just possible but easy and non-error-prone. For example, given a differentiable `Curve`, one should be able to access something like a `Curve<(Vec2, Vec2)>` ergonomically, and not just sample the derivatives piecemeal from point to point. - Derivative access should not step on the toes of ordinary curve usage. In particular, in the above scenario, we want to avoid simply making the same curve both a `Curve` and a `Curve<(Vec2, Vec2)>` because this requires manual disambiguation when the API is used. - Derivative access must work gracefully in both owned and borrowed contexts. ### `HasTangent` We introduce a trait `HasTangent` that provides an associated `Tangent` type for types that have tangent spaces: ```rust pub trait HasTangent { /// The tangent type. type Tangent: VectorSpace; } ``` (Mathematically speaking, it would be more precise to say that these are types that represent spaces which are canonically [parallelized](https://en.wikipedia.org/wiki/Parallelizable_manifold). ) The idea here is that a point moving through a `HasTangent` type may have a derivative valued in the associated `Tangent` type at each time in its journey. We reify this with a `WithDerivative` type that uses `HasTangent` to include derivative information: ```rust pub struct WithDerivative where T: HasTangent, { /// The underlying value. pub value: T, /// The derivative at `value`. pub derivative: T::Tangent, } ``` And we can play the same game with second derivatives as well, since every `VectorSpace` type is `HasTangent` where `Tangent` is itself (we may want to be more restrictive with this in practice, but this holds mathematically). ```rust pub struct WithTwoDerivatives where T: HasTangent, { /// The underlying value. pub value: T, /// The derivative at `value`. pub derivative: T::Tangent, /// The second derivative at `value`. pub second_derivative: ::Tangent, } ``` In this PR, `HasTangent` is only implemented for `VectorSpace` types, but it would be valuable to have this implementation for types like `Rot2` and `Quat` as well. We could also do it for the isometry types and, potentially, transforms as well. (This is in decreasing order of value in my opinion.) ### `CurveWithDerivative` This is a trait for a `Curve` which allows the construction of a `Curve>` when derivative information is known intrinsically. It looks like this: ```rust /// Trait for curves that have a well-defined notion of derivative, allowing for /// derivatives to be extracted along with values. pub trait CurveWithDerivative where T: HasTangent, { /// This curve, but with its first derivative included in sampling. fn with_derivative(self) -> impl Curve>; } ``` The idea here is to provide patterns like this: ```rust let value_and_derivative = my_curve.with_derivative().sample_clamped(t); ``` One of the main points here is that `Curve>` is useful as an output because it can be used durably. For example, in a dynamic context, something that needs curves with derivatives can store something like a `Box>>`. Note that `CurveWithDerivative` is not dyn-compatible. ### `SampleDerivative` Many curves "know" how to sample their derivatives instrinsically, but implementing `CurveWithDerivative` as given would be onerous or require an annoying amount of boilerplate. There are also hurdles to overcome that involve references to curves: for the `Curve` API, the expectation is that curve transformations like `with_derivative` take things by value, with the contract that they can still be used by reference through deref-magic by including `by_ref` in a method chain. These problems are solved simultaneously by a trait `SampleDerivative` which, when implemented, automatically derives `CurveWithDerivative` for a type and all types that dereference to it. It just looks like this: ```rust pub trait SampleDerivative: Curve where T: HasTangent, { fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative; // ... other sampling variants as default methods } ``` The point is that the output of `with_derivative` is a `Curve>` that uses the `SampleDerivative` implementation. On a `SampleDerivative` type, you can also just call `my_curve.sample_with_derivative(t)` instead of something like `my_curve.by_ref().with_derivative().sample(t)`, which is more verbose and less accessible. In practice, `CurveWithDerivative` is actually a "sealed" extension trait of `SampleDerivative`. ## Adaptors `SampleDerivative` has automatic implementations on all curve adaptors except for `FunctionCurve`, `MapCurve`, and `ReparamCurve` (because we do not have a notion of differentiable Rust functions). For example, `CurveReparamCurve` (the reparametrization of a curve by another curve) can compute derivatives using the chain rule in the case both its constituents have them. ## Testing Tests for derivatives on the curve adaptors are included. --- ## Showcase This development allows derivative information to be included with and extracted from curves using the `Curve` API. ```rust let points = [ vec2(-1.0, -20.0), vec2(3.0, 2.0), vec2(5.0, 3.0), vec2(9.0, 8.0), ]; // A cubic spline curve that goes through `points`. let curve = CubicCardinalSpline::new(0.3, points).to_curve().unwrap(); // Calling `with_derivative` causes derivative output to be included in the output of the curve API. let curve_with_derivative = curve.with_derivative(); // A `Curve` that outputs the speed of the original. let speed_curve = curve_with_derivative.map(|x| x.derivative.norm()); ``` --- ## Questions - ~~Maybe we should seal `WithDerivative` or make it require `SampleDerivative` (i.e. make it unimplementable except through `SampleDerivative`).~~ I decided this is a good idea. - ~~Unclear whether `VectorSpace: HasTangent` blanket implementation is really appropriate. For colors, for example, I'm not sure that the derivative values can really be interpreted as a color. In any case, it should still remain the case that `VectorSpace` types are `HasTangent` and that `HasTangent::Tangent: HasTangent`.~~ I think this is fine. - Infinity bikeshed on names of traits and things. ## Future - Faster implementations of `SampleDerivative` for cubic spline curves. - Improve ergonomics for accessing only derivatives (and other kinds of transformations on derivative curves). - Implement `HasTangent` for: - `Rot2`/`Quat` - `Isometry` types - `Transform`, maybe - Implement derivatives for easing curves. - Marker traits for continuous/differentiable curves. (It's actually unclear to me how much value this has in practice, but we have discussed it in the past.) --------- Co-authored-by: Alice Cecile --- crates/bevy_math/src/common_traits.rs | 130 +++- .../src/cubic_splines/curve_impls.rs | 159 +++++ .../mod.rs} | 120 ++-- crates/bevy_math/src/curve/adaptors.rs | 32 +- .../src/curve/derivatives/adaptor_impls.rs | 648 ++++++++++++++++++ crates/bevy_math/src/curve/derivatives/mod.rs | 226 ++++++ crates/bevy_math/src/curve/mod.rs | 1 + typos.toml | 23 +- 8 files changed, 1243 insertions(+), 96 deletions(-) create mode 100644 crates/bevy_math/src/cubic_splines/curve_impls.rs rename crates/bevy_math/src/{cubic_splines.rs => cubic_splines/mod.rs} (97%) create mode 100644 crates/bevy_math/src/curve/derivatives/adaptor_impls.rs create mode 100644 crates/bevy_math/src/curve/derivatives/mod.rs diff --git a/crates/bevy_math/src/common_traits.rs b/crates/bevy_math/src/common_traits.rs index 455f3dd628..90bc77629e 100644 --- a/crates/bevy_math/src/common_traits.rs +++ b/crates/bevy_math/src/common_traits.rs @@ -30,7 +30,7 @@ pub trait VectorSpace: + Div + Add + Sub - + Neg + + Neg + Default + Debug + Clone @@ -71,6 +71,89 @@ impl VectorSpace for f32 { const ZERO: Self = 0.0; } +/// A type consisting of formal sums of elements from `V` and `W`. That is, +/// each value `Sum(v, w)` is thought of as `v + w`, with no available +/// simplification. In particular, if `V` and `W` are [vector spaces], then +/// `Sum` is a vector space whose dimension is the sum of those of `V` +/// and `W`, and the field accessors `.0` and `.1` are vector space projections. +/// +/// [vector spaces]: VectorSpace +#[derive(Debug, Clone, Copy)] +pub struct Sum(pub V, pub W); + +impl Mul for Sum +where + V: VectorSpace, + W: VectorSpace, +{ + type Output = Self; + fn mul(self, rhs: f32) -> Self::Output { + Sum(self.0 * rhs, self.1 * rhs) + } +} + +impl Div for Sum +where + V: VectorSpace, + W: VectorSpace, +{ + type Output = Self; + fn div(self, rhs: f32) -> Self::Output { + Sum(self.0 / rhs, self.1 / rhs) + } +} + +impl Add for Sum +where + V: VectorSpace, + W: VectorSpace, +{ + type Output = Self; + fn add(self, other: Self) -> Self::Output { + Sum(self.0 + other.0, self.1 + other.1) + } +} + +impl Sub for Sum +where + V: VectorSpace, + W: VectorSpace, +{ + type Output = Self; + fn sub(self, other: Self) -> Self::Output { + Sum(self.0 - other.0, self.1 - other.1) + } +} + +impl Neg for Sum +where + V: VectorSpace, + W: VectorSpace, +{ + type Output = Self; + fn neg(self) -> Self::Output { + Sum(-self.0, -self.1) + } +} + +impl Default for Sum +where + V: VectorSpace, + W: VectorSpace, +{ + fn default() -> Self { + Sum(V::default(), W::default()) + } +} + +impl VectorSpace for Sum +where + V: VectorSpace, + W: VectorSpace, +{ + const ZERO: Self = Sum(V::ZERO, W::ZERO); +} + /// A type that supports the operations of a normed vector space; i.e. a norm operation in addition /// to those of [`VectorSpace`]. Specifically, the implementor must guarantee that the following /// relationships hold, within the limitations of floating point arithmetic: @@ -410,3 +493,48 @@ impl_stable_interpolate_tuple!( (T9, 9), (T10, 10) ); + +/// A type that has tangents. +pub trait HasTangent { + /// The tangent type. + type Tangent: VectorSpace; +} + +/// A value with its derivative. +pub struct WithDerivative +where + T: HasTangent, +{ + /// The underlying value. + pub value: T, + + /// The derivative at `value`. + pub derivative: T::Tangent, +} + +/// A value together with its first and second derivatives. +pub struct WithTwoDerivatives +where + T: HasTangent, +{ + /// The underlying value. + pub value: T, + + /// The derivative at `value`. + pub derivative: T::Tangent, + + /// The second derivative at `value`. + pub second_derivative: ::Tangent, +} + +impl HasTangent for V { + type Tangent = V; +} + +impl HasTangent for (M, N) +where + M: HasTangent, + N: HasTangent, +{ + type Tangent = Sum; +} diff --git a/crates/bevy_math/src/cubic_splines/curve_impls.rs b/crates/bevy_math/src/cubic_splines/curve_impls.rs new file mode 100644 index 0000000000..85fd9fb6ad --- /dev/null +++ b/crates/bevy_math/src/cubic_splines/curve_impls.rs @@ -0,0 +1,159 @@ +use super::{CubicSegment, RationalSegment}; +use crate::common_traits::{VectorSpace, WithDerivative, WithTwoDerivatives}; +use crate::curve::{ + derivatives::{SampleDerivative, SampleTwoDerivatives}, + Curve, Interval, +}; + +#[cfg(feature = "alloc")] +use super::{CubicCurve, RationalCurve}; + +// -- CubicSegment + +impl Curve

for CubicSegment

{ + #[inline] + fn domain(&self) -> Interval { + Interval::UNIT + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> P { + self.position(t) + } +} + +impl SampleDerivative

for CubicSegment

{ + #[inline] + fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative

{ + WithDerivative { + value: self.position(t), + derivative: self.velocity(t), + } + } +} + +impl SampleTwoDerivatives

for CubicSegment

{ + #[inline] + fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives

{ + WithTwoDerivatives { + value: self.position(t), + derivative: self.velocity(t), + second_derivative: self.acceleration(t), + } + } +} + +// -- CubicCurve + +#[cfg(feature = "alloc")] +impl Curve

for CubicCurve

{ + #[inline] + fn domain(&self) -> Interval { + // The non-emptiness invariant guarantees that this succeeds. + Interval::new(0.0, self.segments.len() as f32) + .expect("CubicCurve is invalid because it has no segments") + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> P { + self.position(t) + } +} + +#[cfg(feature = "alloc")] +impl SampleDerivative

for CubicCurve

{ + #[inline] + fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative

{ + WithDerivative { + value: self.position(t), + derivative: self.velocity(t), + } + } +} + +#[cfg(feature = "alloc")] +impl SampleTwoDerivatives

for CubicCurve

{ + #[inline] + fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives

{ + WithTwoDerivatives { + value: self.position(t), + derivative: self.velocity(t), + second_derivative: self.acceleration(t), + } + } +} + +// -- RationalSegment + +impl Curve

for RationalSegment

{ + #[inline] + fn domain(&self) -> Interval { + Interval::UNIT + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> P { + self.position(t) + } +} + +impl SampleDerivative

for RationalSegment

{ + #[inline] + fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative

{ + WithDerivative { + value: self.position(t), + derivative: self.velocity(t), + } + } +} + +impl SampleTwoDerivatives

for RationalSegment

{ + #[inline] + fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives

{ + WithTwoDerivatives { + value: self.position(t), + derivative: self.velocity(t), + second_derivative: self.acceleration(t), + } + } +} + +// -- RationalCurve + +#[cfg(feature = "alloc")] +impl Curve

for RationalCurve

{ + #[inline] + fn domain(&self) -> Interval { + // The non-emptiness invariant guarantees the success of this. + Interval::new(0.0, self.length()) + .expect("RationalCurve is invalid because it has zero length") + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> P { + self.position(t) + } +} + +#[cfg(feature = "alloc")] +impl SampleDerivative

for RationalCurve

{ + #[inline] + fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative

{ + WithDerivative { + value: self.position(t), + derivative: self.velocity(t), + } + } +} + +#[cfg(feature = "alloc")] +impl SampleTwoDerivatives

for RationalCurve

{ + #[inline] + fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives

{ + WithTwoDerivatives { + value: self.position(t), + derivative: self.velocity(t), + second_derivative: self.acceleration(t), + } + } +} diff --git a/crates/bevy_math/src/cubic_splines.rs b/crates/bevy_math/src/cubic_splines/mod.rs similarity index 97% rename from crates/bevy_math/src/cubic_splines.rs rename to crates/bevy_math/src/cubic_splines/mod.rs index ce57667879..ecc0f789c6 100644 --- a/crates/bevy_math/src/cubic_splines.rs +++ b/crates/bevy_math/src/cubic_splines/mod.rs @@ -1,22 +1,16 @@ //! Provides types for building cubic splines for rendering curves and use with animation easing. -use core::fmt::Debug; - +#[cfg(feature = "curve")] +mod curve_impls; use crate::{ ops::{self, FloatPow}, Vec2, VectorSpace, }; - -use thiserror::Error; - -#[cfg(feature = "alloc")] -use {alloc::vec, alloc::vec::Vec, core::iter::once, itertools::Itertools}; - -#[cfg(feature = "curve")] -use crate::curve::{Curve, Interval}; - #[cfg(feature = "bevy_reflect")] use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use thiserror::Error; +#[cfg(feature = "alloc")] +use {alloc::vec, alloc::vec::Vec, core::iter::once, itertools::Itertools}; /// A spline composed of a single cubic Bezier curve. /// @@ -24,15 +18,18 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; /// [`CubicSegment::new_bezier`] for use in easing. /// /// ### Interpolation +/// /// The curve only passes through the first and last control point in each set of four points. The curve /// is divided into "segments" by every fourth control point. /// /// ### Tangency +/// /// Tangents are manually defined by the two intermediate control points within each set of four points. /// You can think of the control points the curve passes through as "anchors", and as the intermediate /// control points as the anchors displaced along their tangent vectors /// /// ### Continuity +/// /// A Bezier curve is at minimum C0 continuous, meaning it has no holes or jumps. Each curve segment is /// C2, meaning the tangent vector changes smoothly between each set of four control points, but this /// doesn't hold at the control points between segments. Making the whole curve C1 or C2 requires moving @@ -52,8 +49,8 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; /// let bezier = CubicBezier::new(points).to_curve().unwrap(); /// let positions: Vec<_> = bezier.iter_positions(100).collect(); /// ``` -#[cfg(feature = "alloc")] #[derive(Clone, Debug)] +#[cfg(feature = "alloc")] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] pub struct CubicBezier { /// The control points of the Bezier curve. @@ -99,7 +96,6 @@ impl CubicGenerator

for CubicBezier

{ } } } - /// An error returned during cubic curve generation for cubic Bezier curves indicating that a /// segment of control points was not present. #[derive(Clone, Debug, Error)] @@ -114,16 +110,20 @@ pub struct CubicBezierError; /// such as network prediction. /// /// ### Interpolation +/// /// The curve passes through every control point. /// /// ### Tangency +/// /// Tangents are explicitly defined at each control point. /// /// ### Continuity +/// /// The curve is at minimum C1 continuous, meaning that it has no holes or jumps and the tangent vector also /// has no sudden jumps. /// /// ### Parametrization +/// /// The first segment of the curve connects the first two control points, the second connects the second and /// third, and so on. This remains true when a cyclic curve is formed with [`to_curve_cyclic`], in which case /// the final curve segment connects the last control point to the first. @@ -149,8 +149,8 @@ pub struct CubicBezierError; /// ``` /// /// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic -#[cfg(feature = "alloc")] #[derive(Clone, Debug)] +#[cfg(feature = "alloc")] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] pub struct CubicHermite { /// The control points of the Hermite curve. @@ -245,16 +245,20 @@ impl CyclicCubicGenerator

for CubicHermite

{ /// **Note** the Catmull-Rom spline is a special case of Cardinal spline where the tension is 0.5. /// /// ### Interpolation +/// /// The curve passes through every control point. /// /// ### Tangency +/// /// Tangents are automatically computed based on the positions of control points. /// /// ### Continuity +/// /// The curve is at minimum C1, meaning that it is continuous (it has no holes or jumps), and its tangent /// vector is also well-defined everywhere, without sudden jumps. /// /// ### Parametrization +/// /// The first segment of the curve connects the first two control points, the second connects the second and /// third, and so on. This remains true when a cyclic curve is formed with [`to_curve_cyclic`], in which case /// the final curve segment connects the last control point to the first. @@ -274,8 +278,8 @@ impl CyclicCubicGenerator

for CubicHermite

{ /// ``` /// /// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic -#[cfg(feature = "alloc")] #[derive(Clone, Debug)] +#[cfg(feature = "alloc")] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] pub struct CubicCardinalSpline { /// Tension @@ -404,17 +408,20 @@ impl CyclicCubicGenerator

for CubicCardinalSpline

{ /// necessarily pass through any of the control points. /// /// ### Interpolation +/// /// The curve does not necessarily pass through its control points. /// /// ### Tangency /// Tangents are automatically computed based on the positions of control points. /// /// ### Continuity +/// /// The curve is C2 continuous, meaning it has no holes or jumps, the tangent vector changes smoothly along /// the entire curve, and the acceleration also varies continuously. The acceleration continuity of this /// spline makes it useful for camera paths. /// /// ### Parametrization +/// /// Each curve segment is defined by a window of four control points taken in sequence. When [`to_curve_cyclic`] /// is used to form a cyclic curve, the three additional segments used to close the curve come last. /// @@ -433,14 +440,13 @@ impl CyclicCubicGenerator

for CubicCardinalSpline

{ /// ``` /// /// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic -#[cfg(feature = "alloc")] #[derive(Clone, Debug)] +#[cfg(feature = "alloc")] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] pub struct CubicBSpline { /// The control points of the spline pub control_points: Vec

, } - #[cfg(feature = "alloc")] impl CubicBSpline

{ /// Build a new B-Spline. @@ -564,6 +570,7 @@ pub enum CubicNurbsError { /// represent a much more diverse class of curves (like perfect circles and ellipses). /// /// ### Non-uniformity +/// /// The 'NU' part of NURBS stands for "Non-Uniform". This has to do with a parameter called 'knots'. /// The knots are a non-decreasing sequence of floating point numbers. The first and last three pairs of /// knots control the behavior of the curve as it approaches its endpoints. The intermediate pairs @@ -572,16 +579,20 @@ pub enum CubicNurbsError { /// and can create sharp corners. /// /// ### Rationality +/// /// The 'R' part of NURBS stands for "Rational". This has to do with NURBS allowing each control point to /// be assigned a weighting, which controls how much it affects the curve compared to the other points. /// /// ### Interpolation +/// /// The curve will not pass through the control points except where a knot has multiplicity four. /// /// ### Tangency +/// /// Tangents are automatically computed based on the position of control points. /// /// ### Continuity +/// /// When there is no knot multiplicity, the curve is C2 continuous, meaning it has no holes or jumps and the /// tangent vector changes smoothly along the entire curve length. Like the [`CubicBSpline`], the acceleration /// continuity makes it useful for camera paths. Knot multiplicity of 2 in intermediate knots reduces the @@ -606,8 +617,8 @@ pub enum CubicNurbsError { /// .unwrap(); /// let positions: Vec<_> = nurbs.iter_positions(100).collect(); /// ``` -#[cfg(feature = "alloc")] #[derive(Clone, Debug)] +#[cfg(feature = "alloc")] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] pub struct CubicNurbs { /// The control points of the NURBS @@ -822,21 +833,25 @@ impl RationalGenerator

for CubicNurbs

{ /// A spline interpolated linearly between the nearest 2 points. /// /// ### Interpolation +/// /// The curve passes through every control point. /// /// ### Tangency +/// /// The curve is not generally differentiable at control points. /// /// ### Continuity +/// /// The curve is C0 continuous, meaning it has no holes or jumps. /// /// ### Parametrization +/// /// Each curve segment connects two adjacent control points in sequence. When a cyclic curve is /// formed with [`to_curve_cyclic`], the final segment connects the last control point with the first. /// /// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic -#[cfg(feature = "alloc")] #[derive(Clone, Debug)] +#[cfg(feature = "alloc")] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] pub struct LinearSpline { /// The control points of the linear spline. @@ -945,6 +960,7 @@ pub trait CyclicCubicGenerator { /// Segments can be chained together to form a longer [compound curve]. /// /// [compound curve]: CubicCurve +/// [`Curve`]: crate::curve::Curve #[derive(Copy, Clone, Debug, Default, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Default))] @@ -1108,26 +1124,15 @@ impl CubicSegment { } } -#[cfg(feature = "curve")] -impl Curve

for CubicSegment

{ - #[inline] - fn domain(&self) -> Interval { - Interval::UNIT - } - - #[inline] - fn sample_unchecked(&self, t: f32) -> P { - self.position(t) - } -} - /// A collection of [`CubicSegment`]s chained into a single parametric curve. It is a [`Curve`] /// with domain `[0, N]`, where `N` is its number of segments. /// /// Use any struct that implements the [`CubicGenerator`] trait to create a new curve, such as /// [`CubicBezier`]. -#[cfg(feature = "alloc")] +/// +/// [`Curve`]: crate::curve::Curve #[derive(Clone, Debug, PartialEq)] +#[cfg(feature = "alloc")] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] pub struct CubicCurve { @@ -1248,21 +1253,6 @@ impl CubicCurve

{ } } -#[cfg(all(feature = "curve", feature = "alloc"))] -impl Curve

for CubicCurve

{ - #[inline] - fn domain(&self) -> Interval { - // The non-emptiness invariant guarantees the success of this. - Interval::new(0.0, self.segments.len() as f32) - .expect("CubicCurve is invalid because it has no segments") - } - - #[inline] - fn sample_unchecked(&self, t: f32) -> P { - self.position(t) - } -} - #[cfg(feature = "alloc")] impl Extend> for CubicCurve

{ fn extend>>(&mut self, iter: T) { @@ -1298,6 +1288,7 @@ pub trait RationalGenerator { /// together. /// /// [compound curves]: RationalCurve +/// [`Curve`]: crate::curve::Curve #[derive(Copy, Clone, Debug, Default, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Default))] @@ -1309,7 +1300,6 @@ pub struct RationalSegment { /// The width of the domain of this segment. pub knot_span: f32, } - impl RationalSegment

{ /// Instantaneous position of a point at parametric value `t` in `[0, 1]`. #[inline] @@ -1424,26 +1414,15 @@ impl RationalSegment

{ } } -#[cfg(feature = "curve")] -impl Curve

for RationalSegment

{ - #[inline] - fn domain(&self) -> Interval { - Interval::UNIT - } - - #[inline] - fn sample_unchecked(&self, t: f32) -> P { - self.position(t) - } -} - /// A collection of [`RationalSegment`]s chained into a single parametric curve. It is a [`Curve`] /// with domain `[0, N]`, where `N` is the number of segments. /// /// Use any struct that implements the [`RationalGenerator`] trait to create a new curve, such as /// [`CubicNurbs`], or convert [`CubicCurve`] using `into/from`. -#[cfg(feature = "alloc")] +/// +/// [`Curve`]: crate::curve::Curve #[derive(Clone, Debug, PartialEq)] +#[cfg(feature = "alloc")] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] pub struct RationalCurve { @@ -1583,21 +1562,6 @@ impl RationalCurve

{ } } -#[cfg(all(feature = "curve", feature = "alloc"))] -impl Curve

for RationalCurve

{ - #[inline] - fn domain(&self) -> Interval { - // The non-emptiness invariant guarantees the success of this. - Interval::new(0.0, self.length()) - .expect("RationalCurve is invalid because it has zero length") - } - - #[inline] - fn sample_unchecked(&self, t: f32) -> P { - self.position(t) - } -} - #[cfg(feature = "alloc")] impl Extend> for RationalCurve

{ fn extend>>(&mut self, iter: T) { diff --git a/crates/bevy_math/src/curve/adaptors.rs b/crates/bevy_math/src/curve/adaptors.rs index 1dfdecdbbc..20e0bcd29c 100644 --- a/crates/bevy_math/src/curve/adaptors.rs +++ b/crates/bevy_math/src/curve/adaptors.rs @@ -620,15 +620,25 @@ where #[inline] fn sample_unchecked(&self, t: f32) -> T { + let t = self.base_curve_sample_time(t); + self.curve.sample_unchecked(t) + } +} + +impl RepeatCurve +where + C: Curve, +{ + #[inline] + pub(crate) fn base_curve_sample_time(&self, t: f32) -> f32 { // the domain is bounded by construction let d = self.curve.domain(); let cyclic_t = ops::rem_euclid(t - d.start(), d.length()); - let t = if t != d.start() && cyclic_t == 0.0 { + if t != d.start() && cyclic_t == 0.0 { d.end() } else { d.start() + cyclic_t - }; - self.curve.sample_unchecked(t) + } } } @@ -668,15 +678,25 @@ where #[inline] fn sample_unchecked(&self, t: f32) -> T { + let t = self.base_curve_sample_time(t); + self.curve.sample_unchecked(t) + } +} + +impl ForeverCurve +where + C: Curve, +{ + #[inline] + pub(crate) fn base_curve_sample_time(&self, t: f32) -> f32 { // the domain is bounded by construction let d = self.curve.domain(); let cyclic_t = ops::rem_euclid(t - d.start(), d.length()); - let t = if t != d.start() && cyclic_t == 0.0 { + if t != d.start() && cyclic_t == 0.0 { d.end() } else { d.start() + cyclic_t - }; - self.curve.sample_unchecked(t) + } } } diff --git a/crates/bevy_math/src/curve/derivatives/adaptor_impls.rs b/crates/bevy_math/src/curve/derivatives/adaptor_impls.rs new file mode 100644 index 0000000000..6a32f1bb20 --- /dev/null +++ b/crates/bevy_math/src/curve/derivatives/adaptor_impls.rs @@ -0,0 +1,648 @@ +//! Implementations of derivatives on curve adaptors. These allow +//! compositionality for derivatives. + +use super::{SampleDerivative, SampleTwoDerivatives}; +use crate::common_traits::{HasTangent, Sum, VectorSpace, WithDerivative, WithTwoDerivatives}; +use crate::curve::{ + adaptors::{ + ChainCurve, ConstantCurve, ContinuationCurve, CurveReparamCurve, ForeverCurve, GraphCurve, + LinearReparamCurve, PingPongCurve, RepeatCurve, ReverseCurve, ZipCurve, + }, + Curve, +}; + +// -- ConstantCurve + +impl SampleDerivative for ConstantCurve +where + T: HasTangent + Clone, +{ + fn sample_with_derivative_unchecked(&self, _t: f32) -> WithDerivative { + WithDerivative { + value: self.value.clone(), + derivative: VectorSpace::ZERO, + } + } +} + +impl SampleTwoDerivatives for ConstantCurve +where + T: HasTangent + Clone, +{ + fn sample_with_two_derivatives_unchecked(&self, _t: f32) -> WithTwoDerivatives { + WithTwoDerivatives { + value: self.value.clone(), + derivative: VectorSpace::ZERO, + second_derivative: VectorSpace::ZERO, + } + } +} + +// -- ChainCurve + +impl SampleDerivative for ChainCurve +where + T: HasTangent, + C: SampleDerivative, + D: SampleDerivative, +{ + fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative { + if t > self.first.domain().end() { + self.second.sample_with_derivative_unchecked( + // `t - first.domain.end` computes the offset into the domain of the second. + t - self.first.domain().end() + self.second.domain().start(), + ) + } else { + self.first.sample_with_derivative_unchecked(t) + } + } +} + +impl SampleTwoDerivatives for ChainCurve +where + T: HasTangent, + C: SampleTwoDerivatives, + D: SampleTwoDerivatives, +{ + fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives { + if t > self.first.domain().end() { + self.second.sample_with_two_derivatives_unchecked( + // `t - first.domain.end` computes the offset into the domain of the second. + t - self.first.domain().end() + self.second.domain().start(), + ) + } else { + self.first.sample_with_two_derivatives_unchecked(t) + } + } +} + +// -- ContinuationCurve + +impl SampleDerivative for ContinuationCurve +where + T: VectorSpace, + C: SampleDerivative, + D: SampleDerivative, +{ + fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative { + if t > self.first.domain().end() { + let mut output = self.second.sample_with_derivative_unchecked( + // `t - first.domain.end` computes the offset into the domain of the second. + t - self.first.domain().end() + self.second.domain().start(), + ); + output.value = output.value + self.offset; + output + } else { + self.first.sample_with_derivative_unchecked(t) + } + } +} + +impl SampleTwoDerivatives for ContinuationCurve +where + T: VectorSpace, + C: SampleTwoDerivatives, + D: SampleTwoDerivatives, +{ + fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives { + if t > self.first.domain().end() { + let mut output = self.second.sample_with_two_derivatives_unchecked( + // `t - first.domain.end` computes the offset into the domain of the second. + t - self.first.domain().end() + self.second.domain().start(), + ); + output.value = output.value + self.offset; + output + } else { + self.first.sample_with_two_derivatives_unchecked(t) + } + } +} + +// -- RepeatCurve + +impl SampleDerivative for RepeatCurve +where + T: HasTangent, + C: SampleDerivative, +{ + fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative { + let t = self.base_curve_sample_time(t); + self.curve.sample_with_derivative_unchecked(t) + } +} + +impl SampleTwoDerivatives for RepeatCurve +where + T: HasTangent, + C: SampleTwoDerivatives, +{ + fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives { + let t = self.base_curve_sample_time(t); + self.curve.sample_with_two_derivatives_unchecked(t) + } +} + +// -- ForeverCurve + +impl SampleDerivative for ForeverCurve +where + T: HasTangent, + C: SampleDerivative, +{ + fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative { + let t = self.base_curve_sample_time(t); + self.curve.sample_with_derivative_unchecked(t) + } +} + +impl SampleTwoDerivatives for ForeverCurve +where + T: HasTangent, + C: SampleTwoDerivatives, +{ + fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives { + let t = self.base_curve_sample_time(t); + self.curve.sample_with_two_derivatives_unchecked(t) + } +} + +// -- PingPongCurve + +impl SampleDerivative for PingPongCurve +where + T: HasTangent, + C: SampleDerivative, +{ + fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative { + if t > self.curve.domain().end() { + let t = self.curve.domain().end() * 2.0 - t; + // The derivative of the preceding expression is -1, so the chain + // rule implies the derivative should be negated. + let mut output = self.curve.sample_with_derivative_unchecked(t); + output.derivative = -output.derivative; + output + } else { + self.curve.sample_with_derivative_unchecked(t) + } + } +} + +impl SampleTwoDerivatives for PingPongCurve +where + T: HasTangent, + C: SampleTwoDerivatives, +{ + fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives { + if t > self.curve.domain().end() { + let t = self.curve.domain().end() * 2.0 - t; + // See the implementation on `ReverseCurve` for an explanation of + // why this is correct. + let mut output = self.curve.sample_with_two_derivatives_unchecked(t); + output.derivative = -output.derivative; + output + } else { + self.curve.sample_with_two_derivatives_unchecked(t) + } + } +} + +// -- ZipCurve + +impl SampleDerivative<(S, T)> for ZipCurve +where + S: HasTangent, + T: HasTangent, + C: SampleDerivative, + D: SampleDerivative, +{ + fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<(S, T)> { + let first_output = self.first.sample_with_derivative_unchecked(t); + let second_output = self.second.sample_with_derivative_unchecked(t); + WithDerivative { + value: (first_output.value, second_output.value), + derivative: Sum(first_output.derivative, second_output.derivative), + } + } +} + +impl SampleTwoDerivatives<(S, T)> for ZipCurve +where + S: HasTangent, + T: HasTangent, + C: SampleTwoDerivatives, + D: SampleTwoDerivatives, +{ + fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<(S, T)> { + let first_output = self.first.sample_with_two_derivatives_unchecked(t); + let second_output = self.second.sample_with_two_derivatives_unchecked(t); + WithTwoDerivatives { + value: (first_output.value, second_output.value), + derivative: Sum(first_output.derivative, second_output.derivative), + second_derivative: Sum( + first_output.second_derivative, + second_output.second_derivative, + ), + } + } +} + +// -- GraphCurve + +impl SampleDerivative<(f32, T)> for GraphCurve +where + T: HasTangent, + C: SampleDerivative, +{ + fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<(f32, T)> { + let output = self.base.sample_with_derivative_unchecked(t); + WithDerivative { + value: (t, output.value), + derivative: Sum(1.0, output.derivative), + } + } +} + +impl SampleTwoDerivatives<(f32, T)> for GraphCurve +where + T: HasTangent, + C: SampleTwoDerivatives, +{ + fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<(f32, T)> { + let output = self.base.sample_with_two_derivatives_unchecked(t); + WithTwoDerivatives { + value: (t, output.value), + derivative: Sum(1.0, output.derivative), + second_derivative: Sum(0.0, output.second_derivative), + } + } +} + +// -- ReverseCurve + +impl SampleDerivative for ReverseCurve +where + T: HasTangent, + C: SampleDerivative, +{ + fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative { + // This gets almost the correct value, but we haven't accounted for the + // reversal of orientation yet. + let mut output = self + .curve + .sample_with_derivative_unchecked(self.domain().end() - (t - self.domain().start())); + + output.derivative = -output.derivative; + + output + } +} + +impl SampleTwoDerivatives for ReverseCurve +where + T: HasTangent, + C: SampleTwoDerivatives, +{ + fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives { + // This gets almost the correct value, but we haven't accounted for the + // reversal of orientation yet. + let mut output = self.curve.sample_with_two_derivatives_unchecked( + self.domain().end() - (t - self.domain().start()), + ); + + output.derivative = -output.derivative; + + // (Note that the reparametrization that reverses the curve satisfies + // g'(t)^2 = 1 and g''(t) = 0, so the second derivative is already + // correct.) + + output + } +} + +// -- CurveReparamCurve + +impl SampleDerivative for CurveReparamCurve +where + T: HasTangent, + C: SampleDerivative, + D: SampleDerivative, +{ + fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative { + // This curve is r(t) = f(g(t)), where f(t) is `self.base` and g(t) + // is `self.reparam_curve`. + + // Start by computing g(t) and g'(t). + let reparam_output = self.reparam_curve.sample_with_derivative_unchecked(t); + + // Compute: + // - value: f(g(t)) + // - derivative: f'(g(t)) + let mut output = self + .base + .sample_with_derivative_unchecked(reparam_output.value); + + // Do the multiplication part of the chain rule. + output.derivative = output.derivative * reparam_output.derivative; + + // value: f(g(t)), derivative: f'(g(t)) g'(t) + output + } +} + +impl SampleTwoDerivatives for CurveReparamCurve +where + T: HasTangent, + C: SampleTwoDerivatives, + D: SampleTwoDerivatives, +{ + fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives { + // This curve is r(t) = f(g(t)), where f(t) is `self.base` and g(t) + // is `self.reparam_curve`. + + // Start by computing g(t), g'(t), g''(t). + let reparam_output = self.reparam_curve.sample_with_two_derivatives_unchecked(t); + + // Compute: + // - value: f(g(t)) + // - derivative: f'(g(t)) + // - second derivative: f''(g(t)) + let mut output = self + .base + .sample_with_two_derivatives_unchecked(reparam_output.value); + + // Set the second derivative according to the chain and product rules + // r''(t) = f''(g(t)) g'(t)^2 + f'(g(t)) g''(t) + output.second_derivative = (output.second_derivative + * (reparam_output.derivative * reparam_output.derivative)) + + (output.derivative * reparam_output.second_derivative); + + // Set the first derivative according to the chain rule + // r'(t) = f'(g(t)) g'(t) + output.derivative = output.derivative * reparam_output.derivative; + + output + } +} + +// -- LinearReparamCurve + +impl SampleDerivative for LinearReparamCurve +where + T: HasTangent, + C: SampleDerivative, +{ + fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative { + // This curve is r(t) = f(g(t)), where f(t) is `self.base` and g(t) is + // the linear map bijecting `self.new_domain` onto `self.base.domain()`. + + // The invariants imply this unwrap always succeeds. + let g = self.new_domain.linear_map_to(self.base.domain()).unwrap(); + + // Compute g'(t) from the domain lengths. + let g_derivative = self.base.domain().length() / self.new_domain.length(); + + // Compute: + // - value: f(g(t)) + // - derivative: f'(g(t)) + let mut output = self.base.sample_with_derivative_unchecked(g(t)); + + // Adjust the derivative according to the chain rule. + output.derivative = output.derivative * g_derivative; + + output + } +} + +impl SampleTwoDerivatives for LinearReparamCurve +where + T: HasTangent, + C: SampleTwoDerivatives, +{ + fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives { + // This curve is r(t) = f(g(t)), where f(t) is `self.base` and g(t) is + // the linear map bijecting `self.new_domain` onto `self.base.domain()`. + + // The invariants imply this unwrap always succeeds. + let g = self.new_domain.linear_map_to(self.base.domain()).unwrap(); + + // Compute g'(t) from the domain lengths. + let g_derivative = self.base.domain().length() / self.new_domain.length(); + + // Compute: + // - value: f(g(t)) + // - derivative: f'(g(t)) + // - second derivative: f''(g(t)) + let mut output = self.base.sample_with_two_derivatives_unchecked(g(t)); + + // Set the second derivative according to the chain and product rules + // r''(t) = f''(g(t)) g'(t)^2 (g''(t) = 0) + output.second_derivative = output.second_derivative * (g_derivative * g_derivative); + + // Set the first derivative according to the chain rule + // r'(t) = f'(g(t)) g'(t) + output.derivative = output.derivative * g_derivative; + + output + } +} + +#[cfg(test)] +mod tests { + + use approx::assert_abs_diff_eq; + + use super::*; + use crate::cubic_splines::{CubicBezier, CubicCardinalSpline, CubicCurve, CubicGenerator}; + use crate::curve::{Curve, Interval}; + use crate::{vec2, Vec2, Vec3}; + + fn test_curve() -> CubicCurve { + let control_pts = [[ + vec2(0.0, 0.0), + vec2(1.0, 0.0), + vec2(0.0, 1.0), + vec2(1.0, 1.0), + ]]; + + CubicBezier::new(control_pts).to_curve().unwrap() + } + + fn other_test_curve() -> CubicCurve { + let control_pts = [ + vec2(1.0, 1.0), + vec2(2.0, 1.0), + vec2(2.0, 0.0), + vec2(1.0, 0.0), + ]; + + CubicCardinalSpline::new(0.5, control_pts) + .to_curve() + .unwrap() + } + + fn reparam_curve() -> CubicCurve { + let control_pts = [[0.0, 0.25, 0.75, 1.0]]; + + CubicBezier::new(control_pts).to_curve().unwrap() + } + + #[test] + fn constant_curve() { + let curve = ConstantCurve::new(Interval::UNIT, Vec3::new(0.2, 1.5, -2.6)); + let jet = curve.sample_with_derivative(0.5).unwrap(); + assert_abs_diff_eq!(jet.derivative, Vec3::ZERO); + } + + #[test] + fn chain_curve() { + let curve1 = test_curve(); + let curve2 = other_test_curve(); + let curve = curve1.by_ref().chain(&curve2).unwrap(); + + let jet = curve.sample_with_derivative(0.65).unwrap(); + let true_jet = curve1.sample_with_derivative(0.65).unwrap(); + assert_abs_diff_eq!(jet.value, true_jet.value); + assert_abs_diff_eq!(jet.derivative, true_jet.derivative); + + let jet = curve.sample_with_derivative(1.1).unwrap(); + let true_jet = curve2.sample_with_derivative(0.1).unwrap(); + assert_abs_diff_eq!(jet.value, true_jet.value); + assert_abs_diff_eq!(jet.derivative, true_jet.derivative); + } + + #[test] + fn continuation_curve() { + let curve1 = test_curve(); + let curve2 = other_test_curve(); + let curve = curve1.by_ref().chain_continue(&curve2).unwrap(); + + let jet = curve.sample_with_derivative(0.99).unwrap(); + let true_jet = curve1.sample_with_derivative(0.99).unwrap(); + assert_abs_diff_eq!(jet.value, true_jet.value); + assert_abs_diff_eq!(jet.derivative, true_jet.derivative); + + let jet = curve.sample_with_derivative(1.3).unwrap(); + let true_jet = curve2.sample_with_derivative(0.3).unwrap(); + assert_abs_diff_eq!(jet.value, true_jet.value); + assert_abs_diff_eq!(jet.derivative, true_jet.derivative); + } + + #[test] + fn repeat_curve() { + let curve1 = test_curve(); + let curve = curve1.by_ref().repeat(3).unwrap(); + + let jet = curve.sample_with_derivative(0.73).unwrap(); + let true_jet = curve1.sample_with_derivative(0.73).unwrap(); + assert_abs_diff_eq!(jet.value, true_jet.value); + assert_abs_diff_eq!(jet.derivative, true_jet.derivative); + + let jet = curve.sample_with_derivative(3.5).unwrap(); + let true_jet = curve1.sample_with_derivative(0.5).unwrap(); + assert_abs_diff_eq!(jet.value, true_jet.value); + assert_abs_diff_eq!(jet.derivative, true_jet.derivative); + } + + #[test] + fn forever_curve() { + let curve1 = test_curve(); + let curve = curve1.by_ref().forever().unwrap(); + + let jet = curve.sample_with_derivative(0.12).unwrap(); + let true_jet = curve1.sample_with_derivative(0.12).unwrap(); + assert_abs_diff_eq!(jet.value, true_jet.value); + assert_abs_diff_eq!(jet.derivative, true_jet.derivative); + + let jet = curve.sample_with_derivative(500.5).unwrap(); + let true_jet = curve1.sample_with_derivative(0.5).unwrap(); + assert_abs_diff_eq!(jet.value, true_jet.value); + assert_abs_diff_eq!(jet.derivative, true_jet.derivative); + } + + #[test] + fn ping_pong_curve() { + let curve1 = test_curve(); + let curve = curve1.by_ref().ping_pong().unwrap(); + + let jet = curve.sample_with_derivative(0.99).unwrap(); + let comparison_jet = curve1.sample_with_derivative(0.99).unwrap(); + assert_abs_diff_eq!(jet.value, comparison_jet.value); + assert_abs_diff_eq!(jet.derivative, comparison_jet.derivative); + + let jet = curve.sample_with_derivative(1.3).unwrap(); + let comparison_jet = curve1.sample_with_derivative(0.7).unwrap(); + assert_abs_diff_eq!(jet.value, comparison_jet.value); + assert_abs_diff_eq!(jet.derivative, -comparison_jet.derivative, epsilon = 1.0e-5); + } + + #[test] + fn zip_curve() { + let curve1 = test_curve(); + let curve2 = other_test_curve(); + let curve = curve1.by_ref().zip(&curve2).unwrap(); + + let jet = curve.sample_with_derivative(0.7).unwrap(); + let comparison_jet1 = curve1.sample_with_derivative(0.7).unwrap(); + let comparison_jet2 = curve2.sample_with_derivative(0.7).unwrap(); + assert_abs_diff_eq!(jet.value.0, comparison_jet1.value); + assert_abs_diff_eq!(jet.value.1, comparison_jet2.value); + let Sum(derivative1, derivative2) = jet.derivative; + assert_abs_diff_eq!(derivative1, comparison_jet1.derivative); + assert_abs_diff_eq!(derivative2, comparison_jet2.derivative); + } + + #[test] + fn graph_curve() { + let curve1 = test_curve(); + let curve = curve1.by_ref().graph(); + + let jet = curve.sample_with_derivative(0.25).unwrap(); + let comparison_jet = curve1.sample_with_derivative(0.25).unwrap(); + assert_abs_diff_eq!(jet.value.0, 0.25); + assert_abs_diff_eq!(jet.value.1, comparison_jet.value); + let Sum(one, derivative) = jet.derivative; + assert_abs_diff_eq!(one, 1.0); + assert_abs_diff_eq!(derivative, comparison_jet.derivative); + } + + #[test] + fn reverse_curve() { + let curve1 = test_curve(); + let curve = curve1.by_ref().reverse().unwrap(); + + let jet = curve.sample_with_derivative(0.23).unwrap(); + let comparison_jet = curve1.sample_with_derivative(0.77).unwrap(); + assert_abs_diff_eq!(jet.value, comparison_jet.value); + assert_abs_diff_eq!(jet.derivative, -comparison_jet.derivative); + } + + #[test] + fn curve_reparam_curve() { + let reparam_curve = reparam_curve(); + let reparam_jet = reparam_curve.sample_with_derivative(0.6).unwrap(); + + let curve1 = test_curve(); + let curve = curve1.by_ref().reparametrize_by_curve(&reparam_curve); + + let jet = curve.sample_with_derivative(0.6).unwrap(); + let base_jet = curve1 + .sample_with_derivative(reparam_curve.sample(0.6).unwrap()) + .unwrap(); + assert_abs_diff_eq!(jet.value, base_jet.value); + assert_abs_diff_eq!(jet.derivative, base_jet.derivative * reparam_jet.derivative); + } + + #[test] + fn linear_reparam_curve() { + let curve1 = test_curve(); + let curve = curve1 + .by_ref() + .reparametrize_linear(Interval::new(0.0, 0.5).unwrap()) + .unwrap(); + + let jet = curve.sample_with_derivative(0.23).unwrap(); + let comparison_jet = curve1.sample_with_derivative(0.46).unwrap(); + assert_abs_diff_eq!(jet.value, comparison_jet.value); + assert_abs_diff_eq!(jet.derivative, comparison_jet.derivative * 2.0); + } +} diff --git a/crates/bevy_math/src/curve/derivatives/mod.rs b/crates/bevy_math/src/curve/derivatives/mod.rs new file mode 100644 index 0000000000..e3b9e531db --- /dev/null +++ b/crates/bevy_math/src/curve/derivatives/mod.rs @@ -0,0 +1,226 @@ +//! This module holds traits related to extracting derivatives from curves. In +//! applications, the derivatives of interest are chiefly the first and second; +//! in this module, these are provided by the traits [`CurveWithDerivative`] +//! and [`CurveWithTwoDerivatives`]. +//! +//! These take ownership of the curve they are used on by default, so that +//! the resulting output may be used in more durable contexts. For example, +//! `CurveWithDerivative` is not dyn-compatible, but `Curve>` +//! is, so if such a curve needs to be stored in a dynamic context, calling +//! [`with_derivative`] and then placing the result in a +//! `Box>>` is sensible. +//! +//! On the other hand, in more transient contexts, consuming a value merely to +//! sample derivatives is inconvenient, and in these cases, it is recommended +//! to use [`by_ref`] when possible to create a referential curve first, retaining +//! liveness of the original. +//! +//! This module also holds the [`SampleDerivative`] and [`SampleTwoDerivatives`] +//! traits, which can be used to easily implement `CurveWithDerivative` and its +//! counterpart. +//! +//! [`with_derivative`]: CurveWithDerivative::with_derivative +//! [`by_ref`]: Curve::by_ref + +pub mod adaptor_impls; + +use crate::{ + common_traits::{HasTangent, WithDerivative, WithTwoDerivatives}, + curve::{Curve, Interval}, +}; +use core::ops::Deref; + +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::{FromReflect, Reflect}; + +/// Trait for curves that have a well-defined notion of derivative, allowing for +/// derivatives to be extracted along with values. +/// +/// This is implemented by implementing [`SampleDerivative`]. +pub trait CurveWithDerivative: SampleDerivative +where + T: HasTangent, +{ + /// This curve, but with its first derivative included in sampling. + fn with_derivative(self) -> impl Curve>; +} + +/// Trait for curves that have a well-defined notion of second derivative, +/// allowing for two derivatives to be extracted along with values. +/// +/// This is implemented by implementing [`SampleTwoDerivatives`]. +pub trait CurveWithTwoDerivatives: SampleTwoDerivatives +where + T: HasTangent, +{ + /// This curve, but with its first two derivatives included in sampling. + fn with_two_derivatives(self) -> impl Curve>; +} + +/// A trait for curves that can sample derivatives in addition to values. +/// +/// Types that implement this trait automatically implement [`CurveWithDerivative`]; +/// the curve produced by [`with_derivative`] uses the sampling defined in the trait +/// implementation. +/// +/// [`with_derivative`]: CurveWithDerivative::with_derivative +pub trait SampleDerivative: Curve +where + T: HasTangent, +{ + /// Sample this curve at the parameter value `t`, extracting the associated value + /// in addition to its derivative. This is the unchecked version of sampling, which + /// should only be used if the sample time `t` is already known to lie within the + /// curve's domain. + /// + /// See [`Curve::sample_unchecked`] for more information. + fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative; + + /// Sample this curve's value and derivative at the parameter value `t`, returning + /// `None` if the point is outside of the curve's domain. + fn sample_with_derivative(&self, t: f32) -> Option> { + match self.domain().contains(t) { + true => Some(self.sample_with_derivative_unchecked(t)), + false => None, + } + } + + /// Sample this curve's value and derivative at the parameter value `t`, clamping `t` + /// to lie inside the domain of the curve. + fn sample_with_derivative_clamped(&self, t: f32) -> WithDerivative { + let t = self.domain().clamp(t); + self.sample_with_derivative_unchecked(t) + } +} + +impl SampleDerivative for D +where + T: HasTangent, + C: SampleDerivative + ?Sized, + D: Deref, +{ + fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative { + >::sample_with_derivative_unchecked(self, t) + } +} + +/// A trait for curves that can sample two derivatives in addition to values. +/// +/// Types that implement this trait automatically implement [`CurveWithTwoDerivatives`]; +/// the curve produced by [`with_two_derivatives`] uses the sampling defined in the trait +/// implementation. +/// +/// [`with_two_derivatives`]: CurveWithTwoDerivatives::with_two_derivatives +pub trait SampleTwoDerivatives: Curve +where + T: HasTangent, +{ + /// Sample this curve at the parameter value `t`, extracting the associated value + /// in addition to two derivatives. This is the unchecked version of sampling, which + /// should only be used if the sample time `t` is already known to lie within the + /// curve's domain. + /// + /// See [`Curve::sample_unchecked`] for more information. + fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives; + + /// Sample this curve's value and two derivatives at the parameter value `t`, returning + /// `None` if the point is outside of the curve's domain. + fn sample_with_two_derivatives(&self, t: f32) -> Option> { + match self.domain().contains(t) { + true => Some(self.sample_with_two_derivatives_unchecked(t)), + false => None, + } + } + + /// Sample this curve's value and two derivatives at the parameter value `t`, clamping `t` + /// to lie inside the domain of the curve. + fn sample_with_two_derivatives_clamped(&self, t: f32) -> WithTwoDerivatives { + let t = self.domain().clamp(t); + self.sample_with_two_derivatives_unchecked(t) + } +} + +/// A wrapper that uses a [`SampleDerivative`] curve to produce a `Curve>`. +#[derive(Copy, Clone, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect, FromReflect), + reflect(from_reflect = false) +)] +pub struct SampleDerivativeWrapper(C); + +impl Curve> for SampleDerivativeWrapper +where + T: HasTangent, + C: SampleDerivative, +{ + fn domain(&self) -> Interval { + self.0.domain() + } + + fn sample_unchecked(&self, t: f32) -> WithDerivative { + self.0.sample_with_derivative_unchecked(t) + } + + fn sample(&self, t: f32) -> Option> { + self.0.sample_with_derivative(t) + } + + fn sample_clamped(&self, t: f32) -> WithDerivative { + self.0.sample_with_derivative_clamped(t) + } +} + +/// A wrapper that uses a [`SampleTwoDerivatives`] curve to produce a +/// `Curve>`. +#[derive(Copy, Clone, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect, FromReflect), + reflect(from_reflect = false) +)] +pub struct SampleTwoDerivativesWrapper(C); + +impl Curve> for SampleTwoDerivativesWrapper +where + T: HasTangent, + C: SampleTwoDerivatives, +{ + fn domain(&self) -> Interval { + self.0.domain() + } + + fn sample_unchecked(&self, t: f32) -> WithTwoDerivatives { + self.0.sample_with_two_derivatives_unchecked(t) + } + + fn sample(&self, t: f32) -> Option> { + self.0.sample_with_two_derivatives(t) + } + + fn sample_clamped(&self, t: f32) -> WithTwoDerivatives { + self.0.sample_with_two_derivatives_clamped(t) + } +} + +impl CurveWithDerivative for C +where + T: HasTangent, + C: SampleDerivative, +{ + fn with_derivative(self) -> impl Curve> { + SampleDerivativeWrapper(self) + } +} + +impl CurveWithTwoDerivatives for C +where + T: HasTangent, + C: SampleTwoDerivatives + CurveWithDerivative, +{ + fn with_two_derivatives(self) -> impl Curve> { + SampleTwoDerivativesWrapper(self) + } +} diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 6f8ce6c301..fa8636338f 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -287,6 +287,7 @@ pub mod adaptors; pub mod cores; +pub mod derivatives; pub mod easing; pub mod interval; pub mod iterable; diff --git a/typos.toml b/typos.toml index 332f68bead..6c97c5d0fc 100644 --- a/typos.toml +++ b/typos.toml @@ -16,17 +16,18 @@ TOI = "TOI" # Time of impact locale = "en-us" # Ignored typos regexes extend-ignore-identifiers-re = [ - "Ba", # Bitangent for Anisotropy - "ba", # Part of an accessor in WGSL - color.ba - "ser", # ron::ser - Serializer - "SME", # Subject Matter Expert - "Sur", # macOS Big Sur - South - "NDK", # NDK - Native Development Kit - "PNG", # PNG - Portable Network Graphics file format - "Masia", # The surname of one of the authors of SMAA - "metalness", # Rendering term (metallicity) - "inventario", # Inventory in Portuguese - "[Rr]eparametrize", # Mathematical term in curve context (reparameterize) + "Ba", # Bitangent for Anisotropy + "ba", # Part of an accessor in WGSL - color.ba + "ser", # ron::ser - Serializer + "SME", # Subject Matter Expert + "Sur", # macOS Big Sur - South + "NDK", # NDK - Native Development Kit + "PNG", # PNG - Portable Network Graphics file format + "Masia", # The surname of one of the authors of SMAA + "metalness", # Rendering term (metallicity) + "inventario", # Inventory in Portuguese + "[Rr]eparametrize", # Mathematical term in curve context (reparameterize) + "[Rr]eparametrization", # Used in bevy_mikktspace "iFO", "vOt",