bevy
bevy copied to clipboard
`Curve` implementation for cubic curves
This is an in-progress prototype implementation of Curve
for the geometric curves created in bevy_math
. Everything here is subject to heavy renaming; I just wanted to get something functional up and running. Notably, this design presently excludes NURBS curves, which is a whole new bag of worms.
Objective
Add support for returning data in the form of a Curve
(in the sense of the curve-trait RFC) to the bevy_math
cubic curves.
The output data of these curves is somewhat stratified. For example, the output of the CubicBezier
builder has very few guarantees on its output compared to CubicBSpline
; in the first case, it doesn't make too much sense to ask about derivatives globally, in general (since there is no reason for them to match up between segments), whereas in the latter, one always has globally valid first and second derivatives. The aim is for the API to reflect this in some way without getting in the user's way excessively.
Solution
Technical changes
This particular prototype represents, essentially, the most "overengineered" part of the solution space, since it provides explicit type-level safeguards that prevent users from, for instance, accessing a Curve
holding both the position and velocity for curve constructions which are not globally C1 (although this limitation can be bypassed). It consists of a number of moving parts to accomplish this:
- Firstly,
CubicCurve
now has a second parameter which serves as a marker. This is limited to a set of marker types which satisfy a marker trait,Smoothness
, and the set of all of them consists ofNoGuarantees
,C0
,C1
, andC2
. - Secondly, there are a few new data types which play the role of
T
inCurve<T>
; these combine position, velocity, and acceleration data into convenient packages to be used together; e.g.:pub struct C0Data<P: VectorSpace> { position: P, } pub struct C1Data<P: VectorSpace> { position: P, velocity: P, } pub struct C2Data<P: VectorSpace> { position: P, velocity: P, acceleration: P, }
- Next, each of these has a corresponding trait which dictates whether a curve with that output can be created from the given data; e.g.:
/// A trait for a type that can be turned into a curve which is continuous and also has /// continuous derivatives. pub trait ToC1Curve<P: VectorSpace> { /// The type of the curve. type CurveType: Curve<C1Data<P>>; /// Yield a C1 curve. fn to_curve_c1(&self) -> Self::CurveType; }
- These traits are implemented using wrapper structs; the point is that, for example,
ToC1Curve
is implemented by anyCubicCurve
with a marker value at least C1 (so,C1
andC2
in that case):/// A wrapper struct which actually implements the [`Curve`] trait associated to its level. /// Here, the `Smoothness` parameter is actually precise; a `CubicCurveWrapper` with a /// `Smoothness` of `C2` does not access the `C0` data, for example. pub struct CubicCurveWrapper<L, P> where L: Smoothness, P: VectorSpace, { inner: CubicCurve<L, P>, } // ... impl<L, P> ToC1Curve<P> for CubicCurve<L, P> where L: AtLeastC1, P: VectorSpace, { type CurveType = CubicCurveWrapper<C1, P>; fn to_curve_c1(&self) -> Self::CurveType { CubicCurveWrapper { inner: self.clone().transmute_smoothness(), } } }
- Furthermore, the
Smoothness
parameter can be raised explicitly by usingbless
functions which otherwise have no effect. This allows, for example,C1
data to be accessed on a curve whose continuity of derivatives cannot be guaranteed at the type level (either for hacky reasons or because the type jsut fails to capture enough knowledge); these have signatures that end up looking like this (although they are actually implemented using traits):/// Grants access to C1-level curve data that cannot be guaranteed to be valid by the /// invariants from this curve's construction. fn bless_c1(self) -> CubicCurve<C1, P> { //... }
Usage
What this actually ends up looking like for users is this; let's suppose, for example, that we want to extract a Curve
from a cubic B-spline:
// ... definition of `control_points` ...
// Use the B-spline construction to get a `CubicCurve<C2, P>`
let my_curve = CubicBSpline::new(control_points).to_curve();
// Now we can get something which is actually a `Curve<C1Data<P>>` by using one of the trait methods:
let position_and_velocity_curve = my_curve.to_curve_c1();
On the other hand, suppose that we have something like a Hermite interpolation:
// ... definitions of `control_points`, `tangents`, ...
// Use the data to get a curve from a Hermite spline (`CubicCurve<C1, P>`):
let my_curve = CubicHermite::new(control_points, tangents).to_curve();
// It's only C1, but I want the acceleration anyway!!! HAHAHA!!!
// This is a `Curve<C2Data<P>>`; the resulting acceleration might be discontinuous.
let c2_data_curve = my_curve.bless_c2().to_curve_c2();
(As you can see, the terminology is presently quite suboptimal.)
Changelog
Migration Guide
Any external code explicitly using the CubicCurve
type (e.g. as part of a Component) will need to be demarcated with a marker type depending on how it is used.
Discussion
As I wrote above, this feels slightly overengineered to me, and the places where that actually comes through negatively, in my opinion, are:
- When users have to actually interact with the marker types it's weird
- The
C0Data
,C1Data
, etc. bundling doesn't feel necessarily natural from a user perspective (especially with names liketo_curve_c1
, but perhaps that can be remedied somehow).
Another direction I could see going with this is just not bundling the position/velocity/acceleration data together, and instead just having separate methods splitting out Curve<P>
s for the position, velocity, and acceleration separately. This has the advantage that it requires potentially less interfacing with technical jargon, but users would often need to zip the data together to use it in other processes (e.g. basically anything that requires the velocity would probably require position as well). Of course, this could still be gated behind the "smoothness" bounds in essentially the same way.