Skip to content

Commit c60dcea

Browse files
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<Vec2>`, 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<Vec2>` 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<T>` type that uses `HasTangent` to include derivative information: ```rust pub struct WithDerivative<T> 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<T> 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: <T::Tangent as HasTangent>::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<T>` which allows the construction of a `Curve<WithDerivative<T>>` 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<T> where T: HasTangent, { /// This curve, but with its first derivative included in sampling. fn with_derivative(self) -> impl Curve<WithDerivative<T>>; } ``` 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<WithDerivative<T>>` 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<dyn Curve<WithDerivative<T>>>`. 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<T>: Curve<T> where T: HasTangent, { fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<T>; // ... other sampling variants as default methods } ``` The point is that the output of `with_derivative` is a `Curve<WithDerivative<T>>` 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<T>` is actually a "sealed" extension trait of `SampleDerivative<T>`. ## 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<f32>` 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 <[email protected]>
1 parent 711246a commit c60dcea

File tree

8 files changed

+1242
-95
lines changed

8 files changed

+1242
-95
lines changed

crates/bevy_math/src/common_traits.rs

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ pub trait VectorSpace:
3030
+ Div<f32, Output = Self>
3131
+ Add<Self, Output = Self>
3232
+ Sub<Self, Output = Self>
33-
+ Neg
33+
+ Neg<Output = Self>
3434
+ Default
3535
+ Debug
3636
+ Clone
@@ -71,6 +71,89 @@ impl VectorSpace for f32 {
7171
const ZERO: Self = 0.0;
7272
}
7373

74+
/// A type consisting of formal sums of elements from `V` and `W`. That is,
75+
/// each value `Sum(v, w)` is thought of as `v + w`, with no available
76+
/// simplification. In particular, if `V` and `W` are [vector spaces], then
77+
/// `Sum<V, W>` is a vector space whose dimension is the sum of those of `V`
78+
/// and `W`, and the field accessors `.0` and `.1` are vector space projections.
79+
///
80+
/// [vector spaces]: VectorSpace
81+
#[derive(Debug, Clone, Copy)]
82+
pub struct Sum<V, W>(pub V, pub W);
83+
84+
impl<V, W> Mul<f32> for Sum<V, W>
85+
where
86+
V: VectorSpace,
87+
W: VectorSpace,
88+
{
89+
type Output = Self;
90+
fn mul(self, rhs: f32) -> Self::Output {
91+
Sum(self.0 * rhs, self.1 * rhs)
92+
}
93+
}
94+
95+
impl<V, W> Div<f32> for Sum<V, W>
96+
where
97+
V: VectorSpace,
98+
W: VectorSpace,
99+
{
100+
type Output = Self;
101+
fn div(self, rhs: f32) -> Self::Output {
102+
Sum(self.0 / rhs, self.1 / rhs)
103+
}
104+
}
105+
106+
impl<V, W> Add<Self> for Sum<V, W>
107+
where
108+
V: VectorSpace,
109+
W: VectorSpace,
110+
{
111+
type Output = Self;
112+
fn add(self, other: Self) -> Self::Output {
113+
Sum(self.0 + other.0, self.1 + other.1)
114+
}
115+
}
116+
117+
impl<V, W> Sub<Self> for Sum<V, W>
118+
where
119+
V: VectorSpace,
120+
W: VectorSpace,
121+
{
122+
type Output = Self;
123+
fn sub(self, other: Self) -> Self::Output {
124+
Sum(self.0 - other.0, self.1 - other.1)
125+
}
126+
}
127+
128+
impl<V, W> Neg for Sum<V, W>
129+
where
130+
V: VectorSpace,
131+
W: VectorSpace,
132+
{
133+
type Output = Self;
134+
fn neg(self) -> Self::Output {
135+
Sum(-self.0, -self.1)
136+
}
137+
}
138+
139+
impl<V, W> Default for Sum<V, W>
140+
where
141+
V: VectorSpace,
142+
W: VectorSpace,
143+
{
144+
fn default() -> Self {
145+
Sum(V::default(), W::default())
146+
}
147+
}
148+
149+
impl<V, W> VectorSpace for Sum<V, W>
150+
where
151+
V: VectorSpace,
152+
W: VectorSpace,
153+
{
154+
const ZERO: Self = Sum(V::ZERO, W::ZERO);
155+
}
156+
74157
/// A type that supports the operations of a normed vector space; i.e. a norm operation in addition
75158
/// to those of [`VectorSpace`]. Specifically, the implementor must guarantee that the following
76159
/// relationships hold, within the limitations of floating point arithmetic:
@@ -410,3 +493,48 @@ impl_stable_interpolate_tuple!(
410493
(T9, 9),
411494
(T10, 10)
412495
);
496+
497+
/// A type that has tangents.
498+
pub trait HasTangent {
499+
/// The tangent type.
500+
type Tangent: VectorSpace;
501+
}
502+
503+
/// A value with its derivative.
504+
pub struct WithDerivative<T>
505+
where
506+
T: HasTangent,
507+
{
508+
/// The underlying value.
509+
pub value: T,
510+
511+
/// The derivative at `value`.
512+
pub derivative: T::Tangent,
513+
}
514+
515+
/// A value together with its first and second derivatives.
516+
pub struct WithTwoDerivatives<T>
517+
where
518+
T: HasTangent,
519+
{
520+
/// The underlying value.
521+
pub value: T,
522+
523+
/// The derivative at `value`.
524+
pub derivative: T::Tangent,
525+
526+
/// The second derivative at `value`.
527+
pub second_derivative: <T::Tangent as HasTangent>::Tangent,
528+
}
529+
530+
impl<V: VectorSpace> HasTangent for V {
531+
type Tangent = V;
532+
}
533+
534+
impl<M, N> HasTangent for (M, N)
535+
where
536+
M: HasTangent,
537+
N: HasTangent,
538+
{
539+
type Tangent = Sum<M::Tangent, N::Tangent>;
540+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
use super::{CubicSegment, RationalSegment};
2+
use crate::common_traits::{VectorSpace, WithDerivative, WithTwoDerivatives};
3+
use crate::curve::{
4+
derivatives::{SampleDerivative, SampleTwoDerivatives},
5+
Curve, Interval,
6+
};
7+
8+
#[cfg(feature = "alloc")]
9+
use super::{CubicCurve, RationalCurve};
10+
11+
// -- CubicSegment
12+
13+
impl<P: VectorSpace> Curve<P> for CubicSegment<P> {
14+
#[inline]
15+
fn domain(&self) -> Interval {
16+
Interval::UNIT
17+
}
18+
19+
#[inline]
20+
fn sample_unchecked(&self, t: f32) -> P {
21+
self.position(t)
22+
}
23+
}
24+
25+
impl<P: VectorSpace> SampleDerivative<P> for CubicSegment<P> {
26+
#[inline]
27+
fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<P> {
28+
WithDerivative {
29+
value: self.position(t),
30+
derivative: self.velocity(t),
31+
}
32+
}
33+
}
34+
35+
impl<P: VectorSpace> SampleTwoDerivatives<P> for CubicSegment<P> {
36+
#[inline]
37+
fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<P> {
38+
WithTwoDerivatives {
39+
value: self.position(t),
40+
derivative: self.velocity(t),
41+
second_derivative: self.acceleration(t),
42+
}
43+
}
44+
}
45+
46+
// -- CubicCurve
47+
48+
#[cfg(feature = "alloc")]
49+
impl<P: VectorSpace> Curve<P> for CubicCurve<P> {
50+
#[inline]
51+
fn domain(&self) -> Interval {
52+
// The non-emptiness invariant guarantees that this succeeds.
53+
Interval::new(0.0, self.segments.len() as f32)
54+
.expect("CubicCurve is invalid because it has no segments")
55+
}
56+
57+
#[inline]
58+
fn sample_unchecked(&self, t: f32) -> P {
59+
self.position(t)
60+
}
61+
}
62+
63+
#[cfg(feature = "alloc")]
64+
impl<P: VectorSpace> SampleDerivative<P> for CubicCurve<P> {
65+
#[inline]
66+
fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<P> {
67+
WithDerivative {
68+
value: self.position(t),
69+
derivative: self.velocity(t),
70+
}
71+
}
72+
}
73+
74+
#[cfg(feature = "alloc")]
75+
impl<P: VectorSpace> SampleTwoDerivatives<P> for CubicCurve<P> {
76+
#[inline]
77+
fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<P> {
78+
WithTwoDerivatives {
79+
value: self.position(t),
80+
derivative: self.velocity(t),
81+
second_derivative: self.acceleration(t),
82+
}
83+
}
84+
}
85+
86+
// -- RationalSegment
87+
88+
impl<P: VectorSpace> Curve<P> for RationalSegment<P> {
89+
#[inline]
90+
fn domain(&self) -> Interval {
91+
Interval::UNIT
92+
}
93+
94+
#[inline]
95+
fn sample_unchecked(&self, t: f32) -> P {
96+
self.position(t)
97+
}
98+
}
99+
100+
impl<P: VectorSpace> SampleDerivative<P> for RationalSegment<P> {
101+
#[inline]
102+
fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<P> {
103+
WithDerivative {
104+
value: self.position(t),
105+
derivative: self.velocity(t),
106+
}
107+
}
108+
}
109+
110+
impl<P: VectorSpace> SampleTwoDerivatives<P> for RationalSegment<P> {
111+
#[inline]
112+
fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<P> {
113+
WithTwoDerivatives {
114+
value: self.position(t),
115+
derivative: self.velocity(t),
116+
second_derivative: self.acceleration(t),
117+
}
118+
}
119+
}
120+
121+
// -- RationalCurve
122+
123+
#[cfg(feature = "alloc")]
124+
impl<P: VectorSpace> Curve<P> for RationalCurve<P> {
125+
#[inline]
126+
fn domain(&self) -> Interval {
127+
// The non-emptiness invariant guarantees the success of this.
128+
Interval::new(0.0, self.length())
129+
.expect("RationalCurve is invalid because it has zero length")
130+
}
131+
132+
#[inline]
133+
fn sample_unchecked(&self, t: f32) -> P {
134+
self.position(t)
135+
}
136+
}
137+
138+
#[cfg(feature = "alloc")]
139+
impl<P: VectorSpace> SampleDerivative<P> for RationalCurve<P> {
140+
#[inline]
141+
fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<P> {
142+
WithDerivative {
143+
value: self.position(t),
144+
derivative: self.velocity(t),
145+
}
146+
}
147+
}
148+
149+
#[cfg(feature = "alloc")]
150+
impl<P: VectorSpace> SampleTwoDerivatives<P> for RationalCurve<P> {
151+
#[inline]
152+
fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<P> {
153+
WithTwoDerivatives {
154+
value: self.position(t),
155+
derivative: self.velocity(t),
156+
second_derivative: self.acceleration(t),
157+
}
158+
}
159+
}

0 commit comments

Comments
 (0)