c60dcea231
7 Commits
Author | SHA1 | Message | Date | |
---|---|---|---|---|
![]() |
c60dcea231
|
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 <alice.i.cecile@gmail.com> |
||
![]() |
a8b9c945c7
|
Add no_std Support to bevy_math (#15810)
# Objective - Contributes to #15460 ## Solution - Added two new features, `std` (default) and `alloc`, gating `std` and `alloc` behind them respectively. - Added missing `f32` functions to `std_ops` as required. These `f32` methods have been added to the `clippy.toml` deny list to aid in `no_std` development. ## Testing - CI - `cargo clippy -p bevy_math --no-default-features --features libm --target "x86_64-unknown-none"` - `cargo test -p bevy_math --no-default-features --features libm` - `cargo test -p bevy_math --no-default-features --features "libm, alloc"` - `cargo test -p bevy_math --no-default-features --features "libm, alloc, std"` - `cargo test -p bevy_math --no-default-features --features "std"` ## Notes The following items require the `alloc` feature to be enabled: - `CubicBSpline` - `CubicBezier` - `CubicCardinalSpline` - `CubicCurve` - `CubicGenerator` - `CubicHermite` - `CubicNurbs` - `CyclicCubicGenerator` - `RationalCurve` - `RationalGenerator` - `BoxedPolygon` - `BoxedPolyline2d` - `BoxedPolyline3d` - `SampleCurve` - `SampleAutoCurve` - `UnevenSampleCurve` - `UnevenSampleAutoCurve` - `EvenCore` - `UnevenCore` - `ChunkedUnevenCore` This requirement could be relaxed in certain cases, but I had erred on the side of gating rather than modifying. Since `no_std` is a new set of platforms we are adding support to, and the `alloc` feature is enabled by default, this is not a breaking change. --------- Co-authored-by: Benjamin Brienen <benjamin.brienen@outlook.com> Co-authored-by: Matty <2975848+mweatherley@users.noreply.github.com> Co-authored-by: Joona Aalto <jondolf.dev@gmail.com> |
||
![]() |
a44b668b90
|
Bump crate-ci/typos from 1.26.8 to 1.27.0 (#16236)
# Objective - Closes #16224 ## Solution - Bumps `crate-ci/typos@v1.26.8` to `crate-ci/typos@v1.27.0`. ## Testing - CI checks should pass. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
![]() |
6521e759ea
|
Improve PhantomData held by curve adaptors (#15881)
# Objective The previous `PhantomData` instances were written somewhat lazily, so they were just things like `PhantomData<T>` for curves with an output type of `T`. This looks innocuous, but it unnecessarily constrains `Send/Sync` inference based on `T`. See [here](https://doc.rust-lang.org/nomicon/phantom-data.html#table-of-phantomdata-patterns). ## Solution Switch to `PhantomData` of the form `PhantomData<fn() -> T>` for most of these adaptors. Since they only have a functional relationship to `T` (i.e. it shows up in the return type of trait methods), this is more accurate. ## Testing Tested by compiling Bevy. Co-authored-by: François Mockers <mockersf@gmail.com> |
||
![]() |
9b863be2fb
|
Curves: FromReflect boogaloo part 2 (#15714)
# Objective Allow curve adaptors to be reliably `Reflect` even if the curves they hold are not `FromReflect`. This allows them, for example, to be used in `bevy_animation`. I previously addressed this with the functional adaptors, but I forgot to address this in the case of fields that hold other curves and not arbitrary functions. ## Solution Do the following on every curve adaptor that holds another curve: ```rust // old: #[derive(Reflect)] ``` ```rust // new: #[derive(Reflect, FromReflect)] #[reflect(from_reflect = false)] ``` This looks inane, but it's necessary because the default `#[derive(Reflect)]` macro places `FromReflect` bounds on everything. To avoid this, we opt out of deriving `FromReflect` with that macro by adding `#[reflect(from_reflect = false)]`, then separately derive `FromReflect`. (Of course, the latter still has the `FromReflect` bounds, which is fine.) |
||
![]() |
429987ebf8
|
Curve-based animation (#15434)
# Objective This PR extends and reworks the material from #15282 by allowing arbitrary curves to be used by the animation system to animate arbitrary properties. The goals of this work are to: - Allow far greater flexibility in how animations are allowed to be defined in order to be used with `bevy_animation`. - Delegate responsibility over keyframe interpolation to `bevy_math` and the `Curve` libraries and reduce reliance on keyframes in animation definitions generally. - Move away from allowing the glTF spec to completely define animations on a mechanical level. ## Solution ### Overview At a high level, curves have been incorporated into the animation system using the `AnimationCurve` trait (closely related to what was `Keyframes`). From the top down: 1. In `animate_targets`, animations are driven by `VariableCurve`, which is now a thin wrapper around a `Box<dyn AnimationCurve>`. 2. `AnimationCurve` is something built out of a `Curve`, and it tells the animation system how to use the curve's output to actually mutate component properties. The trait looks like this: ```rust /// A low-level trait that provides control over how curves are actually applied to entities /// by the animation system. /// /// Typically, this will not need to be implemented manually, since it is automatically /// implemented by [`AnimatableCurve`] and other curves used by the animation system /// (e.g. those that animate parts of transforms or morph weights). However, this can be /// implemented manually when `AnimatableCurve` is not sufficiently expressive. /// /// In many respects, this behaves like a type-erased form of [`Curve`], where the output /// type of the curve is remembered only in the components that are mutated in the /// implementation of [`apply`]. /// /// [`apply`]: AnimationCurve::apply pub trait AnimationCurve: Reflect + Debug + Send + Sync { /// Returns a boxed clone of this value. fn clone_value(&self) -> Box<dyn AnimationCurve>; /// The range of times for which this animation is defined. fn domain(&self) -> Interval; /// Write the value of sampling this curve at time `t` into `transform` or `entity`, /// as appropriate, interpolating between the existing value and the sampled value /// using the given `weight`. fn apply<'a>( &self, t: f32, transform: Option<Mut<'a, Transform>>, entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle<AnimationGraph>)>, weight: f32, ) -> Result<(), AnimationEvaluationError>; } ``` 3. The conversion process from a `Curve` to an `AnimationCurve` involves using wrappers which communicate the intent to animate a particular property. For example, here is `TranslationCurve`, which wraps a `Curve<Vec3>` and uses it to animate `Transform::translation`: ```rust /// This type allows a curve valued in `Vec3` to become an [`AnimationCurve`] that animates /// the translation component of a transform. pub struct TranslationCurve<C>(pub C); ``` ### Animatable Properties The `AnimatableProperty` trait survives in the transition, and it can be used to allow curves to animate arbitrary component properties. The updated documentation for `AnimatableProperty` explains this process: <details> <summary>Expand AnimatableProperty example</summary An `AnimatableProperty` is a value on a component that Bevy can animate. You can implement this trait on a unit struct in order to support animating custom components other than transforms and morph weights. Use that type in conjunction with `AnimatableCurve` (and perhaps `AnimatableKeyframeCurve` to define the animation itself). For example, in order to animate font size of a text section from 24 pt. to 80 pt., you might use: ```rust #[derive(Reflect)] struct FontSizeProperty; impl AnimatableProperty for FontSizeProperty { type Component = Text; type Property = f32; fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property> { Some(&mut component.sections.get_mut(0)?.style.font_size) } } ``` You can then create an `AnimationClip` to animate this property like so: ```rust let mut animation_clip = AnimationClip::default(); animation_clip.add_curve_to_target( animation_target_id, AnimatableKeyframeCurve::new( [ (0.0, 24.0), (1.0, 80.0), ] ) .map(AnimatableCurve::<FontSizeProperty, _>::from_curve) .expect("Failed to create font size curve") ); ``` Here, the use of `AnimatableKeyframeCurve` creates a curve out of the given keyframe time-value pairs, using the `Animatable` implementation of `f32` to interpolate between them. The invocation of `AnimatableCurve::from_curve` with `FontSizeProperty` indicates that the `f32` output from that curve is to be used to animate the font size of a `Text` component (as configured above). </details> ### glTF Loading glTF animations are now loaded into `Curve` types of various kinds, depending on what is being animated and what interpolation mode is being used. Those types get wrapped into and converted into `Box<dyn AnimationCurve>` and shoved inside of a `VariableCurve` just like everybody else. ### Morph Weights There is an `IterableCurve` abstraction which allows sampling these from a contiguous buffer without allocating. Its only reason for existing is that Rust disallows you from naming function types, otherwise we would just use `Curve` with an iterator output type. (The iterator involves `Map`, and the name of the function type would have to be able to be named, but it is not.) A `WeightsCurve` adaptor turns an `IterableCurve` into an `AnimationCurve`, so it behaves like everything else in that regard. ## Testing Tested by running existing animation examples. Interpolation logic has had additional tests added within the `Curve` API to replace the tests in `bevy_animation`. Some kinds of out-of-bounds errors have become impossible. Performance testing on `many_foxes` (`animate_targets`) suggests that performance is very similar to the existing implementation. Here are a couple trace histograms across different runs (yellow is this branch, red is main). <img width="669" alt="Screenshot 2024-09-27 at 9 41 50 AM" src="https://github.com/user-attachments/assets/5ba4e9ac-3aea-452e-aaf8-1492acc2d7fc"> <img width="673" alt="Screenshot 2024-09-27 at 9 45 18 AM" src="https://github.com/user-attachments/assets/8982538b-04cf-46b5-97b2-164c6bc8162e"> --- ## Migration Guide Most user code that does not directly deal with `AnimationClip` and `VariableCurve` will not need to be changed. On the other hand, `VariableCurve` has been completely overhauled. If you were previously defining animation curves in code using keyframes, you will need to migrate that code to use curve constructors instead. For example, a rotation animation defined using keyframes and added to an animation clip like this: ```rust animation_clip.add_curve_to_target( animation_target_id, VariableCurve { keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0, 4.0], keyframes: Keyframes::Rotation(vec![ Quat::IDENTITY, Quat::from_axis_angle(Vec3::Y, PI / 2.), Quat::from_axis_angle(Vec3::Y, PI / 2. * 2.), Quat::from_axis_angle(Vec3::Y, PI / 2. * 3.), Quat::IDENTITY, ]), interpolation: Interpolation::Linear, }, ); ``` would now be added like this: ```rust animation_clip.add_curve_to_target( animation_target_id, AnimatableKeyframeCurve::new([0.0, 1.0, 2.0, 3.0, 4.0].into_iter().zip([ Quat::IDENTITY, Quat::from_axis_angle(Vec3::Y, PI / 2.), Quat::from_axis_angle(Vec3::Y, PI / 2. * 2.), Quat::from_axis_angle(Vec3::Y, PI / 2. * 3.), Quat::IDENTITY, ])) .map(RotationCurve) .expect("Failed to build rotation curve"), ); ``` Note that the interface of `AnimationClip::add_curve_to_target` has also changed (as this example shows, if subtly), and now takes its curve input as an `impl AnimationCurve`. If you need to add a `VariableCurve` directly, a new method `add_variable_curve_to_target` accommodates that (and serves as a one-to-one migration in this regard). ### For reviewers The diff is pretty big, and the structure of some of the changes might not be super-obvious: - `keyframes.rs` became `animation_curves.rs`, and `AnimationCurve` is based heavily on `Keyframes`, with the adaptors also largely following suite. - The Curve API adaptor structs were moved from `bevy_math::curve::mod` into their own module `adaptors`. There are no functional changes to how these adaptors work; this is just to make room for the specialized reflection implementations since `mod.rs` was getting kind of cramped. - The new module `gltf_curves` holds the additional curve constructions that are needed by the glTF loader. Note that the loader uses a mix of these and off-the-shelf `bevy_math` curve stuff. - `animatable.rs` no longer holds logic related to keyframe interpolation, which is now delegated to the existing abstractions in `bevy_math::curve::cores`. --------- Co-authored-by: Gino Valente <49806985+MrGVSV@users.noreply.github.com> Co-authored-by: aecsocket <43144841+aecsocket@users.noreply.github.com> |
||
![]() |
ff308488fe
|
add more Curve adaptors (#14794)
# Objective This implements another item on the way to complete the `Curves` implementation initiative Citing @mweatherley > Curve adaptors for making a curve repeat or ping-pong would be useful. This adds three widely applicable adaptors: - `ReverseCurve` "plays" the curve backwards - `RepeatCurve` to repeat the curve for `n` times where `n` in `[0,inf)` - `ForeverCurve` which extends the curves domain to `EVERYWHERE` - `PingPongCurve` (name wip (?)) to chain the curve with it's reverse. This would be achievable with `ReverseCurve` and `ChainCurve`, but it would require the use of `by_ref` which can be restrictive in some scenarios where you'd rather just consume the curve. Users can still create the same effect by combination of the former two, but since this will be most likely a very typical adaptor we should also provide it on the library level. (Why it's typical: you can create a single period of common waves with it pretty easily, think square wave (= pingpong + step), triangle wave ( = pingpong + linear), etc.) - `ContinuationCurve` which chains two curves but also makes sure that the samples of the second curve are translated so that `sample(first.end) == sample(second.start)` ## Solution Implement the adaptors above. (More suggestions are welcome!) ## Testing - [x] add simple tests. One per adaptor --------- Co-authored-by: eckz <567737+eckz@users.noreply.github.com> Co-authored-by: Matty <2975848+mweatherley@users.noreply.github.com> Co-authored-by: IQuick 143 <IQuick143cz@gmail.com> Co-authored-by: Matty <weatherleymatthew@gmail.com> Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> |