A Curve trait for general interoperation — Part II (#14700)
# Objective Finish what we started in #14630. The Curve RFC is [here](https://github.com/bevyengine/rfcs/blob/main/rfcs/80-curve-trait.md). ## Solution This contains the rest of the library from my branch. The main things added here are: - Bulk sampling / resampling methods on `Curve` itself - Data structures supporting the above - The `cores` submodule that those data structures use to encapsulate sample interpolation The weirdest thing in here is probably `ChunkedUnevenCore` in `cores`, which is not used by anything in the Curve library itself but which is required for efficient storage of glTF animation curves. (See #13105.) We can move it into a different PR if we want to; I don't have strong feelings either way. ## Testing New tests related to resampling are included. As I write this, I realize we could use some tests in `cores` itself, so I will add some on this branch before too long. --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: Robert Walter <26892280+RobWalt@users.noreply.github.com>
This commit is contained in:
parent
2012f13c05
commit
20a9b921a0
628
crates/bevy_math/src/curve/cores.rs
Normal file
628
crates/bevy_math/src/curve/cores.rs
Normal file
@ -0,0 +1,628 @@
|
|||||||
|
//! Core data structures to be used internally in Curve implementations, encapsulating storage
|
||||||
|
//! and access patterns for reuse.
|
||||||
|
//!
|
||||||
|
//! The `Core` types here expose their fields publicly so that it is easier to manipulate and
|
||||||
|
//! extend them, but in doing so, you must maintain the invariants of those fields yourself. The
|
||||||
|
//! provided methods all maintain the invariants, so this is only a concern if you manually mutate
|
||||||
|
//! the fields.
|
||||||
|
|
||||||
|
use super::interval::Interval;
|
||||||
|
use core::fmt::Debug;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[cfg(feature = "bevy_reflect")]
|
||||||
|
use bevy_reflect::Reflect;
|
||||||
|
|
||||||
|
/// This type expresses the relationship of a value to a fixed collection of values. It is a kind
|
||||||
|
/// of summary used intermediately by sampling operations.
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
|
||||||
|
pub enum InterpolationDatum<T> {
|
||||||
|
/// This value lies exactly on a value in the family.
|
||||||
|
Exact(T),
|
||||||
|
|
||||||
|
/// This value is off the left tail of the family; the inner value is the family's leftmost.
|
||||||
|
LeftTail(T),
|
||||||
|
|
||||||
|
/// This value is off the right tail of the family; the inner value is the family's rightmost.
|
||||||
|
RightTail(T),
|
||||||
|
|
||||||
|
/// This value lies on the interior, in between two points, with a third parameter expressing
|
||||||
|
/// the interpolation factor between the two.
|
||||||
|
Between(T, T, f32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> InterpolationDatum<T> {
|
||||||
|
/// Map all values using a given function `f`, leaving the interpolation parameters in any
|
||||||
|
/// [`Between`] variants unchanged.
|
||||||
|
///
|
||||||
|
/// [`Between`]: `InterpolationDatum::Between`
|
||||||
|
#[must_use]
|
||||||
|
pub fn map<S>(self, f: impl Fn(T) -> S) -> InterpolationDatum<S> {
|
||||||
|
match self {
|
||||||
|
InterpolationDatum::Exact(v) => InterpolationDatum::Exact(f(v)),
|
||||||
|
InterpolationDatum::LeftTail(v) => InterpolationDatum::LeftTail(f(v)),
|
||||||
|
InterpolationDatum::RightTail(v) => InterpolationDatum::RightTail(f(v)),
|
||||||
|
InterpolationDatum::Between(u, v, s) => InterpolationDatum::Between(f(u), f(v), s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The data core of a curve derived from evenly-spaced samples. The intention is to use this
|
||||||
|
/// in addition to explicit or inferred interpolation information in user-space in order to
|
||||||
|
/// implement curves using [`domain`] and [`sample_with`].
|
||||||
|
///
|
||||||
|
/// The internals are made transparent to give curve authors freedom, but [the provided constructor]
|
||||||
|
/// enforces the required invariants, and the methods maintain those invariants.
|
||||||
|
///
|
||||||
|
/// [the provided constructor]: EvenCore::new
|
||||||
|
/// [`domain`]: EvenCore::domain
|
||||||
|
/// [`sample_with`]: EvenCore::sample_with
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust
|
||||||
|
/// # use bevy_math::curve::*;
|
||||||
|
/// # use bevy_math::curve::cores::*;
|
||||||
|
/// // Let's make a curve that interpolates evenly spaced samples using either linear interpolation
|
||||||
|
/// // or step "interpolation" — i.e. just using the most recent sample as the source of truth.
|
||||||
|
/// enum InterpolationMode {
|
||||||
|
/// Linear,
|
||||||
|
/// Step,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// // Linear interpolation mode is driven by a trait.
|
||||||
|
/// trait LinearInterpolate {
|
||||||
|
/// fn lerp(&self, other: &Self, t: f32) -> Self;
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// // Step interpolation just uses an explicit function.
|
||||||
|
/// fn step<T: Clone>(first: &T, second: &T, t: f32) -> T {
|
||||||
|
/// if t >= 1.0 {
|
||||||
|
/// second.clone()
|
||||||
|
/// } else {
|
||||||
|
/// first.clone()
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// // Omitted: Implementing `LinearInterpolate` on relevant types; e.g. `f32`, `Vec3`, and so on.
|
||||||
|
///
|
||||||
|
/// // The curve itself uses `EvenCore` to hold the evenly-spaced samples, and the `sample_with`
|
||||||
|
/// // function will do all the work of interpolating once given a function to do it with.
|
||||||
|
/// struct MyCurve<T> {
|
||||||
|
/// core: EvenCore<T>,
|
||||||
|
/// interpolation_mode: InterpolationMode,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl<T> Curve<T> for MyCurve<T>
|
||||||
|
/// where
|
||||||
|
/// T: LinearInterpolate + Clone,
|
||||||
|
/// {
|
||||||
|
/// fn domain(&self) -> Interval {
|
||||||
|
/// self.core.domain()
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn sample_unchecked(&self, t: f32) -> T {
|
||||||
|
/// // To sample this curve, check the interpolation mode and dispatch accordingly.
|
||||||
|
/// match self.interpolation_mode {
|
||||||
|
/// InterpolationMode::Linear => self.core.sample_with(t, <T as LinearInterpolate>::lerp),
|
||||||
|
/// InterpolationMode::Step => self.core.sample_with(t, step),
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
|
||||||
|
pub struct EvenCore<T> {
|
||||||
|
/// The domain over which the samples are taken, which corresponds to the domain of the curve
|
||||||
|
/// formed by interpolating them.
|
||||||
|
///
|
||||||
|
/// # Invariants
|
||||||
|
/// This must always be a bounded interval; i.e. its endpoints must be finite.
|
||||||
|
pub domain: Interval,
|
||||||
|
|
||||||
|
/// The samples that are interpolated to extract values.
|
||||||
|
///
|
||||||
|
/// # Invariants
|
||||||
|
/// This must always have a length of at least 2.
|
||||||
|
pub samples: Vec<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An error indicating that an [`EvenCore`] could not be constructed.
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
#[error("Could not construct an EvenCore")]
|
||||||
|
pub enum EvenCoreError {
|
||||||
|
/// Not enough samples were provided.
|
||||||
|
#[error("Need at least two samples to create an EvenCore, but {samples} were provided")]
|
||||||
|
NotEnoughSamples {
|
||||||
|
/// The number of samples that were provided.
|
||||||
|
samples: usize,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Unbounded domains are not compatible with `EvenCore`.
|
||||||
|
#[error("Cannot create a EvenCore over an unbounded domain")]
|
||||||
|
UnboundedDomain,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> EvenCore<T> {
|
||||||
|
/// Create a new [`EvenCore`] from the specified `domain` and `samples`. The samples are
|
||||||
|
/// regarded to be evenly spaced within the given domain interval, so that the outermost
|
||||||
|
/// samples form the boundary of that interval. An error is returned if there are not at
|
||||||
|
/// least 2 samples or if the given domain is unbounded.
|
||||||
|
#[inline]
|
||||||
|
pub fn new(
|
||||||
|
domain: Interval,
|
||||||
|
samples: impl IntoIterator<Item = T>,
|
||||||
|
) -> Result<Self, EvenCoreError> {
|
||||||
|
let samples: Vec<T> = samples.into_iter().collect();
|
||||||
|
if samples.len() < 2 {
|
||||||
|
return Err(EvenCoreError::NotEnoughSamples {
|
||||||
|
samples: samples.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if !domain.is_bounded() {
|
||||||
|
return Err(EvenCoreError::UnboundedDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(EvenCore { domain, samples })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The domain of the curve derived from this core.
|
||||||
|
#[inline]
|
||||||
|
pub fn domain(&self) -> Interval {
|
||||||
|
self.domain
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtain a value from the held samples using the given `interpolation` to interpolate
|
||||||
|
/// between adjacent samples.
|
||||||
|
///
|
||||||
|
/// The interpolation takes two values by reference together with a scalar parameter and
|
||||||
|
/// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and
|
||||||
|
/// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively.
|
||||||
|
#[inline]
|
||||||
|
pub fn sample_with<I>(&self, t: f32, interpolation: I) -> T
|
||||||
|
where
|
||||||
|
T: Clone,
|
||||||
|
I: Fn(&T, &T, f32) -> T,
|
||||||
|
{
|
||||||
|
match even_interp(self.domain, self.samples.len(), t) {
|
||||||
|
InterpolationDatum::Exact(idx)
|
||||||
|
| InterpolationDatum::LeftTail(idx)
|
||||||
|
| InterpolationDatum::RightTail(idx) => self.samples[idx].clone(),
|
||||||
|
InterpolationDatum::Between(lower_idx, upper_idx, s) => {
|
||||||
|
interpolation(&self.samples[lower_idx], &self.samples[upper_idx], s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given a time `t`, obtain a [`InterpolationDatum`] which governs how interpolation might recover
|
||||||
|
/// a sample at time `t`. For example, when a [`Between`] value is returned, its contents can
|
||||||
|
/// be used to interpolate between the two contained values with the given parameter. The other
|
||||||
|
/// variants give additional context about where the value is relative to the family of samples.
|
||||||
|
///
|
||||||
|
/// [`Between`]: `InterpolationDatum::Between`
|
||||||
|
pub fn sample_interp(&self, t: f32) -> InterpolationDatum<&T> {
|
||||||
|
even_interp(self.domain, self.samples.len(), t).map(|idx| &self.samples[idx])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like [`sample_interp`], but the returned values include the sample times. This can be
|
||||||
|
/// useful when sample interpolation is not scale-invariant.
|
||||||
|
///
|
||||||
|
/// [`sample_interp`]: EvenCore::sample_interp
|
||||||
|
pub fn sample_interp_timed(&self, t: f32) -> InterpolationDatum<(f32, &T)> {
|
||||||
|
let segment_len = self.domain.length() / (self.samples.len() - 1) as f32;
|
||||||
|
even_interp(self.domain, self.samples.len(), t).map(|idx| {
|
||||||
|
(
|
||||||
|
self.domain.start() + segment_len * idx as f32,
|
||||||
|
&self.samples[idx],
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given a domain and a number of samples taken over that interval, return an [`InterpolationDatum`]
|
||||||
|
/// that governs how samples are extracted relative to the stored data.
|
||||||
|
///
|
||||||
|
/// `domain` must be a bounded interval (i.e. `domain.is_bounded() == true`).
|
||||||
|
///
|
||||||
|
/// `samples` must be at least 2.
|
||||||
|
///
|
||||||
|
/// This function will never panic, but it may return invalid indices if its assumptions are violated.
|
||||||
|
pub fn even_interp(domain: Interval, samples: usize, t: f32) -> InterpolationDatum<usize> {
|
||||||
|
let subdivs = samples - 1;
|
||||||
|
let step = domain.length() / subdivs as f32;
|
||||||
|
let t_shifted = t - domain.start();
|
||||||
|
let steps_taken = t_shifted / step;
|
||||||
|
|
||||||
|
if steps_taken <= 0.0 {
|
||||||
|
// To the left side of all the samples.
|
||||||
|
InterpolationDatum::LeftTail(0)
|
||||||
|
} else if steps_taken >= subdivs as f32 {
|
||||||
|
// To the right side of all the samples
|
||||||
|
InterpolationDatum::RightTail(samples - 1)
|
||||||
|
} else {
|
||||||
|
let lower_index = steps_taken.floor() as usize;
|
||||||
|
// This upper index is always valid because `steps_taken` is a finite value
|
||||||
|
// strictly less than `samples - 1`, so its floor is at most `samples - 2`
|
||||||
|
let upper_index = lower_index + 1;
|
||||||
|
let s = steps_taken.fract();
|
||||||
|
InterpolationDatum::Between(lower_index, upper_index, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The data core of a curve defined by unevenly-spaced samples or keyframes. The intention is to
|
||||||
|
/// use this in concert with implicitly or explicitly-defined interpolation in user-space in
|
||||||
|
/// order to implement the curve interface using [`domain`] and [`sample_with`].
|
||||||
|
///
|
||||||
|
/// The internals are made transparent to give curve authors freedom, but [the provided constructor]
|
||||||
|
/// enforces the required invariants, and the methods maintain those invariants.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust
|
||||||
|
/// # use bevy_math::curve::*;
|
||||||
|
/// # use bevy_math::curve::cores::*;
|
||||||
|
/// // Let's make a curve formed by interpolating rotations.
|
||||||
|
/// // We'll support two common modes of interpolation:
|
||||||
|
/// // - Normalized linear: First do linear interpolation, then normalize to get a valid rotation.
|
||||||
|
/// // - Spherical linear: Interpolate through valid rotations with constant angular velocity.
|
||||||
|
/// enum InterpolationMode {
|
||||||
|
/// NormalizedLinear,
|
||||||
|
/// SphericalLinear,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// // Our interpolation modes will be driven by traits.
|
||||||
|
/// trait NormalizedLinearInterpolate {
|
||||||
|
/// fn nlerp(&self, other: &Self, t: f32) -> Self;
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// trait SphericalLinearInterpolate {
|
||||||
|
/// fn slerp(&self, other: &Self, t: f32) -> Self;
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// // Omitted: These traits would be implemented for `Rot2`, `Quat`, and other rotation representations.
|
||||||
|
///
|
||||||
|
/// // The curve itself just needs to use the curve core for keyframes, `UnevenCore`, which handles
|
||||||
|
/// // everything except for the explicit interpolation used.
|
||||||
|
/// struct RotationCurve<T> {
|
||||||
|
/// core: UnevenCore<T>,
|
||||||
|
/// interpolation_mode: InterpolationMode,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl<T> Curve<T> for RotationCurve<T>
|
||||||
|
/// where
|
||||||
|
/// T: NormalizedLinearInterpolate + SphericalLinearInterpolate + Clone,
|
||||||
|
/// {
|
||||||
|
/// fn domain(&self) -> Interval {
|
||||||
|
/// self.core.domain()
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn sample_unchecked(&self, t: f32) -> T {
|
||||||
|
/// // To sample the curve, we just look at the interpolation mode and
|
||||||
|
/// // dispatch accordingly.
|
||||||
|
/// match self.interpolation_mode {
|
||||||
|
/// InterpolationMode::NormalizedLinear =>
|
||||||
|
/// self.core.sample_with(t, <T as NormalizedLinearInterpolate>::nlerp),
|
||||||
|
/// InterpolationMode::SphericalLinear =>
|
||||||
|
/// self.core.sample_with(t, <T as SphericalLinearInterpolate>::slerp),
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [`domain`]: UnevenCore::domain
|
||||||
|
/// [`sample_with`]: UnevenCore::sample_with
|
||||||
|
/// [the provided constructor]: UnevenCore::new
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
|
||||||
|
pub struct UnevenCore<T> {
|
||||||
|
/// The times for the samples of this curve.
|
||||||
|
///
|
||||||
|
/// # Invariants
|
||||||
|
/// This must always have a length of at least 2, be sorted, and have no
|
||||||
|
/// duplicated or non-finite times.
|
||||||
|
pub times: Vec<f32>,
|
||||||
|
|
||||||
|
/// The samples corresponding to the times for this curve.
|
||||||
|
///
|
||||||
|
/// # Invariants
|
||||||
|
/// This must always have the same length as `times`.
|
||||||
|
pub samples: Vec<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An error indicating that an [`UnevenCore`] could not be constructed.
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
#[error("Could not construct an UnevenCore")]
|
||||||
|
pub enum UnevenCoreError {
|
||||||
|
/// Not enough samples were provided.
|
||||||
|
#[error(
|
||||||
|
"Need at least two unique samples to create an UnevenCore, but {samples} were provided"
|
||||||
|
)]
|
||||||
|
NotEnoughSamples {
|
||||||
|
/// The number of samples that were provided.
|
||||||
|
samples: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> UnevenCore<T> {
|
||||||
|
/// Create a new [`UnevenCore`]. The given samples are filtered to finite times and
|
||||||
|
/// sorted internally; if there are not at least 2 valid timed samples, an error will be
|
||||||
|
/// returned.
|
||||||
|
pub fn new(timed_samples: impl IntoIterator<Item = (f32, T)>) -> Result<Self, UnevenCoreError> {
|
||||||
|
// Filter out non-finite sample times first so they don't interfere with sorting/deduplication.
|
||||||
|
let mut timed_samples = timed_samples
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(t, _)| t.is_finite())
|
||||||
|
.collect_vec();
|
||||||
|
timed_samples
|
||||||
|
// Using `total_cmp` is fine because no NANs remain and because deduplication uses
|
||||||
|
// `PartialEq` anyway (so -0.0 and 0.0 will be considered equal later regardless).
|
||||||
|
.sort_by(|(t0, _), (t1, _)| t0.total_cmp(t1));
|
||||||
|
timed_samples.dedup_by_key(|(t, _)| *t);
|
||||||
|
|
||||||
|
if timed_samples.len() < 2 {
|
||||||
|
return Err(UnevenCoreError::NotEnoughSamples {
|
||||||
|
samples: timed_samples.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let (times, samples): (Vec<f32>, Vec<T>) = timed_samples.into_iter().unzip();
|
||||||
|
Ok(UnevenCore { times, samples })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The domain of the curve derived from this core.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// This method may panic if the type's invariants aren't satisfied.
|
||||||
|
#[inline]
|
||||||
|
pub fn domain(&self) -> Interval {
|
||||||
|
let start = self.times.first().unwrap();
|
||||||
|
let end = self.times.last().unwrap();
|
||||||
|
Interval::new(*start, *end).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtain a value from the held samples using the given `interpolation` to interpolate
|
||||||
|
/// between adjacent samples.
|
||||||
|
///
|
||||||
|
/// The interpolation takes two values by reference together with a scalar parameter and
|
||||||
|
/// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and
|
||||||
|
/// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively.
|
||||||
|
#[inline]
|
||||||
|
pub fn sample_with<I>(&self, t: f32, interpolation: I) -> T
|
||||||
|
where
|
||||||
|
T: Clone,
|
||||||
|
I: Fn(&T, &T, f32) -> T,
|
||||||
|
{
|
||||||
|
match uneven_interp(&self.times, t) {
|
||||||
|
InterpolationDatum::Exact(idx)
|
||||||
|
| InterpolationDatum::LeftTail(idx)
|
||||||
|
| InterpolationDatum::RightTail(idx) => self.samples[idx].clone(),
|
||||||
|
InterpolationDatum::Between(lower_idx, upper_idx, s) => {
|
||||||
|
interpolation(&self.samples[lower_idx], &self.samples[upper_idx], s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given a time `t`, obtain a [`InterpolationDatum`] which governs how interpolation might recover
|
||||||
|
/// a sample at time `t`. For example, when a [`Between`] value is returned, its contents can
|
||||||
|
/// be used to interpolate between the two contained values with the given parameter. The other
|
||||||
|
/// variants give additional context about where the value is relative to the family of samples.
|
||||||
|
///
|
||||||
|
/// [`Between`]: `InterpolationDatum::Between`
|
||||||
|
pub fn sample_interp(&self, t: f32) -> InterpolationDatum<&T> {
|
||||||
|
uneven_interp(&self.times, t).map(|idx| &self.samples[idx])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like [`sample_interp`], but the returned values include the sample times. This can be
|
||||||
|
/// useful when sample interpolation is not scale-invariant.
|
||||||
|
///
|
||||||
|
/// [`sample_interp`]: UnevenCore::sample_interp
|
||||||
|
pub fn sample_interp_timed(&self, t: f32) -> InterpolationDatum<(f32, &T)> {
|
||||||
|
uneven_interp(&self.times, t).map(|idx| (self.times[idx], &self.samples[idx]))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This core, but with the sample times moved by the map `f`.
|
||||||
|
/// In principle, when `f` is monotone, this is equivalent to [`Curve::reparametrize`],
|
||||||
|
/// but the function inputs to each are inverses of one another.
|
||||||
|
///
|
||||||
|
/// The samples are re-sorted by time after mapping and deduplicated by output time, so
|
||||||
|
/// the function `f` should generally be injective over the set of sample times, otherwise
|
||||||
|
/// data will be deleted.
|
||||||
|
///
|
||||||
|
/// [`Curve::reparametrize`]: crate::curve::Curve::reparametrize
|
||||||
|
#[must_use]
|
||||||
|
pub fn map_sample_times(mut self, f: impl Fn(f32) -> f32) -> UnevenCore<T> {
|
||||||
|
let mut timed_samples = self
|
||||||
|
.times
|
||||||
|
.into_iter()
|
||||||
|
.map(f)
|
||||||
|
.zip(self.samples)
|
||||||
|
.collect_vec();
|
||||||
|
timed_samples.sort_by(|(t1, _), (t2, _)| t1.total_cmp(t2));
|
||||||
|
timed_samples.dedup_by_key(|(t, _)| *t);
|
||||||
|
(self.times, self.samples) = timed_samples.into_iter().unzip();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The data core of a curve using uneven samples (i.e. keyframes), where each sample time
|
||||||
|
/// yields some fixed number of values — the [sampling width]. This may serve as storage for
|
||||||
|
/// curves that yield vectors or iterators, and in some cases, it may be useful for cache locality
|
||||||
|
/// if the sample type can effectively be encoded as a fixed-length slice of values.
|
||||||
|
///
|
||||||
|
/// [sampling width]: ChunkedUnevenCore::width
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
|
||||||
|
pub struct ChunkedUnevenCore<T> {
|
||||||
|
/// The times, one for each sample.
|
||||||
|
///
|
||||||
|
/// # Invariants
|
||||||
|
/// This must always have a length of at least 2, be sorted, and have no duplicated or
|
||||||
|
/// non-finite times.
|
||||||
|
pub times: Vec<f32>,
|
||||||
|
|
||||||
|
/// The values that are used in sampling. Each width-worth of these correspond to a single sample.
|
||||||
|
///
|
||||||
|
/// # Invariants
|
||||||
|
/// The length of this vector must always be some fixed integer multiple of that of `times`.
|
||||||
|
pub values: Vec<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An error that indicates that a [`ChunkedUnevenCore`] could not be formed.
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
#[error("Could not create a ChunkedUnevenCore")]
|
||||||
|
pub enum ChunkedUnevenCoreError {
|
||||||
|
/// The width of a `ChunkedUnevenCore` cannot be zero.
|
||||||
|
#[error("Chunk width must be at least 1")]
|
||||||
|
ZeroWidth,
|
||||||
|
|
||||||
|
/// At least two sample times are necessary to interpolate in `ChunkedUnevenCore`.
|
||||||
|
#[error(
|
||||||
|
"Need at least two unique samples to create a ChunkedUnevenCore, but {samples} were provided"
|
||||||
|
)]
|
||||||
|
NotEnoughSamples {
|
||||||
|
/// The number of samples that were provided.
|
||||||
|
samples: usize,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// The length of the value buffer is supposed to be the `width` times the number of samples.
|
||||||
|
#[error("Expected {expected} total values based on width, but {actual} were provided")]
|
||||||
|
MismatchedLengths {
|
||||||
|
/// The expected length of the value buffer.
|
||||||
|
expected: usize,
|
||||||
|
/// The actual length of the value buffer.
|
||||||
|
actual: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ChunkedUnevenCore<T> {
|
||||||
|
/// Create a new [`ChunkedUnevenCore`]. The given `times` are sorted, filtered to finite times,
|
||||||
|
/// and deduplicated. See the [type-level documentation] for more information about this type.
|
||||||
|
///
|
||||||
|
/// Produces an error in any of the following circumstances:
|
||||||
|
/// - `width` is zero.
|
||||||
|
/// - `times` has less than `2` valid unique entries.
|
||||||
|
/// - `values` has the incorrect length relative to `times`.
|
||||||
|
///
|
||||||
|
/// [type-level documentation]: ChunkedUnevenCore
|
||||||
|
pub fn new(
|
||||||
|
times: impl Into<Vec<f32>>,
|
||||||
|
values: impl Into<Vec<T>>,
|
||||||
|
width: usize,
|
||||||
|
) -> Result<Self, ChunkedUnevenCoreError> {
|
||||||
|
let times: Vec<f32> = times.into();
|
||||||
|
let values: Vec<T> = values.into();
|
||||||
|
|
||||||
|
if width == 0 {
|
||||||
|
return Err(ChunkedUnevenCoreError::ZeroWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
let times = filter_sort_dedup_times(times);
|
||||||
|
|
||||||
|
if times.len() < 2 {
|
||||||
|
return Err(ChunkedUnevenCoreError::NotEnoughSamples {
|
||||||
|
samples: times.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if values.len() != times.len() * width {
|
||||||
|
return Err(ChunkedUnevenCoreError::MismatchedLengths {
|
||||||
|
expected: times.len() * width,
|
||||||
|
actual: values.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self { times, values })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The domain of the curve derived from this core.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// This may panic if this type's invariants aren't met.
|
||||||
|
#[inline]
|
||||||
|
pub fn domain(&self) -> Interval {
|
||||||
|
let start = self.times.first().unwrap();
|
||||||
|
let end = self.times.last().unwrap();
|
||||||
|
Interval::new(*start, *end).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The sample width: the number of values that are contained in each sample.
|
||||||
|
#[inline]
|
||||||
|
pub fn width(&self) -> usize {
|
||||||
|
self.values.len() / self.times.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given a time `t`, obtain a [`InterpolationDatum`] which governs how interpolation might recover
|
||||||
|
/// a sample at time `t`. For example, when a [`Between`] value is returned, its contents can
|
||||||
|
/// be used to interpolate between the two contained values with the given parameter. The other
|
||||||
|
/// variants give additional context about where the value is relative to the family of samples.
|
||||||
|
///
|
||||||
|
/// [`Between`]: `InterpolationDatum::Between`
|
||||||
|
#[inline]
|
||||||
|
pub fn sample_interp(&self, t: f32) -> InterpolationDatum<&[T]> {
|
||||||
|
uneven_interp(&self.times, t).map(|idx| self.time_index_to_slice(idx))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like [`sample_interp`], but the returned values include the sample times. This can be
|
||||||
|
/// useful when sample interpolation is not scale-invariant.
|
||||||
|
///
|
||||||
|
/// [`sample_interp`]: ChunkedUnevenCore::sample_interp
|
||||||
|
pub fn sample_interp_timed(&self, t: f32) -> InterpolationDatum<(f32, &[T])> {
|
||||||
|
uneven_interp(&self.times, t).map(|idx| (self.times[idx], self.time_index_to_slice(idx)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given an index in [times], returns the slice of [values] that correspond to the sample at
|
||||||
|
/// that time.
|
||||||
|
///
|
||||||
|
/// [times]: ChunkedUnevenCore::times
|
||||||
|
/// [values]: ChunkedUnevenCore::values
|
||||||
|
#[inline]
|
||||||
|
fn time_index_to_slice(&self, idx: usize) -> &[T] {
|
||||||
|
let width = self.width();
|
||||||
|
let lower_idx = width * idx;
|
||||||
|
let upper_idx = lower_idx + width;
|
||||||
|
&self.values[lower_idx..upper_idx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sort the given times, deduplicate them, and filter them to only finite times.
|
||||||
|
fn filter_sort_dedup_times(times: impl IntoIterator<Item = f32>) -> Vec<f32> {
|
||||||
|
// Filter before sorting/deduplication so that NAN doesn't interfere with them.
|
||||||
|
let mut times = times.into_iter().filter(|t| t.is_finite()).collect_vec();
|
||||||
|
times.sort_by(f32::total_cmp);
|
||||||
|
times.dedup();
|
||||||
|
times
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given a list of `times` and a target value, get the interpolation relationship for the
|
||||||
|
/// target value in terms of the indices of the starting list. In a sense, this encapsulates the
|
||||||
|
/// heart of uneven/keyframe sampling.
|
||||||
|
///
|
||||||
|
/// `times` is assumed to be sorted, deduplicated, and consisting only of finite values. It is also
|
||||||
|
/// assumed to contain at least two values.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// This function will panic if `times` contains NAN.
|
||||||
|
pub fn uneven_interp(times: &[f32], t: f32) -> InterpolationDatum<usize> {
|
||||||
|
match times.binary_search_by(|pt| pt.partial_cmp(&t).unwrap()) {
|
||||||
|
Ok(index) => InterpolationDatum::Exact(index),
|
||||||
|
Err(index) => {
|
||||||
|
if index == 0 {
|
||||||
|
// This is before the first keyframe.
|
||||||
|
InterpolationDatum::LeftTail(0)
|
||||||
|
} else if index >= times.len() {
|
||||||
|
// This is after the last keyframe.
|
||||||
|
InterpolationDatum::RightTail(times.len() - 1)
|
||||||
|
} else {
|
||||||
|
// This is actually in the middle somewhere.
|
||||||
|
let t_lower = times[index - 1];
|
||||||
|
let t_upper = times[index];
|
||||||
|
let s = (t - t_lower) / (t_upper - t_lower);
|
||||||
|
InterpolationDatum::Between(index - 1, index, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,10 +2,14 @@
|
|||||||
//! contains the [`Interval`] type, along with a selection of core data structures used to back
|
//! contains the [`Interval`] type, along with a selection of core data structures used to back
|
||||||
//! curves that are interpolated from samples.
|
//! curves that are interpolated from samples.
|
||||||
|
|
||||||
|
pub mod cores;
|
||||||
pub mod interval;
|
pub mod interval;
|
||||||
|
|
||||||
pub use interval::{interval, Interval};
|
pub use interval::{interval, Interval};
|
||||||
|
use itertools::Itertools;
|
||||||
|
|
||||||
|
use crate::StableInterpolate;
|
||||||
|
use cores::{EvenCore, EvenCoreError, UnevenCore, UnevenCoreError};
|
||||||
use interval::InvalidIntervalError;
|
use interval::InvalidIntervalError;
|
||||||
use std::{marker::PhantomData, ops::Deref};
|
use std::{marker::PhantomData, ops::Deref};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
@ -50,6 +54,7 @@ pub trait Curve<T> {
|
|||||||
/// Create a new curve by mapping the values of this curve via a function `f`; i.e., if the
|
/// Create a new curve by mapping the values of this curve via a function `f`; i.e., if the
|
||||||
/// sample at time `t` for this curve is `x`, the value at time `t` on the new curve will be
|
/// sample at time `t` for this curve is `x`, the value at time `t` on the new curve will be
|
||||||
/// `f(x)`.
|
/// `f(x)`.
|
||||||
|
#[must_use]
|
||||||
fn map<S, F>(self, f: F) -> MapCurve<T, S, Self, F>
|
fn map<S, F>(self, f: F) -> MapCurve<T, S, Self, F>
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
@ -98,6 +103,7 @@ pub trait Curve<T> {
|
|||||||
/// let domain = my_curve.domain();
|
/// let domain = my_curve.domain();
|
||||||
/// let eased_curve = my_curve.reparametrize(domain, |t| easing_curve.sample_unchecked(t).y);
|
/// let eased_curve = my_curve.reparametrize(domain, |t| easing_curve.sample_unchecked(t).y);
|
||||||
/// ```
|
/// ```
|
||||||
|
#[must_use]
|
||||||
fn reparametrize<F>(self, domain: Interval, f: F) -> ReparamCurve<T, Self, F>
|
fn reparametrize<F>(self, domain: Interval, f: F) -> ReparamCurve<T, Self, F>
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
@ -142,6 +148,7 @@ pub trait Curve<T> {
|
|||||||
/// The resulting curve samples at time `t` by first sampling `other` at time `t`, which produces
|
/// The resulting curve samples at time `t` by first sampling `other` at time `t`, which produces
|
||||||
/// another sample time `s` which is then used to sample this curve. The domain of the resulting
|
/// another sample time `s` which is then used to sample this curve. The domain of the resulting
|
||||||
/// curve is the domain of `other`.
|
/// curve is the domain of `other`.
|
||||||
|
#[must_use]
|
||||||
fn reparametrize_by_curve<C>(self, other: C) -> CurveReparamCurve<T, Self, C>
|
fn reparametrize_by_curve<C>(self, other: C) -> CurveReparamCurve<T, Self, C>
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
@ -160,6 +167,7 @@ pub trait Curve<T> {
|
|||||||
/// For example, if this curve outputs `x` at time `t`, then the produced curve will produce
|
/// For example, if this curve outputs `x` at time `t`, then the produced curve will produce
|
||||||
/// `(t, x)` at time `t`. In particular, if this curve is a `Curve<T>`, the output of this method
|
/// `(t, x)` at time `t`. In particular, if this curve is a `Curve<T>`, the output of this method
|
||||||
/// is a `Curve<(f32, T)>`.
|
/// is a `Curve<(f32, T)>`.
|
||||||
|
#[must_use]
|
||||||
fn graph(self) -> GraphCurve<T, Self>
|
fn graph(self) -> GraphCurve<T, Self>
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
@ -212,12 +220,171 @@ pub trait Curve<T> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resample this [`Curve`] to produce a new one that is defined by interpolation over equally
|
||||||
|
/// spaced sample values, using the provided `interpolation` to interpolate between adjacent samples.
|
||||||
|
/// The curve is interpolated on `segments` segments between samples. For example, if `segments` is 1,
|
||||||
|
/// only the start and end points of the curve are used as samples; if `segments` is 2, a sample at
|
||||||
|
/// the midpoint is taken as well, and so on. If `segments` is zero, or if this curve has an unbounded
|
||||||
|
/// domain, then a [`ResamplingError`] is returned.
|
||||||
|
///
|
||||||
|
/// The interpolation takes two values by reference together with a scalar parameter and
|
||||||
|
/// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and
|
||||||
|
/// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// # use bevy_math::*;
|
||||||
|
/// # use bevy_math::curve::*;
|
||||||
|
/// let quarter_rotation = function_curve(interval(0.0, 90.0).unwrap(), |t| Rot2::degrees(t));
|
||||||
|
/// // A curve which only stores three data points and uses `nlerp` to interpolate them:
|
||||||
|
/// let resampled_rotation = quarter_rotation.resample(3, |x, y, t| x.nlerp(*y, t));
|
||||||
|
/// ```
|
||||||
|
fn resample<I>(
|
||||||
|
&self,
|
||||||
|
segments: usize,
|
||||||
|
interpolation: I,
|
||||||
|
) -> Result<SampleCurve<T, I>, ResamplingError>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
I: Fn(&T, &T, f32) -> T,
|
||||||
|
{
|
||||||
|
let samples = self.samples(segments + 1)?.collect_vec();
|
||||||
|
Ok(SampleCurve {
|
||||||
|
core: EvenCore {
|
||||||
|
domain: self.domain(),
|
||||||
|
samples,
|
||||||
|
},
|
||||||
|
interpolation,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resample this [`Curve`] to produce a new one that is defined by interpolation over equally
|
||||||
|
/// spaced sample values, using [automatic interpolation] to interpolate between adjacent samples.
|
||||||
|
/// The curve is interpolated on `segments` segments between samples. For example, if `segments` is 1,
|
||||||
|
/// only the start and end points of the curve are used as samples; if `segments` is 2, a sample at
|
||||||
|
/// the midpoint is taken as well, and so on. If `segments` is zero, or if this curve has an unbounded
|
||||||
|
/// domain, then a [`ResamplingError`] is returned.
|
||||||
|
///
|
||||||
|
/// [automatic interpolation]: crate::common_traits::StableInterpolate
|
||||||
|
fn resample_auto(&self, segments: usize) -> Result<SampleAutoCurve<T>, ResamplingError>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
T: StableInterpolate,
|
||||||
|
{
|
||||||
|
let samples = self.samples(segments + 1)?.collect_vec();
|
||||||
|
Ok(SampleAutoCurve {
|
||||||
|
core: EvenCore {
|
||||||
|
domain: self.domain(),
|
||||||
|
samples,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract an iterator over evenly-spaced samples from this curve. If `samples` is less than 2
|
||||||
|
/// or if this curve has unbounded domain, then an error is returned instead.
|
||||||
|
fn samples(&self, samples: usize) -> Result<impl Iterator<Item = T>, ResamplingError>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
if samples < 2 {
|
||||||
|
return Err(ResamplingError::NotEnoughSamples(samples));
|
||||||
|
}
|
||||||
|
if !self.domain().is_bounded() {
|
||||||
|
return Err(ResamplingError::UnboundedDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap on `spaced_points` always succeeds because its error conditions are handled
|
||||||
|
// above.
|
||||||
|
Ok(self
|
||||||
|
.domain()
|
||||||
|
.spaced_points(samples)
|
||||||
|
.unwrap()
|
||||||
|
.map(|t| self.sample_unchecked(t)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resample this [`Curve`] to produce a new one that is defined by interpolation over samples
|
||||||
|
/// taken at a given set of times. The given `interpolation` is used to interpolate adjacent
|
||||||
|
/// samples, and the `sample_times` are expected to contain at least two valid times within the
|
||||||
|
/// curve's domain interval.
|
||||||
|
///
|
||||||
|
/// Redundant sample times, non-finite sample times, and sample times outside of the domain
|
||||||
|
/// are simply filtered out. With an insufficient quantity of data, a [`ResamplingError`] is
|
||||||
|
/// returned.
|
||||||
|
///
|
||||||
|
/// The domain of the produced curve stretches between the first and last sample times of the
|
||||||
|
/// iterator.
|
||||||
|
///
|
||||||
|
/// The interpolation takes two values by reference together with a scalar parameter and
|
||||||
|
/// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and
|
||||||
|
/// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively.
|
||||||
|
fn resample_uneven<I>(
|
||||||
|
&self,
|
||||||
|
sample_times: impl IntoIterator<Item = f32>,
|
||||||
|
interpolation: I,
|
||||||
|
) -> Result<UnevenSampleCurve<T, I>, ResamplingError>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
I: Fn(&T, &T, f32) -> T,
|
||||||
|
{
|
||||||
|
let domain = self.domain();
|
||||||
|
let mut times = sample_times
|
||||||
|
.into_iter()
|
||||||
|
.filter(|t| t.is_finite() && domain.contains(*t))
|
||||||
|
.collect_vec();
|
||||||
|
times.sort_by(f32::total_cmp);
|
||||||
|
times.dedup();
|
||||||
|
if times.len() < 2 {
|
||||||
|
return Err(ResamplingError::NotEnoughSamples(times.len()));
|
||||||
|
}
|
||||||
|
let samples = times.iter().map(|t| self.sample_unchecked(*t)).collect();
|
||||||
|
Ok(UnevenSampleCurve {
|
||||||
|
core: UnevenCore { times, samples },
|
||||||
|
interpolation,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resample this [`Curve`] to produce a new one that is defined by [automatic interpolation] over
|
||||||
|
/// samples taken at the given set of times. The given `sample_times` are expected to contain at least
|
||||||
|
/// two valid times within the curve's domain interval.
|
||||||
|
///
|
||||||
|
/// Redundant sample times, non-finite sample times, and sample times outside of the domain
|
||||||
|
/// are simply filtered out. With an insufficient quantity of data, a [`ResamplingError`] is
|
||||||
|
/// returned.
|
||||||
|
///
|
||||||
|
/// The domain of the produced [`UnevenSampleAutoCurve`] stretches between the first and last
|
||||||
|
/// sample times of the iterator.
|
||||||
|
///
|
||||||
|
/// [automatic interpolation]: crate::common_traits::StableInterpolate
|
||||||
|
fn resample_uneven_auto(
|
||||||
|
&self,
|
||||||
|
sample_times: impl IntoIterator<Item = f32>,
|
||||||
|
) -> Result<UnevenSampleAutoCurve<T>, ResamplingError>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
T: StableInterpolate,
|
||||||
|
{
|
||||||
|
let domain = self.domain();
|
||||||
|
let mut times = sample_times
|
||||||
|
.into_iter()
|
||||||
|
.filter(|t| t.is_finite() && domain.contains(*t))
|
||||||
|
.collect_vec();
|
||||||
|
times.sort_by(f32::total_cmp);
|
||||||
|
times.dedup();
|
||||||
|
if times.len() < 2 {
|
||||||
|
return Err(ResamplingError::NotEnoughSamples(times.len()));
|
||||||
|
}
|
||||||
|
let samples = times.iter().map(|t| self.sample_unchecked(*t)).collect();
|
||||||
|
Ok(UnevenSampleAutoCurve {
|
||||||
|
core: UnevenCore { times, samples },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Borrow this curve rather than taking ownership of it. This is essentially an alias for a
|
/// Borrow this curve rather than taking ownership of it. This is essentially an alias for a
|
||||||
/// prefix `&`; the point is that intermediate operations can be performed while retaining
|
/// prefix `&`; the point is that intermediate operations can be performed while retaining
|
||||||
/// access to the original curve.
|
/// access to the original curve.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```ignore
|
/// ```
|
||||||
/// # use bevy_math::curve::*;
|
/// # use bevy_math::curve::*;
|
||||||
/// let my_curve = function_curve(interval(0.0, 1.0).unwrap(), |t| t * t + 1.0);
|
/// let my_curve = function_curve(interval(0.0, 1.0).unwrap(), |t| t * t + 1.0);
|
||||||
/// // Borrow `my_curve` long enough to resample a mapped version. Note that `map` takes
|
/// // Borrow `my_curve` long enough to resample a mapped version. Note that `map` takes
|
||||||
@ -234,6 +401,7 @@ pub trait Curve<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Flip this curve so that its tuple output is arranged the other way.
|
/// Flip this curve so that its tuple output is arranged the other way.
|
||||||
|
#[must_use]
|
||||||
fn flip<U, V>(self) -> impl Curve<(V, U)>
|
fn flip<U, V>(self) -> impl Curve<(V, U)>
|
||||||
where
|
where
|
||||||
Self: Sized + Curve<(U, V)>,
|
Self: Sized + Curve<(U, V)>,
|
||||||
@ -284,6 +452,20 @@ pub enum ChainError {
|
|||||||
SecondStartInfinite,
|
SecondStartInfinite,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An error indicating that a resampling operation could not be performed because of
|
||||||
|
/// malformed inputs.
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
#[error("Could not resample from this curve because of bad inputs")]
|
||||||
|
pub enum ResamplingError {
|
||||||
|
/// This resampling operation was not provided with enough samples to have well-formed output.
|
||||||
|
#[error("Not enough unique samples to construct resampled curve")]
|
||||||
|
NotEnoughSamples(usize),
|
||||||
|
|
||||||
|
/// This resampling operation failed because of an unbounded interval.
|
||||||
|
#[error("Could not resample because this curve has unbounded domain")]
|
||||||
|
UnboundedDomain,
|
||||||
|
}
|
||||||
|
|
||||||
/// A curve with a constant value over its domain.
|
/// A curve with a constant value over its domain.
|
||||||
///
|
///
|
||||||
/// This is a curve that holds an inner value and always produces a clone of that value when sampled.
|
/// This is a curve that holds an inner value and always produces a clone of that value when sampled.
|
||||||
@ -575,6 +757,199 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A curve that is defined by explicit neighbor interpolation over a set of samples.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
|
||||||
|
pub struct SampleCurve<T, I> {
|
||||||
|
core: EvenCore<T>,
|
||||||
|
interpolation: I,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, I> Curve<T> for SampleCurve<T, I>
|
||||||
|
where
|
||||||
|
T: Clone,
|
||||||
|
I: Fn(&T, &T, f32) -> T,
|
||||||
|
{
|
||||||
|
#[inline]
|
||||||
|
fn domain(&self) -> Interval {
|
||||||
|
self.core.domain()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn sample_unchecked(&self, t: f32) -> T {
|
||||||
|
self.core.sample_with(t, &self.interpolation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, I> SampleCurve<T, I> {
|
||||||
|
/// Create a new [`SampleCurve`] using the specified `interpolation` to interpolate between
|
||||||
|
/// the given `samples`. An error is returned if there are not at least 2 samples or if the
|
||||||
|
/// given `domain` is unbounded.
|
||||||
|
///
|
||||||
|
/// The interpolation takes two values by reference together with a scalar parameter and
|
||||||
|
/// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and
|
||||||
|
/// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively.
|
||||||
|
pub fn new(
|
||||||
|
domain: Interval,
|
||||||
|
samples: impl IntoIterator<Item = T>,
|
||||||
|
interpolation: I,
|
||||||
|
) -> Result<Self, EvenCoreError>
|
||||||
|
where
|
||||||
|
I: Fn(&T, &T, f32) -> T,
|
||||||
|
{
|
||||||
|
Ok(Self {
|
||||||
|
core: EvenCore::new(domain, samples)?,
|
||||||
|
interpolation,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A curve that is defined by neighbor interpolation over a set of samples.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
|
||||||
|
pub struct SampleAutoCurve<T> {
|
||||||
|
core: EvenCore<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Curve<T> for SampleAutoCurve<T>
|
||||||
|
where
|
||||||
|
T: StableInterpolate,
|
||||||
|
{
|
||||||
|
#[inline]
|
||||||
|
fn domain(&self) -> Interval {
|
||||||
|
self.core.domain()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn sample_unchecked(&self, t: f32) -> T {
|
||||||
|
self.core
|
||||||
|
.sample_with(t, <T as StableInterpolate>::interpolate_stable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> SampleAutoCurve<T> {
|
||||||
|
/// Create a new [`SampleCurve`] using type-inferred interpolation to interpolate between
|
||||||
|
/// the given `samples`. An error is returned if there are not at least 2 samples or if the
|
||||||
|
/// given `domain` is unbounded.
|
||||||
|
pub fn new(
|
||||||
|
domain: Interval,
|
||||||
|
samples: impl IntoIterator<Item = T>,
|
||||||
|
) -> Result<Self, EvenCoreError> {
|
||||||
|
Ok(Self {
|
||||||
|
core: EvenCore::new(domain, samples)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A curve that is defined by interpolation over unevenly spaced samples with explicit
|
||||||
|
/// interpolation.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
|
||||||
|
pub struct UnevenSampleCurve<T, I> {
|
||||||
|
core: UnevenCore<T>,
|
||||||
|
interpolation: I,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, I> Curve<T> for UnevenSampleCurve<T, I>
|
||||||
|
where
|
||||||
|
T: Clone,
|
||||||
|
I: Fn(&T, &T, f32) -> T,
|
||||||
|
{
|
||||||
|
#[inline]
|
||||||
|
fn domain(&self) -> Interval {
|
||||||
|
self.core.domain()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn sample_unchecked(&self, t: f32) -> T {
|
||||||
|
self.core.sample_with(t, &self.interpolation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, I> UnevenSampleCurve<T, I> {
|
||||||
|
/// Create a new [`UnevenSampleCurve`] using the provided `interpolation` to interpolate
|
||||||
|
/// between adjacent `timed_samples`. The given samples are filtered to finite times and
|
||||||
|
/// sorted internally; if there are not at least 2 valid timed samples, an error will be
|
||||||
|
/// returned.
|
||||||
|
///
|
||||||
|
/// The interpolation takes two values by reference together with a scalar parameter and
|
||||||
|
/// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and
|
||||||
|
/// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively.
|
||||||
|
pub fn new(
|
||||||
|
timed_samples: impl IntoIterator<Item = (f32, T)>,
|
||||||
|
interpolation: I,
|
||||||
|
) -> Result<Self, UnevenCoreError> {
|
||||||
|
Ok(Self {
|
||||||
|
core: UnevenCore::new(timed_samples)?,
|
||||||
|
interpolation,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This [`UnevenSampleAutoCurve`], but with the sample times moved by the map `f`.
|
||||||
|
/// In principle, when `f` is monotone, this is equivalent to [`Curve::reparametrize`],
|
||||||
|
/// but the function inputs to each are inverses of one another.
|
||||||
|
///
|
||||||
|
/// The samples are re-sorted by time after mapping and deduplicated by output time, so
|
||||||
|
/// the function `f` should generally be injective over the sample times of the curve.
|
||||||
|
pub fn map_sample_times(self, f: impl Fn(f32) -> f32) -> UnevenSampleCurve<T, I> {
|
||||||
|
Self {
|
||||||
|
core: self.core.map_sample_times(f),
|
||||||
|
interpolation: self.interpolation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A curve that is defined by interpolation over unevenly spaced samples.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
|
||||||
|
pub struct UnevenSampleAutoCurve<T> {
|
||||||
|
core: UnevenCore<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Curve<T> for UnevenSampleAutoCurve<T>
|
||||||
|
where
|
||||||
|
T: StableInterpolate,
|
||||||
|
{
|
||||||
|
#[inline]
|
||||||
|
fn domain(&self) -> Interval {
|
||||||
|
self.core.domain()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn sample_unchecked(&self, t: f32) -> T {
|
||||||
|
self.core
|
||||||
|
.sample_with(t, <T as StableInterpolate>::interpolate_stable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> UnevenSampleAutoCurve<T> {
|
||||||
|
/// Create a new [`UnevenSampleAutoCurve`] from a given set of timed samples, interpolated
|
||||||
|
/// using the The samples are filtered to finite times and
|
||||||
|
/// sorted internally; if there are not at least 2 valid timed samples, an error will be
|
||||||
|
/// returned.
|
||||||
|
pub fn new(timed_samples: impl IntoIterator<Item = (f32, T)>) -> Result<Self, UnevenCoreError> {
|
||||||
|
Ok(Self {
|
||||||
|
core: UnevenCore::new(timed_samples)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This [`UnevenSampleAutoCurve`], but with the sample times moved by the map `f`.
|
||||||
|
/// In principle, when `f` is monotone, this is equivalent to [`Curve::reparametrize`],
|
||||||
|
/// but the function inputs to each are inverses of one another.
|
||||||
|
///
|
||||||
|
/// The samples are re-sorted by time after mapping and deduplicated by output time, so
|
||||||
|
/// the function `f` should generally be injective over the sample times of the curve.
|
||||||
|
pub fn map_sample_times(self, f: impl Fn(f32) -> f32) -> UnevenSampleAutoCurve<T> {
|
||||||
|
Self {
|
||||||
|
core: self.core.map_sample_times(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a [`Curve`] that constantly takes the given `value` over the given `domain`.
|
/// Create a [`Curve`] that constantly takes the given `value` over the given `domain`.
|
||||||
pub fn constant_curve<T: Clone>(domain: Interval, value: T) -> ConstantCurve<T> {
|
pub fn constant_curve<T: Clone>(domain: Interval, value: T) -> ConstantCurve<T> {
|
||||||
ConstantCurve { domain, value }
|
ConstantCurve { domain, value }
|
||||||
@ -682,4 +1057,71 @@ mod tests {
|
|||||||
assert_abs_diff_eq!(second_reparam.sample_unchecked(0.5), 1.5);
|
assert_abs_diff_eq!(second_reparam.sample_unchecked(0.5), 1.5);
|
||||||
assert_abs_diff_eq!(second_reparam.sample_unchecked(1.0), 2.0);
|
assert_abs_diff_eq!(second_reparam.sample_unchecked(1.0), 2.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resampling() {
|
||||||
|
let curve = function_curve(interval(1.0, 4.0).unwrap(), ops::log2);
|
||||||
|
|
||||||
|
// Need at least one segment to sample.
|
||||||
|
let nice_try = curve.by_ref().resample_auto(0);
|
||||||
|
assert!(nice_try.is_err());
|
||||||
|
|
||||||
|
// The values of a resampled curve should be very close at the sample points.
|
||||||
|
// Because of denominators, it's not literally equal.
|
||||||
|
// (This is a tradeoff against O(1) sampling.)
|
||||||
|
let resampled_curve = curve.by_ref().resample_auto(100).unwrap();
|
||||||
|
for test_pt in curve.domain().spaced_points(101).unwrap() {
|
||||||
|
let expected = curve.sample_unchecked(test_pt);
|
||||||
|
assert_abs_diff_eq!(
|
||||||
|
resampled_curve.sample_unchecked(test_pt),
|
||||||
|
expected,
|
||||||
|
epsilon = 1e-6
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Another example.
|
||||||
|
let curve = function_curve(interval(0.0, TAU).unwrap(), ops::cos);
|
||||||
|
let resampled_curve = curve.by_ref().resample_auto(1000).unwrap();
|
||||||
|
for test_pt in curve.domain().spaced_points(1001).unwrap() {
|
||||||
|
let expected = curve.sample_unchecked(test_pt);
|
||||||
|
assert_abs_diff_eq!(
|
||||||
|
resampled_curve.sample_unchecked(test_pt),
|
||||||
|
expected,
|
||||||
|
epsilon = 1e-6
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn uneven_resampling() {
|
||||||
|
let curve = function_curve(interval(0.0, f32::INFINITY).unwrap(), ops::exp);
|
||||||
|
|
||||||
|
// Need at least two points to resample.
|
||||||
|
let nice_try = curve.by_ref().resample_uneven_auto([1.0; 1]);
|
||||||
|
assert!(nice_try.is_err());
|
||||||
|
|
||||||
|
// Uneven sampling should produce literal equality at the sample points.
|
||||||
|
// (This is part of what you get in exchange for O(log(n)) sampling.)
|
||||||
|
let sample_points = (0..100).map(|idx| idx as f32 * 0.1);
|
||||||
|
let resampled_curve = curve.by_ref().resample_uneven_auto(sample_points).unwrap();
|
||||||
|
for idx in 0..100 {
|
||||||
|
let test_pt = idx as f32 * 0.1;
|
||||||
|
let expected = curve.sample_unchecked(test_pt);
|
||||||
|
assert_eq!(resampled_curve.sample_unchecked(test_pt), expected);
|
||||||
|
}
|
||||||
|
assert_abs_diff_eq!(resampled_curve.domain().start(), 0.0);
|
||||||
|
assert_abs_diff_eq!(resampled_curve.domain().end(), 9.9, epsilon = 1e-6);
|
||||||
|
|
||||||
|
// Another example.
|
||||||
|
let curve = function_curve(interval(1.0, f32::INFINITY).unwrap(), ops::log2);
|
||||||
|
let sample_points = (0..10).map(|idx| ops::exp2(idx as f32));
|
||||||
|
let resampled_curve = curve.by_ref().resample_uneven_auto(sample_points).unwrap();
|
||||||
|
for idx in 0..10 {
|
||||||
|
let test_pt = ops::exp2(idx as f32);
|
||||||
|
let expected = curve.sample_unchecked(test_pt);
|
||||||
|
assert_eq!(resampled_curve.sample_unchecked(test_pt), expected);
|
||||||
|
}
|
||||||
|
assert_abs_diff_eq!(resampled_curve.domain().start(), 1.0);
|
||||||
|
assert_abs_diff_eq!(resampled_curve.domain().end(), 512.0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user