
# Objective Certain classes of games, usually those with enormous worlds, require some amount of support for double-precision. Libraries like `big_space` exist to allow for large worlds while integrating cleanly with Bevy's primarily single-precision ecosystem, but even then, games will often still work directly in double-precision throughout the part of the pipeline that feeds into the Bevy interface. Currently, working with double-precision types in Bevy is a pain. `glam` provides types like `DVec3`, but Bevy doesn't provide double-precision analogs for `glam` wrappers like `Dir3`. This is mostly because doing so involves one of: - code duplication - generics - templates (like `glam` uses) - macros Each of these has issues that are enough to be deal-breakers as far as maintainability, usability or readability. To work around this, I'm putting together `bevy_dmath`, a crate that duplicates `bevy_math` types and functionality to allow downstream users to enjoy the ergonomics and power of `bevy_math` in double-precision. For the most part, it's a smooth process, but in order to fully integrate, there are some necessary changes that can only be made in `bevy_math`. ## Solution This PR addresses the first and easiest issue with downstream double-precision math support: `VectorSpace` currently can only represent vector spaces over `f32`. This automatically closes the door to double-precision curves, among other things. This restriction can be easily lifted by allowing vector spaces to specify the underlying scalar field. This PR adds a new trait `ScalarField` that satisfies the properties of a scalar field (the ones that can be upheld statically) and adds a new associated type `type Scalar: ScalarField` to `VectorSpace`. It's mostly an unintrusive change. The biggest annoyances are: - it touches a lot of curve code - `bevy_math::ops` doesn't support `f64`, so there are some annoying workarounds As far as curves code, I wanted to make this change unintrusive and bite-sized, so I'm trying to touch as little code as possible. To prove to myself it can be done, I went ahead and (*not* in this PR) migrated most of the curves API to support different `ScalarField`s and it went really smoothly! The ugliest thing was adding `P::Scalar: From<usize>` in several places. There's an argument to be made here that we should be using `num-traits`, but that's not immediately relevant. The point is that for now, the smallest change I could make was to go into every curve impl and make them generic over `VectorSpace<Scalar = f32>`. Curves work exactly like before and don't change the user API at all. # Follow-up - **Extend `bevy_math::ops` to work with `f64`.** `bevy_math::ops` is used all over, and if curves are ever going to support different `ScalarField` types, we'll need to be able to use the correct `std` or `libm` ops for `f64` types as well. Adding an `ops64` mod turned out to be really ugly, but I'll point out the maintenance burden is low because we're not going to be adding new floating-point ops anytime soon. Another solution is to build a floating-point trait that calls the right op variant and impl it for `f32` and `f64`. This reduces maintenance burden because on the off chance we ever *do* want to go modify it, it's all tied together: you can't change the interface on one without changing the trait, which forces you to update the other. A third option is to use `num-traits`, which is basically option 2 but someone else did the work for us. They already support `no_std` using `libm`, so it would be more or less a drop-in replacement. They're missing a couple floating-point ops like `floor` and `ceil`, but we could make our own floating-point traits for those (there's even the potential for upstreaming them into `num-traits`). - **Tweak curves to accept vector spaces over any `ScalarField`.** Curves are ready to support custom scalar types as soon as the bullet above is addressed. I will admit that the code is not as fun to look at: `P::Scalar` instead of `f32` everywhere. We could consider an alternate design where we use `f32` even to interpolate something like a `DVec3`, but personally I think that's a worse solution than parameterizing curves over the vector space's scalar type. At the end of the day, it's not really bad to deal with in my opinion... `ScalarType` supports enough operations that working with them is almost like working with raw float types, and it unlocks a whole ecosystem for games that want to use double-precision.
436 lines
14 KiB
Rust
436 lines
14 KiB
Rust
//! Concrete curve structures used to load glTF curves into the animation system.
|
|
|
|
use bevy_math::{
|
|
curve::{cores::*, iterable::IterableCurve, *},
|
|
vec4, Quat, Vec4, VectorSpace,
|
|
};
|
|
use bevy_reflect::Reflect;
|
|
use either::Either;
|
|
use thiserror::Error;
|
|
|
|
/// A keyframe-defined curve that "interpolates" by stepping at `t = 1.0` to the next keyframe.
|
|
#[derive(Debug, Clone, Reflect)]
|
|
pub struct SteppedKeyframeCurve<T> {
|
|
core: UnevenCore<T>,
|
|
}
|
|
|
|
impl<T> Curve<T> for SteppedKeyframeCurve<T>
|
|
where
|
|
T: Clone,
|
|
{
|
|
#[inline]
|
|
fn domain(&self) -> Interval {
|
|
self.core.domain()
|
|
}
|
|
|
|
#[inline]
|
|
fn sample_clamped(&self, t: f32) -> T {
|
|
self.core
|
|
.sample_with(t, |x, y, t| if t >= 1.0 { y.clone() } else { x.clone() })
|
|
}
|
|
|
|
#[inline]
|
|
fn sample_unchecked(&self, t: f32) -> T {
|
|
self.sample_clamped(t)
|
|
}
|
|
}
|
|
|
|
impl<T> SteppedKeyframeCurve<T> {
|
|
/// Create a new [`SteppedKeyframeCurve`]. If the curve could not be constructed from the
|
|
/// given data, an error is returned.
|
|
#[inline]
|
|
pub fn new(timed_samples: impl IntoIterator<Item = (f32, T)>) -> Result<Self, UnevenCoreError> {
|
|
Ok(Self {
|
|
core: UnevenCore::new(timed_samples)?,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// A keyframe-defined curve that uses cubic spline interpolation, backed by a contiguous buffer.
|
|
#[derive(Debug, Clone, Reflect)]
|
|
pub struct CubicKeyframeCurve<T> {
|
|
// Note: the sample width here should be 3.
|
|
core: ChunkedUnevenCore<T>,
|
|
}
|
|
|
|
impl<V> Curve<V> for CubicKeyframeCurve<V>
|
|
where
|
|
V: VectorSpace<Scalar = f32>,
|
|
{
|
|
#[inline]
|
|
fn domain(&self) -> Interval {
|
|
self.core.domain()
|
|
}
|
|
|
|
#[inline]
|
|
fn sample_clamped(&self, t: f32) -> V {
|
|
match self.core.sample_interp_timed(t) {
|
|
// In all the cases where only one frame matters, defer to the position within it.
|
|
InterpolationDatum::Exact((_, v))
|
|
| InterpolationDatum::LeftTail((_, v))
|
|
| InterpolationDatum::RightTail((_, v)) => v[1],
|
|
|
|
InterpolationDatum::Between((t0, u), (t1, v), s) => {
|
|
cubic_spline_interpolation(u[1], u[2], v[0], v[1], s, t1 - t0)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn sample_unchecked(&self, t: f32) -> V {
|
|
self.sample_clamped(t)
|
|
}
|
|
}
|
|
|
|
impl<T> CubicKeyframeCurve<T> {
|
|
/// Create a new [`CubicKeyframeCurve`] from keyframe `times` and their associated `values`.
|
|
/// Because 3 values are needed to perform cubic interpolation, `values` must have triple the
|
|
/// length of `times` — each consecutive triple `a_k, v_k, b_k` associated to time `t_k`
|
|
/// consists of:
|
|
/// - The in-tangent `a_k` for the sample at time `t_k`
|
|
/// - The actual value `v_k` for the sample at time `t_k`
|
|
/// - The out-tangent `b_k` for the sample at time `t_k`
|
|
///
|
|
/// For example, for a curve built from two keyframes, the inputs would have the following form:
|
|
/// - `times`: `[t_0, t_1]`
|
|
/// - `values`: `[a_0, v_0, b_0, a_1, v_1, b_1]`
|
|
#[inline]
|
|
pub fn new(
|
|
times: impl IntoIterator<Item = f32>,
|
|
values: impl IntoIterator<Item = T>,
|
|
) -> Result<Self, ChunkedUnevenCoreError> {
|
|
Ok(Self {
|
|
core: ChunkedUnevenCore::new(times, values, 3)?,
|
|
})
|
|
}
|
|
}
|
|
|
|
// NOTE: We can probably delete `CubicRotationCurve` once we improve the `Reflect` implementations
|
|
// for the `Curve` API adaptors; this is basically a `CubicKeyframeCurve` composed with `map`.
|
|
|
|
/// A keyframe-defined curve that uses cubic spline interpolation, special-cased for quaternions
|
|
/// since it uses `Vec4` internally.
|
|
#[derive(Debug, Clone, Reflect)]
|
|
#[reflect(Clone)]
|
|
pub struct CubicRotationCurve {
|
|
// Note: The sample width here should be 3.
|
|
core: ChunkedUnevenCore<Vec4>,
|
|
}
|
|
|
|
impl Curve<Quat> for CubicRotationCurve {
|
|
#[inline]
|
|
fn domain(&self) -> Interval {
|
|
self.core.domain()
|
|
}
|
|
|
|
#[inline]
|
|
fn sample_clamped(&self, t: f32) -> Quat {
|
|
let vec = match self.core.sample_interp_timed(t) {
|
|
// In all the cases where only one frame matters, defer to the position within it.
|
|
InterpolationDatum::Exact((_, v))
|
|
| InterpolationDatum::LeftTail((_, v))
|
|
| InterpolationDatum::RightTail((_, v)) => v[1],
|
|
|
|
InterpolationDatum::Between((t0, u), (t1, v), s) => {
|
|
cubic_spline_interpolation(u[1], u[2], v[0], v[1], s, t1 - t0)
|
|
}
|
|
};
|
|
Quat::from_vec4(vec.normalize())
|
|
}
|
|
|
|
#[inline]
|
|
fn sample_unchecked(&self, t: f32) -> Quat {
|
|
self.sample_clamped(t)
|
|
}
|
|
}
|
|
|
|
impl CubicRotationCurve {
|
|
/// Create a new [`CubicRotationCurve`] from keyframe `times` and their associated `values`.
|
|
/// Because 3 values are needed to perform cubic interpolation, `values` must have triple the
|
|
/// length of `times` — each consecutive triple `a_k, v_k, b_k` associated to time `t_k`
|
|
/// consists of:
|
|
/// - The in-tangent `a_k` for the sample at time `t_k`
|
|
/// - The actual value `v_k` for the sample at time `t_k`
|
|
/// - The out-tangent `b_k` for the sample at time `t_k`
|
|
///
|
|
/// For example, for a curve built from two keyframes, the inputs would have the following form:
|
|
/// - `times`: `[t_0, t_1]`
|
|
/// - `values`: `[a_0, v_0, b_0, a_1, v_1, b_1]`
|
|
///
|
|
/// To sample quaternions from this curve, the resulting interpolated `Vec4` output is normalized
|
|
/// and interpreted as a quaternion.
|
|
pub fn new(
|
|
times: impl IntoIterator<Item = f32>,
|
|
values: impl IntoIterator<Item = Vec4>,
|
|
) -> Result<Self, ChunkedUnevenCoreError> {
|
|
Ok(Self {
|
|
core: ChunkedUnevenCore::new(times, values, 3)?,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// A keyframe-defined curve that uses linear interpolation over many samples at once, backed
|
|
/// by a contiguous buffer.
|
|
#[derive(Debug, Clone, Reflect)]
|
|
pub struct WideLinearKeyframeCurve<T> {
|
|
// Here the sample width is the number of things to simultaneously interpolate.
|
|
core: ChunkedUnevenCore<T>,
|
|
}
|
|
|
|
impl<T> IterableCurve<T> for WideLinearKeyframeCurve<T>
|
|
where
|
|
T: VectorSpace<Scalar = f32>,
|
|
{
|
|
#[inline]
|
|
fn domain(&self) -> Interval {
|
|
self.core.domain()
|
|
}
|
|
|
|
#[inline]
|
|
fn sample_iter_clamped(&self, t: f32) -> impl Iterator<Item = T> {
|
|
match self.core.sample_interp(t) {
|
|
InterpolationDatum::Exact(v)
|
|
| InterpolationDatum::LeftTail(v)
|
|
| InterpolationDatum::RightTail(v) => Either::Left(v.iter().copied()),
|
|
|
|
InterpolationDatum::Between(u, v, s) => {
|
|
let interpolated = u.iter().zip(v.iter()).map(move |(x, y)| x.lerp(*y, s));
|
|
Either::Right(interpolated)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn sample_iter_unchecked(&self, t: f32) -> impl Iterator<Item = T> {
|
|
self.sample_iter_clamped(t)
|
|
}
|
|
}
|
|
|
|
impl<T> WideLinearKeyframeCurve<T> {
|
|
/// Create a new [`WideLinearKeyframeCurve`]. An error will be returned if:
|
|
/// - `values` has length zero.
|
|
/// - `times` has less than `2` unique valid entries.
|
|
/// - The length of `values` is not divisible by that of `times` (once sorted, filtered,
|
|
/// and deduplicated).
|
|
#[inline]
|
|
pub fn new(
|
|
times: impl IntoIterator<Item = f32>,
|
|
values: impl IntoIterator<Item = T>,
|
|
) -> Result<Self, WideKeyframeCurveError> {
|
|
Ok(Self {
|
|
core: ChunkedUnevenCore::new_width_inferred(times, values)?,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// A keyframe-defined curve that uses stepped "interpolation" over many samples at once, backed
|
|
/// by a contiguous buffer.
|
|
#[derive(Debug, Clone, Reflect)]
|
|
pub struct WideSteppedKeyframeCurve<T> {
|
|
// Here the sample width is the number of things to simultaneously interpolate.
|
|
core: ChunkedUnevenCore<T>,
|
|
}
|
|
|
|
impl<T> IterableCurve<T> for WideSteppedKeyframeCurve<T>
|
|
where
|
|
T: Clone,
|
|
{
|
|
#[inline]
|
|
fn domain(&self) -> Interval {
|
|
self.core.domain()
|
|
}
|
|
|
|
#[inline]
|
|
fn sample_iter_clamped(&self, t: f32) -> impl Iterator<Item = T> {
|
|
match self.core.sample_interp(t) {
|
|
InterpolationDatum::Exact(v)
|
|
| InterpolationDatum::LeftTail(v)
|
|
| InterpolationDatum::RightTail(v) => Either::Left(v.iter().cloned()),
|
|
|
|
InterpolationDatum::Between(u, v, s) => {
|
|
let interpolated =
|
|
u.iter()
|
|
.zip(v.iter())
|
|
.map(move |(x, y)| if s >= 1.0 { y.clone() } else { x.clone() });
|
|
Either::Right(interpolated)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn sample_iter_unchecked(&self, t: f32) -> impl Iterator<Item = T> {
|
|
self.sample_iter_clamped(t)
|
|
}
|
|
}
|
|
|
|
impl<T> WideSteppedKeyframeCurve<T> {
|
|
/// Create a new [`WideSteppedKeyframeCurve`]. An error will be returned if:
|
|
/// - `values` has length zero.
|
|
/// - `times` has less than `2` unique valid entries.
|
|
/// - The length of `values` is not divisible by that of `times` (once sorted, filtered,
|
|
/// and deduplicated).
|
|
#[inline]
|
|
pub fn new(
|
|
times: impl IntoIterator<Item = f32>,
|
|
values: impl IntoIterator<Item = T>,
|
|
) -> Result<Self, WideKeyframeCurveError> {
|
|
Ok(Self {
|
|
core: ChunkedUnevenCore::new_width_inferred(times, values)?,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// A keyframe-defined curve that uses cubic interpolation over many samples at once, backed by a
|
|
/// contiguous buffer.
|
|
#[derive(Debug, Clone, Reflect)]
|
|
pub struct WideCubicKeyframeCurve<T> {
|
|
core: ChunkedUnevenCore<T>,
|
|
}
|
|
|
|
impl<T> IterableCurve<T> for WideCubicKeyframeCurve<T>
|
|
where
|
|
T: VectorSpace<Scalar = f32>,
|
|
{
|
|
#[inline]
|
|
fn domain(&self) -> Interval {
|
|
self.core.domain()
|
|
}
|
|
|
|
fn sample_iter_clamped(&self, t: f32) -> impl Iterator<Item = T> {
|
|
match self.core.sample_interp_timed(t) {
|
|
InterpolationDatum::Exact((_, v))
|
|
| InterpolationDatum::LeftTail((_, v))
|
|
| InterpolationDatum::RightTail((_, v)) => {
|
|
// Pick out the part of this that actually represents the position (instead of tangents),
|
|
// which is the middle third.
|
|
let width = self.core.width();
|
|
Either::Left(v[width..(width * 2)].iter().copied())
|
|
}
|
|
|
|
InterpolationDatum::Between((t0, u), (t1, v), s) => Either::Right(
|
|
cubic_spline_interpolate_slices(self.core.width() / 3, u, v, s, t1 - t0),
|
|
),
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn sample_iter_unchecked(&self, t: f32) -> impl Iterator<Item = T> {
|
|
self.sample_iter_clamped(t)
|
|
}
|
|
}
|
|
|
|
/// An error indicating that a multisampling keyframe curve could not be constructed.
|
|
#[derive(Debug, Error)]
|
|
#[error("unable to construct a curve using this data")]
|
|
pub enum WideKeyframeCurveError {
|
|
/// The number of given values was not divisible by a multiple of the number of keyframes.
|
|
#[error("number of values ({values_given}) is not divisible by {divisor}")]
|
|
LengthMismatch {
|
|
/// The number of values given.
|
|
values_given: usize,
|
|
/// The number that `values_given` was supposed to be divisible by.
|
|
divisor: usize,
|
|
},
|
|
/// An error was returned by the internal core constructor.
|
|
#[error(transparent)]
|
|
CoreError(#[from] ChunkedUnevenCoreError),
|
|
}
|
|
|
|
impl<T> WideCubicKeyframeCurve<T> {
|
|
/// Create a new [`WideCubicKeyframeCurve`].
|
|
///
|
|
/// An error will be returned if:
|
|
/// - `values` has length zero.
|
|
/// - `times` has less than `2` unique valid entries.
|
|
/// - The length of `values` is not divisible by three times that of `times` (once sorted,
|
|
/// filtered, and deduplicated).
|
|
#[inline]
|
|
pub fn new(
|
|
times: impl IntoIterator<Item = f32>,
|
|
values: impl IntoIterator<Item = T>,
|
|
) -> Result<Self, WideKeyframeCurveError> {
|
|
let times: Vec<f32> = times.into_iter().collect();
|
|
let values: Vec<T> = values.into_iter().collect();
|
|
let divisor = times.len() * 3;
|
|
|
|
if values.len() % divisor != 0 {
|
|
return Err(WideKeyframeCurveError::LengthMismatch {
|
|
values_given: values.len(),
|
|
divisor,
|
|
});
|
|
}
|
|
|
|
Ok(Self {
|
|
core: ChunkedUnevenCore::new_width_inferred(times, values)?,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// A curve specifying the [`MorphWeights`] for a mesh in animation. The variants are broken
|
|
/// down by interpolation mode (with the exception of `Constant`, which never interpolates).
|
|
///
|
|
/// This type is, itself, a `Curve<Vec<f32>>`; however, in order to avoid allocation, it is
|
|
/// recommended to use its implementation of the [`IterableCurve`] trait, which allows iterating
|
|
/// directly over information derived from the curve without allocating.
|
|
///
|
|
/// [`MorphWeights`]: bevy_mesh::morph::MorphWeights
|
|
#[derive(Debug, Clone, Reflect)]
|
|
#[reflect(Clone)]
|
|
pub enum WeightsCurve {
|
|
/// A curve which takes a constant value over its domain. Notably, this is how animations with
|
|
/// only a single keyframe are interpreted.
|
|
Constant(ConstantCurve<Vec<f32>>),
|
|
|
|
/// A curve which interpolates weights linearly between keyframes.
|
|
Linear(WideLinearKeyframeCurve<f32>),
|
|
|
|
/// A curve which interpolates weights between keyframes in steps.
|
|
Step(WideSteppedKeyframeCurve<f32>),
|
|
|
|
/// A curve which interpolates between keyframes by using auxiliary tangent data to join
|
|
/// adjacent keyframes with a cubic Hermite spline, which is then sampled.
|
|
CubicSpline(WideCubicKeyframeCurve<f32>),
|
|
}
|
|
|
|
//---------//
|
|
// HELPERS //
|
|
//---------//
|
|
|
|
/// Helper function for cubic spline interpolation.
|
|
fn cubic_spline_interpolation<T>(
|
|
value_start: T,
|
|
tangent_out_start: T,
|
|
tangent_in_end: T,
|
|
value_end: T,
|
|
lerp: f32,
|
|
step_duration: f32,
|
|
) -> T
|
|
where
|
|
T: VectorSpace<Scalar = f32>,
|
|
{
|
|
let coeffs = (vec4(2.0, 1.0, -2.0, 1.0) * lerp + vec4(-3.0, -2.0, 3.0, -1.0)) * lerp;
|
|
value_start * (coeffs.x * lerp + 1.0)
|
|
+ tangent_out_start * step_duration * lerp * (coeffs.y + 1.0)
|
|
+ value_end * lerp * coeffs.z
|
|
+ tangent_in_end * step_duration * lerp * coeffs.w
|
|
}
|
|
|
|
fn cubic_spline_interpolate_slices<'a, T: VectorSpace<Scalar = f32>>(
|
|
width: usize,
|
|
first: &'a [T],
|
|
second: &'a [T],
|
|
s: f32,
|
|
step_between: f32,
|
|
) -> impl Iterator<Item = T> + 'a {
|
|
(0..width).map(move |idx| {
|
|
cubic_spline_interpolation(
|
|
first[idx + width],
|
|
first[idx + (width * 2)],
|
|
second[idx + width],
|
|
second[idx],
|
|
s,
|
|
step_between,
|
|
)
|
|
})
|
|
}
|