From 708645f74570476be569f45418f762a16d74215d Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Thu, 11 Apr 2024 19:43:54 -0400 Subject: [PATCH 01/44] First draft --- crates/bevy_math/src/curve.rs | 662 ++++++++++++++++++++++++++++++++++ crates/bevy_math/src/lib.rs | 1 + 2 files changed, 663 insertions(+) create mode 100644 crates/bevy_math/src/curve.rs diff --git a/crates/bevy_math/src/curve.rs b/crates/bevy_math/src/curve.rs new file mode 100644 index 0000000000000..c7d5e4b53852b --- /dev/null +++ b/crates/bevy_math/src/curve.rs @@ -0,0 +1,662 @@ +//! Houses the [`Curve`] trait together with the [`Interpolable`] trait that it depends on. + +use std::{cmp::max, marker::PhantomData}; +use crate::Quat; +// use serde::{de::DeserializeOwned, Serialize}; + +use crate::VectorSpace; + +/// A trait for types whose values can be intermediately interpolated between two given values +/// with an auxiliary parameter. +pub trait Interpolable: Clone { + /// Interpolate between this value and the `other` given value using the parameter `t`. + /// Note that the parameter `t` is not necessarily clamped to lie between `0` and `1`. + fn interpolate(&self, other: &Self, t: f32) -> Self; +} + +impl Interpolable for (S, T) +where + S: Interpolable, + T: Interpolable, +{ + fn interpolate(&self, other: &Self, t: f32) -> Self { + ( + self.0.interpolate(&other.0, t), + self.1.interpolate(&other.1, t), + ) + } +} + +impl Interpolable for T +where + T: VectorSpace, +{ + fn interpolate(&self, other: &Self, t: f32) -> Self { + self.lerp(*other, t) + } +} + +impl Interpolable for Quat { + fn interpolate(&self, other: &Self, t: f32) -> Self { + self.slerp(*other, t) + } +} + + +/// A trait for a type that can represent values of type `T` parametrized over a fixed interval. +/// Typical examples of this are actual geometric curves where `T: VectorSpace`, but other kinds +/// of interpolable data can be represented instead (or in addition). +pub trait Curve +where + T: Interpolable, +{ + /// The point at which parameter values of this curve end. That is, this curve is parametrized + /// on the interval `[0, self.duration()]`. + fn duration(&self) -> f32; + + /// Sample a point on this curve at the parameter value `t`, extracting the associated value. + fn sample(&self, t: f32) -> T; + + /// Resample this [`Curve`] to produce a new one that is defined by interpolation over equally + /// spaced values. A total of `samples` samples are used. + /// + /// Panics if `samples == 0`. + fn resample(&self, samples: usize) -> SampleCurve { + assert!(samples != 0); + + // When `samples` is 1, we just record the starting point, and `step` doesn't matter. + let subdivisions = max(1, samples - 1); + let step = self.duration() / subdivisions as f32; + let samples: Vec = (0..samples).map(|s| self.sample(s as f32 * step)).collect(); + SampleCurve { + duration: self.duration(), + samples, + } + } + + /// Resample this [`Curve`] to produce a new one that is defined by interpolation over samples + /// taken at the given set of times. The given `sample_times` are expected to be strictly + /// increasing and nonempty. + fn resample_uneven(&self, sample_times: impl IntoIterator) -> UnevenSampleCurve { + let mut iter = sample_times.into_iter(); + let Some(first) = iter.next() else { + panic!("Empty iterator supplied") + }; + // Offset by the first element so that we get a curve starting at zero. + let first_sample = self.sample(first); + let mut timed_samples = vec![(0.0, first_sample)]; + timed_samples.extend(iter.map(|t| (t - first, self.sample(t)))); + UnevenSampleCurve { timed_samples } + } + + /// 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 + /// `f(x)`. + fn map(self, f: impl Fn(T) -> S) -> impl Curve + where + Self: Sized, + S: Interpolable, + { + MapCurve { + preimage: self, + f, + _phantom: PhantomData, + } + } + + /// Create a new [`Curve`] whose parameter space is related to the parameter space of this curve + /// by `f`. For each time `t`, the sample from the new curve at time `t` is the sample from + /// this curve at time `f(t)`. The given `duration` will be the duration of the new curve. The + /// function `f` is expected to take `[0, duration]` into `[0, self.duration]`. + /// + /// Note that this is the opposite of what one might expect intuitively; for example, if this + /// curve has a parameter interval of `[0, 1]`, then linearly mapping the parameter domain to + /// `[0, 2]` would be performed as follows, dividing by what might be perceived as the scaling + /// factor rather than multiplying: + /// ``` + /// # use bevy_math::curve::*; + /// # let my_curve = constant_curve(1.0, 1.0); + /// let dur = my_curve.duration(); + /// let scaled_curve = my_curve.reparametrize(dur * 2.0, |t| t / 2.0); + /// ``` + /// This kind of linear remapping is provided by the convenience method + /// [`Curve::reparametrize_linear`], which requires only the desired duration for the new curve. + /// + /// # Examples + /// ``` + /// // Reverse a curve: + /// # use bevy_math::curve::*; + /// # use bevy_math::vec2; + /// # let my_curve = constant_curve(1.0, 1.0); + /// let dur = my_curve.duration(); + /// let reversed_curve = my_curve.reparametrize(dur, |t| dur - t); + /// + /// // Take a segment of a curve: + /// # let my_curve = constant_curve(1.0, 1.0); + /// let curve_segment = my_curve.reparametrize(0.5, |t| 0.5 + t); + /// + /// // Reparametrize by an easing curve: + /// # let my_curve = constant_curve(1.0, 1.0); + /// # let easing_curve = constant_curve(1.0, vec2(1.0, 1.0)); + /// let dur = my_curve.duration(); + /// let eased_curve = my_curve.reparametrize(dur, |t| easing_curve.sample(t).y); + /// ``` + /// + /// # Panics + /// Panics if `duration` is not greater than `0.0`. + fn reparametrize(self, duration: f32, f: impl Fn(f32) -> f32) -> impl Curve + where + Self: Sized, + { + assert!(duration > 0.0); + ReparamCurve { + duration, + base: self, + f, + _phantom: PhantomData, + } + } + + /// Linearly reparametrize this [`Curve`], producing a new curve whose duration is the given + /// `duration` instead of the current one. + fn reparametrize_linear(self, duration: f32) -> impl Curve + where + Self: Sized, + { + assert!(duration > 0.0); + let old_duration = self.duration(); + Curve::reparametrize(self, duration, move |t| t * (old_duration / duration)) + } + + /// Reparametrize this [`Curve`] by sampling from another curve. + fn reparametrize_by_curve(self, other: &impl Curve) -> impl Curve + where + Self: Sized, + { + self.reparametrize(other.duration(), |t| other.sample(t)) + } + + /// Create a new [`Curve`] which is the graph of this one; that is, its output includes the + /// parameter itself in the samples. For example, if this curve outputs `x` at time `t`, then + /// the produced curve will produce `(t, x)` at time `t`. + fn graph(self) -> impl Curve<(f32, T)> + where + Self: Sized, + { + GraphCurve { + base: self, + _phantom: PhantomData, + } + } + + /// Create a new [`Curve`] by joining this curve together with another. The sample at time `t` + /// in the new curve is `(x, y)`, where `x` is the sample of `self` at time `t` and `y` is the + /// sample of `other` at time `t`. The duration of the new curve is the smaller of the two + /// between `self` and `other`. + fn and(self, other: C) -> impl Curve<(T, S)> + where + Self: Sized, + S: Interpolable, + C: Curve + Sized, + { + ProductCurve { + first: self, + second: other, + _phantom: PhantomData, + } + } +} + +/// A [`Curve`] which takes a constant value over its duration. +pub struct ConstantCurve +where + T: Interpolable, +{ + duration: f32, + value: T, +} + +impl Curve for ConstantCurve +where + T: Interpolable, +{ + #[inline] + fn duration(&self) -> f32 { + self.duration + } + + #[inline] + fn sample(&self, _t: f32) -> T { + self.value.clone() + } +} + +/// A [`Curve`] defined by a function. +pub struct FunctionCurve +where + T: Interpolable, + F: Fn(f32) -> T, +{ + duration: f32, + f: F, +} + +impl Curve for FunctionCurve +where + T: Interpolable, + F: Fn(f32) -> T, +{ + #[inline] + fn duration(&self) -> f32 { + self.duration + } + + #[inline] + fn sample(&self, t: f32) -> T { + (self.f)(t) + } +} + +/// A [`Curve`] that is defined by neighbor interpolation over a set of samples. +pub struct SampleCurve +where + T: Interpolable, +{ + duration: f32, + + /// The list of samples that define this curve by interpolation. + pub samples: Vec, +} + +impl SampleCurve +where + T: Interpolable, +{ + /// Like [`Curve::map`], but with a concrete return type. + pub fn map_concrete(self, f: impl Fn(T) -> S) -> SampleCurve + where + S: Interpolable, + { + let new_samples: Vec = self.samples.into_iter().map(f).collect(); + SampleCurve { + duration: self.duration, + samples: new_samples, + } + } + + /// Like [`Curve::graph`], but with a concrete return type. + pub fn graph_concrete(self) -> SampleCurve<(f32, T)> { + let subdivisions = max(1, self.samples.len() - 1); + let step = self.duration() / subdivisions as f32; + let times: Vec = (0..self.samples.len()).map(|s| s as f32 * step).collect(); + let new_samples: Vec<(f32, T)> = times.into_iter().zip(self.samples).collect(); + SampleCurve { + duration: self.duration, + samples: new_samples, + } + } +} + +impl Curve for SampleCurve +where + T: Interpolable, +{ + #[inline] + fn duration(&self) -> f32 { + self.duration + } + + #[inline] + fn sample(&self, t: f32) -> T { + let num_samples = self.samples.len(); + // If there is only one sample, then we return the single sample point. We also clamp `t` + // to `[0, self.duration]` here. + if num_samples == 1 || t <= 0.0 { + return self.samples[0].clone(); + } + if t >= self.duration { + return self.samples[self.samples.len() - 1].clone(); + } + + // Inside the curve itself, interpolate between the two nearest sample values. + let subdivs = num_samples - 1; + let step = self.duration / subdivs as f32; + let lower_index = (t / step).floor() as usize; + let upper_index = (t / step).ceil() as usize; + let f = (t / step).fract(); + self.samples[lower_index].interpolate(&self.samples[upper_index], f) + } + + fn map(self, f: impl Fn(T) -> S) -> impl Curve + where + Self: Sized, + S: Interpolable, + { + self.map_concrete(f) + } + + fn graph(self) -> impl Curve<(f32, T)> + where + Self: Sized, + { + self.graph_concrete() + } +} + +/// A [`Curve`] that is defined by interpolation over unevenly spaced samples. +pub struct UnevenSampleCurve +where + T: Interpolable, +{ + timed_samples: Vec<(f32, T)>, +} + +impl UnevenSampleCurve +where + T: Interpolable, +{ + /// Like [`Curve::map`], but with a concrete return type.. + pub fn map_concrete(self, f: impl Fn(T) -> S) -> UnevenSampleCurve + where + S: Interpolable, + { + let new_samples: Vec<(f32, S)> = self + .timed_samples + .into_iter() + .map(|(t, x)| (t, f(x))) + .collect(); + UnevenSampleCurve { + timed_samples: new_samples, + } + } + + /// Like [`Curve::graph`], but with a concrete return type. + pub fn graph_concrete(self) -> UnevenSampleCurve<(f32, T)> { + let new_samples: Vec<(f32, (f32, T))> = self + .timed_samples + .into_iter() + .map(|(t, x)| (t, (t, x))) + .collect(); + UnevenSampleCurve { + timed_samples: new_samples, + } + } +} + +impl Curve for UnevenSampleCurve +where + T: Interpolable, +{ + #[inline] + fn duration(&self) -> f32 { + self.timed_samples.last().unwrap().0 + } + + #[inline] + fn sample(&self, t: f32) -> T { + match self + .timed_samples + .binary_search_by(|(pt, _)| pt.partial_cmp(&t).unwrap()) + { + Ok(index) => self.timed_samples[index].1.clone(), + Err(index) => { + if index == 0 { + self.timed_samples.first().unwrap().1.clone() + } else if index == self.timed_samples.len() { + self.timed_samples.last().unwrap().1.clone() + } else { + let (t_lower, v_lower) = self.timed_samples.get(index - 1).unwrap(); + let (t_upper, v_upper) = self.timed_samples.get(index).unwrap(); + let s = (t - t_lower) / (t_upper - t_lower); + v_lower.interpolate(v_upper, s) + } + } + } + } + + fn map(self, f: impl Fn(T) -> S) -> impl Curve + where + Self: Sized, + S: Interpolable, + { + self.map_concrete(f) + } + + fn graph(self) -> impl Curve<(f32, T)> + where + Self: Sized, + { + self.graph_concrete() + } +} + +/// A [`Curve`] whose samples are defined by mapping samples from another curve through a +/// given function. +pub struct MapCurve +where + S: Interpolable, + T: Interpolable, + C: Curve, + F: Fn(S) -> T, +{ + preimage: C, + f: F, + _phantom: PhantomData<(S, T)>, +} + +impl Curve for MapCurve +where + S: Interpolable, + T: Interpolable, + C: Curve, + F: Fn(S) -> T, +{ + #[inline] + fn duration(&self) -> f32 { + self.preimage.duration() + } + + #[inline] + fn sample(&self, t: f32) -> T { + (self.f)(self.preimage.sample(t)) + } +} + +/// A [`Curve`] whose sample space is mapped onto that of some base curve's before sampling. +pub struct ReparamCurve +where + T: Interpolable, + C: Curve, + F: Fn(f32) -> f32, +{ + duration: f32, + base: C, + f: F, + _phantom: PhantomData, +} + +impl Curve for ReparamCurve +where + T: Interpolable, + C: Curve, + F: Fn(f32) -> f32, +{ + #[inline] + fn duration(&self) -> f32 { + self.duration + } + + #[inline] + fn sample(&self, t: f32) -> T { + self.base.sample((self.f)(t)) + } +} + +/// A [`Curve`] that is the graph of another curve over its parameter space. +pub struct GraphCurve +where + T: Interpolable, + C: Curve, +{ + base: C, + _phantom: PhantomData, +} + +impl Curve<(f32, T)> for GraphCurve +where + T: Interpolable, + C: Curve, +{ + #[inline] + fn duration(&self) -> f32 { + self.base.duration() + } + + #[inline] + fn sample(&self, t: f32) -> (f32, T) { + (t, self.base.sample(t)) + } +} + +/// A [`Curve`] that combines the data from two constituent curves into a tuple output type. +pub struct ProductCurve +where + S: Interpolable, + T: Interpolable, + C: Curve, + D: Curve, +{ + first: C, + second: D, + _phantom: PhantomData<(S, T)>, +} + +impl Curve<(S, T)> for ProductCurve +where + S: Interpolable, + T: Interpolable, + C: Curve, + D: Curve, +{ + #[inline] + fn duration(&self) -> f32 { + f32::min(self.first.duration(), self.second.duration()) + } + + #[inline] + fn sample(&self, t: f32) -> (S, T) { + (self.first.sample(t), self.second.sample(t)) + } +} + +// Experimental stuff: + +// TODO: See how much this needs to be extended / whether it's actually useful. +// The actual point here is to give access to additional trait constraints that are +// satisfied by the output, but not guaranteed depending on the actual data +// that underpins the invoking implementation. + +// pub trait MapConcreteCurve: Curve + Serialize + DeserializeOwned +// where T: Interpolable { +// fn map_concrete(self, f: impl Fn(T) -> S) -> impl MapConcreteCurve +// where S: Interpolable; +// } + +// Library functions: + +/// Create a [`Curve`] that constantly takes the given `value` over the given `duration`. +pub fn constant_curve(duration: f32, value: T) -> impl Curve { + ConstantCurve { duration, value } +} + +/// Convert the given function `f` into a [`Curve`] with the given `duration`, sampled by +/// evaluating the function. +pub fn function_curve(duration: f32, f: F) -> impl Curve +where + T: Interpolable, + F: Fn(f32) -> T, +{ + FunctionCurve { duration, f } +} + +/// Flip a curve that outputs tuples so that the tuples are arranged the other way. +pub fn flip(curve: impl Curve<(S, T)>) -> impl Curve<(T, S)> +where + S: Interpolable, + T: Interpolable, +{ + curve.map(|(s, t)| (t, s)) +} + +/// An error indicating that the implicit function theorem algorithm failed to apply because +/// the input curve did not meet its criteria. +pub struct IftError; + +/// Given a monotone `curve`, produces the curve that it is the graph of, up to reparametrization. +/// This is an algorithmic manifestation of the implicit function theorem; it is a numerical +/// procedure which is only performed to the specified resolutions. +/// +/// The `search_resolution` dictates how many samples are taken of the input curve; linear +/// interpolation is used between these samples to estimate the inverse image. +/// +/// The `outgoing_resolution` dictates the number of samples that are used in the construction of +/// the output itself. +/// +/// The input curve must have its first x-value be `0` or an error will be returned. Furthermore, +/// if the curve is non-monotone, the output of this function may be nonsensical even if an error +/// does not occur. +pub fn ift( + curve: &impl Curve<(f32, T)>, + search_resolution: usize, + outgoing_resolution: usize, +) -> Result, IftError> +where + T: Interpolable, +{ + // The duration of the output curve is the maximum x-value of the input curve. + let (duration, _) = curve.sample(curve.duration()); + let discrete_curve = curve.resample(search_resolution); + + let subdivisions = max(1, outgoing_resolution - 1); + let step = duration / subdivisions as f32; + let times: Vec = (0..outgoing_resolution).map(|s| s as f32 * step).collect(); + + let mut values: Vec = vec![]; + for t in times { + // Find a value on the curve where the x-value is close to `t`. + match discrete_curve + .samples + .binary_search_by(|(x, _y)| x.partial_cmp(&t).unwrap()) + { + // We found an exact match in our samples (pretty unlikely). + Ok(index) => { + let y = discrete_curve.samples[index].1.clone(); + values.push(y); + } + + // We did not find an exact match, so we must interpolate. + Err(index) => { + // The value should be between `index - 1` and `index`. + // If `index` is the sample length or 0, then something went wrong; `t` is outside + // of the range of the function projection. + if index == 0 || index == search_resolution { + return Err(IftError); + } else { + let (t_lower, y_lower) = discrete_curve.samples.get(index - 1).unwrap(); + let (t_upper, y_upper) = discrete_curve.samples.get(index).unwrap(); + if t_lower >= t_upper { + return Err(IftError); + } + // Inverse lerp on projected values to interpolate the y-value. + let s = (t - t_lower) / (t_upper - t_lower); + let value = y_lower.interpolate(y_upper, s); + values.push(value); + } + } + } + } + Ok(SampleCurve { + duration, + samples: values, + }) +} diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index c98c328d1befa..7fb50a4510558 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -16,6 +16,7 @@ mod aspect_ratio; pub mod bounding; mod common_traits; pub mod cubic_splines; +pub mod curve; mod direction; mod float_ord; pub mod primitives; From 71ab763c294dee3e97bffbc6e02694753b2d7ee9 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sat, 13 Apr 2024 14:59:27 -0400 Subject: [PATCH 02/44] Added Interval and refactored to use it as the curve domain --- crates/bevy_math/src/curve.rs | 593 ++++++++++++++++++++++------------ 1 file changed, 394 insertions(+), 199 deletions(-) diff --git a/crates/bevy_math/src/curve.rs b/crates/bevy_math/src/curve.rs index c7d5e4b53852b..06af87aaeb8ec 100644 --- a/crates/bevy_math/src/curve.rs +++ b/crates/bevy_math/src/curve.rs @@ -1,10 +1,100 @@ -//! Houses the [`Curve`] trait together with the [`Interpolable`] trait that it depends on. +//! Houses the [`Curve`] trait together with the [`Interpolable`] trait and the [`Interval`] +//! struct that it depends on. + +use crate::{Quat, VectorSpace}; +use std::{ + cmp::{max, max_by, min_by}, + marker::PhantomData, + ops::RangeInclusive, +}; + +/// A nonempty closed interval, possibly infinite in either direction. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +pub struct Interval { + start: f32, + end: f32, +} + +/// An error that indicates that an operation would have returned an invalid [`Interval`]. +#[derive(Debug)] +pub struct InvalidIntervalError; + +/// An error indicating that an infinite interval was used where it was inappropriate. +#[derive(Debug)] +pub struct InfiniteIntervalError; + +impl Interval { + /// Create a new [`Interval`] with the specified `start` and `end`. The interval can be infinite + /// but cannot be empty; invalid parameters will result in an error. + pub fn new(start: f32, end: f32) -> Result { + if start >= end || start.is_nan() || end.is_nan() { + Err(InvalidIntervalError) + } else { + Ok(Self { start, end }) + } + } + + /// Get the start of this interval. + #[inline] + pub fn start(self) -> f32 { + self.start + } + + /// Get the end of this interval. + #[inline] + pub fn end(self) -> f32 { + self.end + } + + /// Create an [`Interval`] by intersecting this interval with another. Returns an error if the + /// intersection would be empty (hence an invalid interval). + pub fn intersect(self, other: Interval) -> Result { + let lower = max_by(self.start, other.start, |x, y| x.partial_cmp(y).unwrap()); + let upper = min_by(self.end, other.end, |x, y| x.partial_cmp(y).unwrap()); + Self::new(lower, upper) + } + + /// Get the length of this interval. Note that the result may be infinite (`f32::INFINITY`). + #[inline] + pub fn length(self) -> f32 { + self.end - self.start + } + + /// Returns `true` if this interval is finite. + #[inline] + pub fn is_finite(self) -> bool { + self.length().is_finite() + } + + /// Returns `true` if `item` is contained in this interval. + #[inline] + pub fn contains(self, item: f32) -> bool { + (self.start..=self.end).contains(&item) + } + + /// Clamp the given `value` to lie within this interval. + #[inline] + pub fn clamp(self, value: f32) -> f32 { + value.clamp(self.start, self.end) + } -use std::{cmp::max, marker::PhantomData}; -use crate::Quat; -// use serde::{de::DeserializeOwned, Serialize}; + /// Get the linear map which maps this curve onto the `other` one. Returns an error if either + /// interval is infinite. + pub fn linear_map_to(self, other: Self) -> Result f32, InfiniteIntervalError> { + if !self.is_finite() || !other.is_finite() { + return Err(InfiniteIntervalError); + } + let scale = other.length() / self.length(); + Ok(move |x| (x - self.start) * scale + other.start) + } +} -use crate::VectorSpace; +impl TryFrom> for Interval { + type Error = InvalidIntervalError; + fn try_from(range: RangeInclusive) -> Result { + Interval::new(*range.start(), *range.end()) + } +} /// A trait for types whose values can be intermediately interpolated between two given values /// with an auxiliary parameter. @@ -42,6 +132,14 @@ impl Interpolable for Quat { } } +/// An error indicating that a resampling operation could not be performed because of +/// malformed inputs. +pub enum ResamplingError { + /// This resampling operation was not provided with enough samples to have well-formed output. + NotEnoughSamples(usize), + /// This resampling operation failed because of an unbounded interval. + InfiniteInterval(InfiniteIntervalError), +} /// A trait for a type that can represent values of type `T` parametrized over a fixed interval. /// Typical examples of this are actual geometric curves where `T: VectorSpace`, but other kinds @@ -50,43 +148,75 @@ pub trait Curve where T: Interpolable, { - /// The point at which parameter values of this curve end. That is, this curve is parametrized - /// on the interval `[0, self.duration()]`. - fn duration(&self) -> f32; + /// The interval over which this curve is parametrized. + fn domain(&self) -> Interval; /// Sample a point on this curve at the parameter value `t`, extracting the associated value. fn sample(&self, t: f32) -> T; + /// Sample a point on this curve at the parameter value `t`, returning `None` if the point is + /// outside of the curve's domain. + fn sample_checked(&self, t: f32) -> Option { + match self.domain().contains(t) { + true => Some(self.sample(t)), + false => None, + } + } + + /// Sample a point on this curve at the parameter value `t`, clamping `t` to lie inside the + /// domain of the curve. + fn sample_clamped(&self, t: f32) -> T { + let t = self.domain().clamp(t); + self.sample(t) + } + /// Resample this [`Curve`] to produce a new one that is defined by interpolation over equally - /// spaced values. A total of `samples` samples are used. - /// - /// Panics if `samples == 0`. - fn resample(&self, samples: usize) -> SampleCurve { - assert!(samples != 0); + /// spaced values. A total of `samples` samples are used, although at least two samples are + /// required in order to produce well-formed output. If fewer than two samples are provided, + /// or if this curve has an unbounded domain, then a [`ResamplingError`] is returned. + fn resample(&self, samples: usize) -> Result, ResamplingError> { + if samples < 2 { + return Err(ResamplingError::NotEnoughSamples(samples)); + } + if !self.domain().is_finite() { + return Err(ResamplingError::InfiniteInterval(InfiniteIntervalError)); + } // When `samples` is 1, we just record the starting point, and `step` doesn't matter. let subdivisions = max(1, samples - 1); - let step = self.duration() / subdivisions as f32; + let step = self.domain().length() / subdivisions as f32; let samples: Vec = (0..samples).map(|s| self.sample(s as f32 * step)).collect(); - SampleCurve { - duration: self.duration(), + Ok(SampleCurve { + domain: self.domain(), samples, - } + }) } /// Resample this [`Curve`] to produce a new one that is defined by interpolation over samples - /// taken at the given set of times. The given `sample_times` are expected to be strictly - /// increasing and nonempty. - fn resample_uneven(&self, sample_times: impl IntoIterator) -> UnevenSampleCurve { - let mut iter = sample_times.into_iter(); - let Some(first) = iter.next() else { - panic!("Empty iterator supplied") - }; - // Offset by the first element so that we get a curve starting at zero. - let first_sample = self.sample(first); - let mut timed_samples = vec![(0.0, first_sample)]; - timed_samples.extend(iter.map(|t| (t - first, self.sample(t)))); - UnevenSampleCurve { timed_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 range. + /// + /// Irredundant 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 [`UnevenSampleCurve`] stretches between the first and last + /// sample times of the iterator. + fn resample_uneven( + &self, + sample_times: impl IntoIterator, + ) -> Result, ResamplingError> { + let mut times: Vec = sample_times + .into_iter() + .filter(|t| t.is_finite() && self.domain().contains(*t)) + .collect(); + times.dedup_by(|t1, t2| (*t1).eq(t2)); + if times.len() < 2 { + return Err(ResamplingError::NotEnoughSamples(times.len())); + } + times.sort_by(|t1, t2| t1.partial_cmp(t2).unwrap()); + let timed_samples = times.into_iter().map(|t| (t, self.sample(t))).collect(); + Ok(UnevenSampleCurve { timed_samples }) } /// Create a new curve by mapping the values of this curve via a function `f`; i.e., if the @@ -106,8 +236,8 @@ where /// Create a new [`Curve`] whose parameter space is related to the parameter space of this curve /// by `f`. For each time `t`, the sample from the new curve at time `t` is the sample from - /// this curve at time `f(t)`. The given `duration` will be the duration of the new curve. The - /// function `f` is expected to take `[0, duration]` into `[0, self.duration]`. + /// this curve at time `f(t)`. The given `domain` will be the domain of the new curve. The + /// function `f` is expected to take `domain` into `self.domain()`. /// /// Note that this is the opposite of what one might expect intuitively; for example, if this /// curve has a parameter interval of `[0, 1]`, then linearly mapping the parameter domain to @@ -115,57 +245,54 @@ where /// factor rather than multiplying: /// ``` /// # use bevy_math::curve::*; - /// # let my_curve = constant_curve(1.0, 1.0); - /// let dur = my_curve.duration(); - /// let scaled_curve = my_curve.reparametrize(dur * 2.0, |t| t / 2.0); + /// let my_curve = constant_curve(interval(0.0, 1.0).unwrap(), 1.0); + /// let domain = my_curve.domain(); + /// let scaled_curve = my_curve.reparametrize(interval(0.0, 2.0).unwrap(), |t| t / 2.0); /// ``` /// This kind of linear remapping is provided by the convenience method - /// [`Curve::reparametrize_linear`], which requires only the desired duration for the new curve. + /// [`Curve::reparametrize_linear`], which requires only the desired domain for the new curve. /// /// # Examples /// ``` /// // Reverse a curve: /// # use bevy_math::curve::*; /// # use bevy_math::vec2; - /// # let my_curve = constant_curve(1.0, 1.0); - /// let dur = my_curve.duration(); - /// let reversed_curve = my_curve.reparametrize(dur, |t| dur - t); + /// let my_curve = constant_curve(interval(0.0, 1.0).unwrap(), 1.0); + /// let domain = my_curve.domain(); + /// let reversed_curve = my_curve.reparametrize(domain, |t| domain.end() - t); /// /// // Take a segment of a curve: - /// # let my_curve = constant_curve(1.0, 1.0); - /// let curve_segment = my_curve.reparametrize(0.5, |t| 0.5 + t); + /// # let my_curve = constant_curve(interval(0.0, 1.0).unwrap(), 1.0); + /// let curve_segment = my_curve.reparametrize(interval(0.0, 0.5).unwrap(), |t| 0.5 + t); /// /// // Reparametrize by an easing curve: - /// # let my_curve = constant_curve(1.0, 1.0); - /// # let easing_curve = constant_curve(1.0, vec2(1.0, 1.0)); - /// let dur = my_curve.duration(); - /// let eased_curve = my_curve.reparametrize(dur, |t| easing_curve.sample(t).y); + /// # let my_curve = constant_curve(interval(0.0, 1.0).unwrap(), 1.0); + /// # let easing_curve = constant_curve(interval(0.0, 1.0).unwrap(), vec2(1.0, 1.0)); + /// let domain = my_curve.domain(); + /// let eased_curve = my_curve.reparametrize(domain, |t| easing_curve.sample(t).y); /// ``` - /// - /// # Panics - /// Panics if `duration` is not greater than `0.0`. - fn reparametrize(self, duration: f32, f: impl Fn(f32) -> f32) -> impl Curve + fn reparametrize(self, domain: Interval, f: impl Fn(f32) -> f32) -> impl Curve where Self: Sized, { - assert!(duration > 0.0); ReparamCurve { - duration, + domain, base: self, f, _phantom: PhantomData, } } - /// Linearly reparametrize this [`Curve`], producing a new curve whose duration is the given - /// `duration` instead of the current one. - fn reparametrize_linear(self, duration: f32) -> impl Curve + /// Linearly reparametrize this [`Curve`], producing a new curve whose domain is the given + /// `domain` instead of the current one. This operation is only valid for curves with finite + /// domains; if either this curve's domain or the given `domain` is infinite, an + /// [`InfiniteIntervalError`] is returned. + fn reparametrize_linear(self, domain: Interval) -> Result, InfiniteIntervalError> where Self: Sized, { - assert!(duration > 0.0); - let old_duration = self.duration(); - Curve::reparametrize(self, duration, move |t| t * (old_duration / duration)) + let f = domain.linear_map_to(self.domain())?; + Ok(self.reparametrize(domain, f)) } /// Reparametrize this [`Curve`] by sampling from another curve. @@ -173,7 +300,7 @@ where where Self: Sized, { - self.reparametrize(other.duration(), |t| other.sample(t)) + self.reparametrize(other.domain(), |t| other.sample(t)) } /// Create a new [`Curve`] which is the graph of this one; that is, its output includes the @@ -191,28 +318,31 @@ where /// Create a new [`Curve`] by joining this curve together with another. The sample at time `t` /// in the new curve is `(x, y)`, where `x` is the sample of `self` at time `t` and `y` is the - /// sample of `other` at time `t`. The duration of the new curve is the smaller of the two - /// between `self` and `other`. - fn and(self, other: C) -> impl Curve<(T, S)> + /// sample of `other` at time `t`. The domain of the new curve is the intersection of the + /// domains of its constituents. If the domain intersection would be empty, an + /// [`InvalidIntervalError`] is returned. + fn zip(self, other: C) -> Result, InvalidIntervalError> where Self: Sized, S: Interpolable, C: Curve + Sized, { - ProductCurve { + let domain = self.domain().intersect(other.domain())?; + Ok(ProductCurve { + domain, first: self, second: other, _phantom: PhantomData, - } + }) } } -/// A [`Curve`] which takes a constant value over its duration. +/// A [`Curve`] which takes a constant value over its domain. pub struct ConstantCurve where T: Interpolable, { - duration: f32, + domain: Interval, value: T, } @@ -221,8 +351,8 @@ where T: Interpolable, { #[inline] - fn duration(&self) -> f32 { - self.duration + fn domain(&self) -> Interval { + self.domain } #[inline] @@ -232,23 +362,23 @@ where } /// A [`Curve`] defined by a function. -pub struct FunctionCurve +pub struct FunctionCurve where T: Interpolable, F: Fn(f32) -> T, { - duration: f32, + domain: Interval, f: F, } -impl Curve for FunctionCurve +impl Curve for FunctionCurve where T: Interpolable, F: Fn(f32) -> T, { #[inline] - fn duration(&self) -> f32 { - self.duration + fn domain(&self) -> Interval { + self.domain } #[inline] @@ -262,10 +392,11 @@ pub struct SampleCurve where T: Interpolable, { - duration: f32, - - /// The list of samples that define this curve by interpolation. - pub samples: Vec, + domain: Interval, + /// The samples that make up this [`SampleCurve`] by interpolation. + /// + /// Invariant: this must always have a length of at least 2. + samples: Vec, } impl SampleCurve @@ -279,19 +410,21 @@ where { let new_samples: Vec = self.samples.into_iter().map(f).collect(); SampleCurve { - duration: self.duration, + domain: self.domain, samples: new_samples, } } /// Like [`Curve::graph`], but with a concrete return type. pub fn graph_concrete(self) -> SampleCurve<(f32, T)> { - let subdivisions = max(1, self.samples.len() - 1); - let step = self.duration() / subdivisions as f32; - let times: Vec = (0..self.samples.len()).map(|s| s as f32 * step).collect(); + let subdivisions = self.samples.len() - 1; + let step = self.domain.length() / subdivisions as f32; + let times: Vec = (0..self.samples.len()) + .map(|s| self.domain.start() + (s as f32 * step)) + .collect(); let new_samples: Vec<(f32, T)> = times.into_iter().zip(self.samples).collect(); SampleCurve { - duration: self.duration, + domain: self.domain, samples: new_samples, } } @@ -302,28 +435,22 @@ where T: Interpolable, { #[inline] - fn duration(&self) -> f32 { - self.duration + fn domain(&self) -> Interval { + self.domain } #[inline] fn sample(&self, t: f32) -> T { - let num_samples = self.samples.len(); - // If there is only one sample, then we return the single sample point. We also clamp `t` - // to `[0, self.duration]` here. - if num_samples == 1 || t <= 0.0 { - return self.samples[0].clone(); - } - if t >= self.duration { - return self.samples[self.samples.len() - 1].clone(); - } + // We clamp `t` to the domain. + let t = self.domain.clamp(t); // Inside the curve itself, interpolate between the two nearest sample values. - let subdivs = num_samples - 1; - let step = self.duration / subdivs as f32; - let lower_index = (t / step).floor() as usize; - let upper_index = (t / step).ceil() as usize; - let f = (t / step).fract(); + let subdivs = self.samples.len() - 1; + let step = self.domain.length() / subdivs as f32; + let t_shifted = t - self.domain.start(); + let lower_index = (t_shifted / step).floor() as usize; + let upper_index = (t_shifted / step).ceil() as usize; + let f = (t_shifted / step).fract(); self.samples[lower_index].interpolate(&self.samples[upper_index], f) } @@ -348,6 +475,10 @@ pub struct UnevenSampleCurve where T: Interpolable, { + /// The timed that make up this [`UnevenSampleCurve`] by interpolation. + /// + /// Invariants: this must always have a length of at least 2, be sorted by time, and have no + /// duplicated or non-finite times. timed_samples: Vec<(f32, T)>, } @@ -381,6 +512,20 @@ where timed_samples: new_samples, } } + + /// This [`UnevenSampleCurve`], 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 resorted 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(mut self, f: impl Fn(f32) -> f32) -> UnevenSampleCurve { + self.timed_samples.iter_mut().for_each(|(t, _)| *t = f(*t)); + self.timed_samples.dedup_by(|(t1, _), (t2, _)| (*t1).eq(t2)); + self.timed_samples + .sort_by(|(t1, _), (t2, _)| t1.partial_cmp(t2).unwrap()); + self + } } impl Curve for UnevenSampleCurve @@ -388,8 +533,10 @@ where T: Interpolable, { #[inline] - fn duration(&self) -> f32 { - self.timed_samples.last().unwrap().0 + fn domain(&self) -> Interval { + let start = self.timed_samples.first().unwrap().0; + let end = self.timed_samples.last().unwrap().0; + Interval::new(start, end).unwrap() } #[inline] @@ -452,14 +599,41 @@ where F: Fn(S) -> T, { #[inline] - fn duration(&self) -> f32 { - self.preimage.duration() + fn domain(&self) -> Interval { + self.preimage.domain() } #[inline] fn sample(&self, t: f32) -> T { (self.f)(self.preimage.sample(t)) } + + // Specialized implementation of [`Curve::map`] that reuses data. + fn map(self, g: impl Fn(T) -> R) -> impl Curve + where + Self: Sized, + R: Interpolable, + { + let gf = move |x| g((self.f)(x)); + MapCurve { + preimage: self.preimage, + f: gf, + _phantom: PhantomData, + } + } + + fn reparametrize(self, domain: Interval, g: impl Fn(f32) -> f32) -> impl Curve + where + Self: Sized, + { + MapReparamCurve { + reparam_domain: domain, + base: self.preimage, + forward_map: self.f, + reparam_map: g, + _phantom: PhantomData, + } + } } /// A [`Curve`] whose sample space is mapped onto that of some base curve's before sampling. @@ -469,7 +643,7 @@ where C: Curve, F: Fn(f32) -> f32, { - duration: f32, + domain: Interval, base: C, f: F, _phantom: PhantomData, @@ -482,14 +656,110 @@ where F: Fn(f32) -> f32, { #[inline] - fn duration(&self) -> f32 { - self.duration + fn domain(&self) -> Interval { + self.domain } #[inline] fn sample(&self, t: f32) -> T { self.base.sample((self.f)(t)) } + + // Specialized implementation of [`Curve::reparametrize`] that reuses data. + fn reparametrize(self, domain: Interval, g: impl Fn(f32) -> f32) -> impl Curve + where + Self: Sized, + { + let fg = move |t| (self.f)(g(t)); + ReparamCurve { + domain, + base: self.base, + f: fg, + _phantom: PhantomData, + } + } + + fn map(self, g: impl Fn(T) -> S) -> impl Curve + where + Self: Sized, + S: Interpolable, + { + MapReparamCurve { + reparam_domain: self.domain, + base: self.base, + forward_map: g, + reparam_map: self.f, + _phantom: PhantomData, + } + } +} + +/// A [`Curve`] structure that holds both forward and backward remapping information +/// in order to optimize repeated calls of [`Curve::map`] and [`Curve::reparametrize`]. +/// +/// Briefly, the point is that the curve just absorbs new functions instead of rebasing +/// itself inside new structs. +pub struct MapReparamCurve +where + S: Interpolable, + T: Interpolable, + C: Curve, + F: Fn(S) -> T, + G: Fn(f32) -> f32, +{ + reparam_domain: Interval, + base: C, + forward_map: F, + reparam_map: G, + _phantom: PhantomData<(S, T)>, +} + +impl Curve for MapReparamCurve +where + S: Interpolable, + T: Interpolable, + C: Curve, + F: Fn(S) -> T, + G: Fn(f32) -> f32, +{ + #[inline] + fn domain(&self) -> Interval { + self.reparam_domain + } + + #[inline] + fn sample(&self, t: f32) -> T { + (self.forward_map)(self.base.sample((self.reparam_map)(t))) + } + + fn map(self, g: impl Fn(T) -> R) -> impl Curve + where + Self: Sized, + R: Interpolable, + { + let gf = move |x| g((self.forward_map)(x)); + MapReparamCurve { + reparam_domain: self.reparam_domain, + base: self.base, + forward_map: gf, + reparam_map: self.reparam_map, + _phantom: PhantomData, + } + } + + fn reparametrize(self, domain: Interval, g: impl Fn(f32) -> f32) -> impl Curve + where + Self: Sized, + { + let fg = move |t| (self.reparam_map)(g(t)); + MapReparamCurve { + reparam_domain: domain, + base: self.base, + forward_map: self.forward_map, + reparam_map: fg, + _phantom: PhantomData, + } + } } /// A [`Curve`] that is the graph of another curve over its parameter space. @@ -508,8 +778,8 @@ where C: Curve, { #[inline] - fn duration(&self) -> f32 { - self.base.duration() + fn domain(&self) -> Interval { + self.base.domain() } #[inline] @@ -526,6 +796,7 @@ where C: Curve, D: Curve, { + domain: Interval, first: C, second: D, _phantom: PhantomData<(S, T)>, @@ -539,8 +810,8 @@ where D: Curve, { #[inline] - fn duration(&self) -> f32 { - f32::min(self.first.duration(), self.second.duration()) + fn domain(&self) -> Interval { + self.domain } #[inline] @@ -549,34 +820,31 @@ where } } -// Experimental stuff: - -// TODO: See how much this needs to be extended / whether it's actually useful. -// The actual point here is to give access to additional trait constraints that are -// satisfied by the output, but not guaranteed depending on the actual data -// that underpins the invoking implementation. +// Library functions: -// pub trait MapConcreteCurve: Curve + Serialize + DeserializeOwned -// where T: Interpolable { -// fn map_concrete(self, f: impl Fn(T) -> S) -> impl MapConcreteCurve -// where S: Interpolable; -// } +/// Create an [`Interval`] with a given `start` and `end`. Alias of [`Interval::new`]. +pub fn interval(start: f32, end: f32) -> Result { + Interval::new(start, end) +} -// Library functions: +/// The [`Interval`] from negative infinity to infinity. +pub fn everywhere() -> Interval { + Interval::new(f32::NEG_INFINITY, f32::INFINITY).unwrap() +} -/// Create a [`Curve`] that constantly takes the given `value` over the given `duration`. -pub fn constant_curve(duration: f32, value: T) -> impl Curve { - ConstantCurve { duration, value } +/// Create a [`Curve`] that constantly takes the given `value` over the given `domain`. +pub fn constant_curve(domain: Interval, value: T) -> impl Curve { + ConstantCurve { domain, value } } -/// Convert the given function `f` into a [`Curve`] with the given `duration`, sampled by +/// Convert the given function `f` into a [`Curve`] with the given `domain`, sampled by /// evaluating the function. -pub fn function_curve(duration: f32, f: F) -> impl Curve -where +pub fn function_curve(domain: Interval, f: F) -> impl Curve +where T: Interpolable, F: Fn(f32) -> T, { - FunctionCurve { duration, f } + FunctionCurve { domain, f } } /// Flip a curve that outputs tuples so that the tuples are arranged the other way. @@ -587,76 +855,3 @@ where { curve.map(|(s, t)| (t, s)) } - -/// An error indicating that the implicit function theorem algorithm failed to apply because -/// the input curve did not meet its criteria. -pub struct IftError; - -/// Given a monotone `curve`, produces the curve that it is the graph of, up to reparametrization. -/// This is an algorithmic manifestation of the implicit function theorem; it is a numerical -/// procedure which is only performed to the specified resolutions. -/// -/// The `search_resolution` dictates how many samples are taken of the input curve; linear -/// interpolation is used between these samples to estimate the inverse image. -/// -/// The `outgoing_resolution` dictates the number of samples that are used in the construction of -/// the output itself. -/// -/// The input curve must have its first x-value be `0` or an error will be returned. Furthermore, -/// if the curve is non-monotone, the output of this function may be nonsensical even if an error -/// does not occur. -pub fn ift( - curve: &impl Curve<(f32, T)>, - search_resolution: usize, - outgoing_resolution: usize, -) -> Result, IftError> -where - T: Interpolable, -{ - // The duration of the output curve is the maximum x-value of the input curve. - let (duration, _) = curve.sample(curve.duration()); - let discrete_curve = curve.resample(search_resolution); - - let subdivisions = max(1, outgoing_resolution - 1); - let step = duration / subdivisions as f32; - let times: Vec = (0..outgoing_resolution).map(|s| s as f32 * step).collect(); - - let mut values: Vec = vec![]; - for t in times { - // Find a value on the curve where the x-value is close to `t`. - match discrete_curve - .samples - .binary_search_by(|(x, _y)| x.partial_cmp(&t).unwrap()) - { - // We found an exact match in our samples (pretty unlikely). - Ok(index) => { - let y = discrete_curve.samples[index].1.clone(); - values.push(y); - } - - // We did not find an exact match, so we must interpolate. - Err(index) => { - // The value should be between `index - 1` and `index`. - // If `index` is the sample length or 0, then something went wrong; `t` is outside - // of the range of the function projection. - if index == 0 || index == search_resolution { - return Err(IftError); - } else { - let (t_lower, y_lower) = discrete_curve.samples.get(index - 1).unwrap(); - let (t_upper, y_upper) = discrete_curve.samples.get(index).unwrap(); - if t_lower >= t_upper { - return Err(IftError); - } - // Inverse lerp on projected values to interpolate the y-value. - let s = (t - t_lower) / (t_upper - t_lower); - let value = y_lower.interpolate(y_upper, s); - values.push(value); - } - } - } - } - Ok(SampleCurve { - duration, - samples: values, - }) -} From df4629c98f45c3657708735a6c4a11c9e588b30e Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Thu, 18 Apr 2024 09:19:52 -0400 Subject: [PATCH 03/44] Ensured object safety, added Deref blanket impl supported by method by_ref --- crates/bevy_math/src/curve.rs | 56 +++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/crates/bevy_math/src/curve.rs b/crates/bevy_math/src/curve.rs index 06af87aaeb8ec..016a79232b5f2 100644 --- a/crates/bevy_math/src/curve.rs +++ b/crates/bevy_math/src/curve.rs @@ -5,7 +5,7 @@ use crate::{Quat, VectorSpace}; use std::{ cmp::{max, max_by, min_by}, marker::PhantomData, - ops::RangeInclusive, + ops::{Deref, RangeInclusive}, }; /// A nonempty closed interval, possibly infinite in either direction. @@ -134,6 +134,7 @@ impl Interpolable for Quat { /// An error indicating that a resampling operation could not be performed because of /// malformed inputs. +#[derive(Debug)] // TODO: Make this an actual Error. pub enum ResamplingError { /// This resampling operation was not provided with enough samples to have well-formed output. NotEnoughSamples(usize), @@ -205,7 +206,9 @@ where fn resample_uneven( &self, sample_times: impl IntoIterator, - ) -> Result, ResamplingError> { + ) -> Result, ResamplingError> + where Self: Sized + { let mut times: Vec = sample_times .into_iter() .filter(|t| t.is_finite() && self.domain().contains(*t)) @@ -335,6 +338,40 @@ where _phantom: PhantomData, }) } + + /// 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 + /// access to the original curve. + /// + /// # Example + /// ``` + /// # use bevy_math::curve::*; + /// 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 + /// // ownership of its input. + /// let samples = my_curve.by_ref().map(|x| x * 2.0).resample(100).unwrap(); + /// // Do something else with `my_curve` since we retained ownership: + /// let new_curve = my_curve.reparametrize_linear(interval(-1.0, 1.0).unwrap()).unwrap(); + /// ``` + fn by_ref(&self) -> &Self + where Self: Sized { + self + } +} + +impl Curve for D +where + T: Interpolable, + C: Curve + ?Sized, + D: Deref, +{ + fn domain(&self) -> Interval { + >::domain(self) + } + + fn sample(&self, t: f32) -> T { + >::sample(self, t) + } } /// A [`Curve`] which takes a constant value over its domain. @@ -855,3 +892,18 @@ where { curve.map(|(s, t)| (t, s)) } + +#[test] +fn my_test() { + let my_curve = function_curve((0.0..=1.0).try_into().unwrap(), |t| t * t + 1.0); + let samples = my_curve.by_ref().map(|x| x * 2.0).resample(100).unwrap(); + let new_curve = my_curve.map(|x| x * x); + println!("samples: {:?}", samples.samples); +} + +#[test] +fn another_test() { + let boxed_curve: Box> = Box::new(function_curve(everywhere(), |t| t * t)); + println!("size: {:?}", std::mem::size_of::> >()); + let mapped = boxed_curve.map(|x| 2.0 * x); +} \ No newline at end of file From ded34c23a6a73ad5fd81c836fd33ff076dad694b Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Thu, 18 Apr 2024 11:02:07 -0400 Subject: [PATCH 04/44] Refactored interval steps to a dedicated function --- crates/bevy_math/src/curve.rs | 58 ++++++++++++++++------------------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/crates/bevy_math/src/curve.rs b/crates/bevy_math/src/curve.rs index 016a79232b5f2..b7f4e0c4b0c5f 100644 --- a/crates/bevy_math/src/curve.rs +++ b/crates/bevy_math/src/curve.rs @@ -3,7 +3,7 @@ use crate::{Quat, VectorSpace}; use std::{ - cmp::{max, max_by, min_by}, + cmp::{max_by, min_by}, marker::PhantomData, ops::{Deref, RangeInclusive}, }; @@ -87,6 +87,16 @@ impl Interval { let scale = other.length() / self.length(); Ok(move |x| (x - self.start) * scale + other.start) } + + /// Get an iterator over `points` equally-spaced points from this interval in increasing order. + /// Returns `None` if `points` is less than 2; the spaced points always include the endpoints. + pub fn spaced_points(self, points: usize) -> Option> { + if points < 2 { + return None; + } + let step = self.length() / (points - 1) as f32; + Some((0..points).map(move |x| self.start + x as f32 * step)) + } } impl TryFrom> for Interval { @@ -183,10 +193,12 @@ where return Err(ResamplingError::InfiniteInterval(InfiniteIntervalError)); } - // When `samples` is 1, we just record the starting point, and `step` doesn't matter. - let subdivisions = max(1, samples - 1); - let step = self.domain().length() / subdivisions as f32; - let samples: Vec = (0..samples).map(|s| self.sample(s as f32 * step)).collect(); + let samples: Vec = self + .domain() + .spaced_points(samples) + .unwrap() + .map(|t| self.sample(t)) + .collect(); Ok(SampleCurve { domain: self.domain(), samples, @@ -206,8 +218,9 @@ where fn resample_uneven( &self, sample_times: impl IntoIterator, - ) -> Result, ResamplingError> - where Self: Sized + ) -> Result, ResamplingError> + where + Self: Sized, { let mut times: Vec = sample_times .into_iter() @@ -342,19 +355,21 @@ where /// 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 /// access to the original curve. - /// + /// /// # Example /// ``` /// # use bevy_math::curve::*; /// 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 /// // ownership of its input. /// let samples = my_curve.by_ref().map(|x| x * 2.0).resample(100).unwrap(); /// // Do something else with `my_curve` since we retained ownership: /// let new_curve = my_curve.reparametrize_linear(interval(-1.0, 1.0).unwrap()).unwrap(); /// ``` fn by_ref(&self) -> &Self - where Self: Sized { + where + Self: Sized, + { self } } @@ -454,12 +469,8 @@ where /// Like [`Curve::graph`], but with a concrete return type. pub fn graph_concrete(self) -> SampleCurve<(f32, T)> { - let subdivisions = self.samples.len() - 1; - let step = self.domain.length() / subdivisions as f32; - let times: Vec = (0..self.samples.len()) - .map(|s| self.domain.start() + (s as f32 * step)) - .collect(); - let new_samples: Vec<(f32, T)> = times.into_iter().zip(self.samples).collect(); + let times = self.domain().spaced_points(self.samples.len()).unwrap(); + let new_samples: Vec<(f32, T)> = times.zip(self.samples).collect(); SampleCurve { domain: self.domain, samples: new_samples, @@ -892,18 +903,3 @@ where { curve.map(|(s, t)| (t, s)) } - -#[test] -fn my_test() { - let my_curve = function_curve((0.0..=1.0).try_into().unwrap(), |t| t * t + 1.0); - let samples = my_curve.by_ref().map(|x| x * 2.0).resample(100).unwrap(); - let new_curve = my_curve.map(|x| x * x); - println!("samples: {:?}", samples.samples); -} - -#[test] -fn another_test() { - let boxed_curve: Box> = Box::new(function_curve(everywhere(), |t| t * t)); - println!("size: {:?}", std::mem::size_of::> >()); - let mapped = boxed_curve.map(|x| 2.0 * x); -} \ No newline at end of file From f0abd384114c93c21baa2a8675bbc21e5e64cb02 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 22 Apr 2024 07:26:51 -0400 Subject: [PATCH 05/44] Comment change --- crates/bevy_math/src/curve.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_math/src/curve.rs b/crates/bevy_math/src/curve.rs index b7f4e0c4b0c5f..4ceb80f2ce338 100644 --- a/crates/bevy_math/src/curve.rs +++ b/crates/bevy_math/src/curve.rs @@ -25,7 +25,7 @@ pub struct InfiniteIntervalError; impl Interval { /// Create a new [`Interval`] with the specified `start` and `end`. The interval can be infinite - /// but cannot be empty; invalid parameters will result in an error. + /// but cannot be empty and neither endpoint can be NaN; invalid parameters will result in an error. pub fn new(start: f32, end: f32) -> Result { if start >= end || start.is_nan() || end.is_nan() { Err(InvalidIntervalError) From 33e12e3f5e686aafe355896a78b42a24598f1291 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Thu, 25 Apr 2024 18:17:58 -0400 Subject: [PATCH 06/44] Curve replacements for the parts of VariableCurve --- crates/bevy_animation/src/curves.rs | 384 ++++++++++++++++++++++++++ crates/bevy_animation/src/lib.rs | 1 + crates/bevy_math/src/cubic_splines.rs | 2 +- crates/bevy_math/src/curve.rs | 35 +++ 4 files changed, 421 insertions(+), 1 deletion(-) create mode 100644 crates/bevy_animation/src/curves.rs diff --git a/crates/bevy_animation/src/curves.rs b/crates/bevy_animation/src/curves.rs new file mode 100644 index 0000000000000..3c294f0c6b8e7 --- /dev/null +++ b/crates/bevy_animation/src/curves.rs @@ -0,0 +1,384 @@ +use bevy_math::{ + cubic_splines::{CubicGenerator, CubicHermite}, + curve::{interval, ConstantCurve, Curve, Interpolable, Interval, UnevenSampleCurve}, + FloatExt, Quat, Vec3, Vec4, VectorSpace, +}; + +/// A wrapper struct that gives the enclosed type the property of being [`Interpolable`] with +/// naïve step interpolation. `self.interpolate(other, t)` is such that `self` is returned when +/// `t` is less than `1.0`, while `other` is returned for values `1.0` and greater. +#[derive(Clone, Copy)] +pub struct Stepped(pub T) +where + T: Clone; + +impl Interpolable for Stepped { + fn interpolate(&self, other: &Self, t: f32) -> Self { + if t < 1.0 { + self.clone() + } else { + other.clone() + } + } +} + +/// A struct that wraps a vector space type together with data needed for cubic spline (Hermite) +/// interpolation. The resulting type is [`Interpolable`], with the interior position and velocity +/// between adjacent points determined by the Hermite spline connecting them. +/// +/// Note that outside of the interval `[0, 1]`, this uses global extrapolation based on the +/// out-tangent of the left-hand point and the in-tangent of the right-hand point. +#[derive(Clone, Copy)] +pub struct TwoSidedHermite { + /// The position of the datum in space. + pub point: V, + + /// The incoming tangent vector used for interpolation. + pub in_tangent: V, + + /// The outgoing tangent vector used for interpolation. + pub out_tangent: V, +} + +impl Interpolable for TwoSidedHermite +where + V: VectorSpace, +{ + fn interpolate(&self, other: &Self, t: f32) -> Self { + let control_points = [self.point, other.point]; + let tangents = [self.out_tangent, other.in_tangent]; + // We should probably have a better way of constructing these... + let curve_segment = CubicHermite::new(control_points, tangents) + .to_curve() + .segments()[0]; + // (For interior points, the in-tangents and out-tangents are just the tangent.) + Self { + point: curve_segment.position(t), + in_tangent: curve_segment.velocity(t), + out_tangent: curve_segment.velocity(t), + } + } +} + +// Pie in the sky: `TranslationCurve` is basically the same thing as a `Box>` etc. + +/// A curve specifying the translation component of a [`Transform`] in animation. The variants are +/// broken down by interpolation mode (with the exception of `Constant`, which never interpolates). +/// +/// This type is, itself, a `Curve`, and it internally uses the provided sampling modes; +/// however, linear interpolation is intrinsic to `Vec3` itself, so the interpolation metadata +/// itself will be lost if the curve is resampled. On the other hand, the variant curves each +/// properly know their own modes of interpolation. +pub enum TranslationCurve { + /// A curve which takes a constant value over its domain. Notably, this is how animations with + /// only a single keyframe are interpreted. + Constant(ConstantCurve), + + /// A curve which interpolates linearly between keyframes. + Linear(UnevenSampleCurve), + + /// A curve which interpolates between keyframes in steps. + Step(UnevenSampleCurve>), + + /// 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(UnevenSampleCurve>), +} + +impl Curve for TranslationCurve { + fn domain(&self) -> Interval { + match self { + TranslationCurve::Constant(c) => c.domain(), + TranslationCurve::Linear(c) => c.domain(), + TranslationCurve::Step(c) => c.domain(), + TranslationCurve::CubicSpline(c) => c.domain(), + } + } + + fn sample(&self, t: f32) -> Vec3 { + match self { + TranslationCurve::Constant(c) => c.sample(t), + TranslationCurve::Linear(c) => c.sample(t), + TranslationCurve::Step(c) => c.sample(t).0, + TranslationCurve::CubicSpline(c) => c.map(|x| x.point).sample(t), + } + } +} + +/// A curve specifying the scale component of a [`Transform`] in animation. The variants are +/// broken down by interpolation mode (with the exception of `Constant`, which never interpolates). +/// +/// This type is, itself, a `Curve`, and it internally uses the provided sampling modes; +/// however, linear interpolation is intrinsic to `Vec3` itself, so the interpolation metadata +/// itself will be lost if the curve is resampled. On the other hand, the variant curves each +/// properly know their own modes of interpolation. +pub enum ScaleCurve { + /// A curve which takes a constant value over its domain. Notably, this is how animations with + /// only a single keyframe are interpreted. + Constant(ConstantCurve), + + /// A curve which interpolates linearly between keyframes. + Linear(UnevenSampleCurve), + + /// A curve which interpolates between keyframes in steps. + Step(UnevenSampleCurve>), + + /// 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(UnevenSampleCurve>), +} + +impl Curve for ScaleCurve { + fn domain(&self) -> Interval { + match self { + ScaleCurve::Constant(c) => c.domain(), + ScaleCurve::Linear(c) => c.domain(), + ScaleCurve::Step(c) => c.domain(), + ScaleCurve::CubicSpline(c) => c.domain(), + } + } + + fn sample(&self, t: f32) -> Vec3 { + match self { + ScaleCurve::Constant(c) => c.sample(t), + ScaleCurve::Linear(c) => c.sample(t), + ScaleCurve::Step(c) => c.map(|x| x.0).sample(t), + ScaleCurve::CubicSpline(c) => c.map(|x| x.point).sample(t), + } + } +} + +/// A curve specifying the scale component of a [`Transform`] in animation. The variants are +/// broken down by interpolation mode (with the exception of `Constant`, which never interpolates). +/// +/// This type is, itself, a `Curve`, and it internally uses the provided sampling modes; +/// however, spherical linear interpolation is intrinsic to `Vec3` itself, so the interpolation +/// metadata itself will be lost if the curve is resampled. On the other hand, the variant curves each +/// properly know their own modes of interpolation. +pub enum RotationCurve { + /// A curve which takes a constant value over its domain. Notably, this is how animations with + /// only a single keyframe are interpreted. + Constant(ConstantCurve), + + /// A curve which uses spherical linear interpolation between keyframes. + SphericalLinear(UnevenSampleCurve), + + /// A curve which interpolates between keyframes in steps. + Step(UnevenSampleCurve>), + + /// A curve which interpolates between keyframes by using auxiliary tangent data to join + /// adjacent keyframes with a cubic Hermite spline. For quaternions, this means interpolating + /// the underlying 4-vectors, sampling, and normalizing the result. + CubicSpline(UnevenSampleCurve>), +} + +impl Curve for RotationCurve { + fn domain(&self) -> Interval { + match self { + RotationCurve::Constant(c) => c.domain(), + RotationCurve::SphericalLinear(c) => c.domain(), + RotationCurve::Step(c) => c.domain(), + RotationCurve::CubicSpline(c) => c.domain(), + } + } + + fn sample(&self, t: f32) -> Quat { + match self { + RotationCurve::Constant(c) => c.sample(t), + RotationCurve::SphericalLinear(c) => c.sample(t), + RotationCurve::Step(c) => c.map(|x| x.0).sample(t), + RotationCurve::CubicSpline(c) => { + c.map(|x| Quat::from_vec4(x.point).normalize()).sample(t) + } + } + } +} + +/// A curve for animating either a the component of a [`Transform`] (translation, rotation, scale) +/// or the [`MorphWeights`] of morph targets for a mesh. +/// +/// Each variant yields a [`Curve`] over the data that it parametrizes. +pub enum VariableCurve { + /// A [`TranslationCurve`] for animating the `translation` component of a [`Transform`]. + Translation(TranslationCurve), + + /// A [`RotationCurve`] for animating the `rotation` component of a [`Transform`]. + Rotation(RotationCurve), + + /// A [`ScaleCurve`] for animating the `scale` component of a [`Transform`]. + Scale(ScaleCurve), + + /// A [`DynamicArrayCurve`] for animating [`MorphWeights`] of a mesh. + Weights(DynamicArrayCurve), +} + +//--------------// +// EXPERIMENTAL // +//--------------// + +// Idea: Perhaps this thing can be combined explicitly with a fixed buffer to create a curve that +// does not allocate. + +/// A curve-like data structure which holds data for a list of keyframes in a number of distinct +/// "channels" equal to its `width`. This is sampled through `sample_into`, which places the data +/// into an external buffer. +pub struct DynamicArrayCurve +where + T: Interpolable, +{ + /// The times at which the keyframes are placed. There must be at least two of these, and they + /// must always be sorted in increasing order. + times: Vec, + + /// The output values. These are stored in multiples of the `width`, with each `width` of + /// indices corresponding to the outputs for a single keyframe. For instance, the indices + /// `0..self.width` in `values` correspond to the different channels for keyframe `0`. + /// + /// The length of this vector must always be `width` times that of `times`. + values: Vec, + + /// The number of channels that this data structure maintains, and therefore the factor between + /// the length of `times` and that of `values`. Must be at least `1`. + width: usize, +} + +/// An error that indicates that a [`DynamicArrayCurve`] could not be formed. +#[derive(Debug, Clone, Copy)] +pub struct DynamicArrayError; + +impl DynamicArrayCurve +where + T: Interpolable, +{ + /// Create a new [`DynamicArrayCurve`]. Produces an error in any of the following circumstances: + /// * `width` is zero. + /// * `times` has a length less than `2`. + /// * `values` has the incorrect length relative to `times`. + pub fn new( + times: impl Into>, + values: impl Into>, + width: usize, + ) -> Result { + let times: Vec = times.into(); + let values: Vec = values.into(); + + if width == 0 { + return Err(DynamicArrayError); + } + if times.len() < 2 { + return Err(DynamicArrayError); + } + if values.len() != times.len() * width { + return Err(DynamicArrayError); + } + + Ok(Self { + times, + values, + width, + }) + } + + fn find_keyframe(&self, t: f32) -> Option { + match self + .times + .binary_search_by(|pt| pt.partial_cmp(&t).unwrap()) + { + Ok(index) => { + if index >= self.times.len() - 1 { + // This is the index of the last keyframe + None + } else { + // Exact match that is not the last keyframe + Some(index) + } + } + Err(index) => { + if index == 0 { + // This is before the first keyframe + None + } else if index >= self.times.len() { + // This is after the last keyframe + None + } else { + // This is actually in the middle somewhere; we subtract 1 to return the index + // of the lower of the two keyframes + Some(index - 1) + } + } + } + } + + /// The width for this curve; i.e., the number of distinct channels sampled for each keyframe. + pub fn width(&self) -> usize { + self.width + } + + /// The interval which spans between the first and last sample times. + fn domain(&self) -> Interval { + let start = self.times.first().unwrap(); + let end = self.times.last().unwrap(); + interval(*start, *end).unwrap() + } + + /// Sample the interpolated data at time `t` into a given `buffer`. + /// + /// # Panics + /// Panics if the provided buffer is not at least as large as `width`. + pub fn sample_into(&self, buffer: &mut [T], t: f32) { + assert!(buffer.len() >= self.width); + + let t = self.domain().clamp(t); + + let Some(lower_index) = self.find_keyframe(t) else { + // After clamping, `find_keyframe` will only return None if we landed on the + // last keyframe. + let index = self.times.len() - 1; + + // Jump to where the values for the last keyframe are: + let morph_index = index * self.width; + + // Copy the values for the last keyframe into the buffer: + for offset in 0..self.width { + buffer[offset] = self.values[morph_index + offset].clone(); + } + + return; + }; + + // Get the adjacent timestamps and the lerp parameter of `t` between them: + let upper_index = lower_index + 1; + let lower_timestamp = self.times[lower_index]; + let upper_timestamp = self.times[upper_index]; + let lerp_param = f32::inverse_lerp(lower_timestamp, upper_timestamp, t); + + // The indices in `self.values` where the values actually start: + let lower_morph_index = lower_index * self.width; + let upper_morph_index = upper_index * self.width; + + // Interpolate and dump the results into the given buffer: + for offset in 0..self.width { + let lower_value = &self.values[lower_morph_index + offset]; + let upper_value = &self.values[upper_morph_index + offset]; + buffer[offset] = lower_value.interpolate(upper_value, lerp_param); + } + } +} + +// Note that the `sample` function always allocates its output, whereas `sample_into` can dump +// the sample data into an external buffer, bypassing the need to allocate. + +impl Curve> for DynamicArrayCurve +where + T: Interpolable + Default, +{ + fn domain(&self) -> Interval { + self.domain() + } + + fn sample(&self, t: f32) -> Vec { + let mut output: Vec = vec![::default(); self.width]; + self.sample_into(output.as_mut_slice(), t); + output + } +} diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 5837c86511e04..c6607b6dd860b 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -8,6 +8,7 @@ //! Animation for the game engine Bevy mod animatable; +pub mod curves; mod graph; mod transition; mod util; diff --git a/crates/bevy_math/src/cubic_splines.rs b/crates/bevy_math/src/cubic_splines.rs index c1da3184f7be8..09c4d4523d255 100644 --- a/crates/bevy_math/src/cubic_splines.rs +++ b/crates/bevy_math/src/cubic_splines.rs @@ -624,7 +624,7 @@ pub trait CubicGenerator { /// Can be evaluated as a parametric curve over the domain `[0, 1)`. /// /// Segments can be chained together to form a longer compound curve. -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct CubicSegment { coeff: [P; 4], } diff --git a/crates/bevy_math/src/curve.rs b/crates/bevy_math/src/curve.rs index 4ceb80f2ce338..c5843680313c7 100644 --- a/crates/bevy_math/src/curve.rs +++ b/crates/bevy_math/src/curve.rs @@ -3,6 +3,7 @@ use crate::{Quat, VectorSpace}; use std::{ + borrow::Cow, cmp::{max_by, min_by}, marker::PhantomData, ops::{Deref, RangeInclusive}, @@ -142,6 +143,40 @@ impl Interpolable for Quat { } } +impl Interpolable for Vec +where + T: Interpolable, +{ + fn interpolate(&self, other: &Self, t: f32) -> Self { + self.iter() + .zip(other) + .map(|(x, y)| x.interpolate(y, t)) + .collect() + } +} + +impl Interpolable for Cow<'_, [T]> +where + T: Interpolable, +{ + fn interpolate(&self, other: &Self, t: f32) -> Self { + if t <= 0.0 { + self.clone() + } else if t >= 1.0 { + other.clone() + } else { + Cow::Owned( + // Cow<'_, [T]> derefs into [T], and its owned counterpart is Vec + (*self) + .into_iter() + .zip((*other).into_iter()) + .map(|(x, y)| x.interpolate(y, t)) + .collect::>(), + ) + } + } +} + /// An error indicating that a resampling operation could not be performed because of /// malformed inputs. #[derive(Debug)] // TODO: Make this an actual Error. From 34222f7c94da63f58f166bf4c0bb7668cfab3463 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Thu, 25 Apr 2024 18:56:31 -0400 Subject: [PATCH 07/44] Added WeightsCurve, MultiCurve subtrait --- crates/bevy_animation/src/curves.rs | 52 ++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/crates/bevy_animation/src/curves.rs b/crates/bevy_animation/src/curves.rs index 3c294f0c6b8e7..66afae2bd9f38 100644 --- a/crates/bevy_animation/src/curves.rs +++ b/crates/bevy_animation/src/curves.rs @@ -1,6 +1,6 @@ use bevy_math::{ cubic_splines::{CubicGenerator, CubicHermite}, - curve::{interval, ConstantCurve, Curve, Interpolable, Interval, UnevenSampleCurve}, + curve::{interval, ConstantCurve, Curve, Interpolable, Interval, MapCurve, UnevenSampleCurve}, FloatExt, Quat, Vec3, Vec4, VectorSpace, }; @@ -193,6 +193,27 @@ impl Curve for RotationCurve { } } } +/// 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>`; however, in order to avoid allocation, it is +/// recommended to use its implementation of the [`MultiCurve`] subtrait, which allows dumping +/// cross-channel sample data into an external buffer, avoiding allocation. +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>), + + /// A curve which interpolates linearly between keyframes. + Linear(DynamicArrayCurve), + + /// A curve which interpolates between keyframes in steps. + Step(DynamicArrayCurve>), + + /// 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(DynamicArrayCurve>), +} /// A curve for animating either a the component of a [`Transform`] (translation, rotation, scale) /// or the [`MorphWeights`] of morph targets for a mesh. @@ -208,20 +229,32 @@ pub enum VariableCurve { /// A [`ScaleCurve`] for animating the `scale` component of a [`Transform`]. Scale(ScaleCurve), - /// A [`DynamicArrayCurve`] for animating [`MorphWeights`] of a mesh. - Weights(DynamicArrayCurve), + /// A [`WeightsCurve`] for animating [`MorphWeights`] of a mesh. + Weights(WeightsCurve), } +// TODO: Actually implement `MultiCurve` for this. + //--------------// // EXPERIMENTAL // //--------------// +pub trait MultiCurve: Curve> +where + T: Interpolable, +{ + fn sample_into(&self, buffer: &mut [T], t: f32); +} + +// Blanket for `MapCurve` + // Idea: Perhaps this thing can be combined explicitly with a fixed buffer to create a curve that // does not allocate. -/// A curve-like data structure which holds data for a list of keyframes in a number of distinct +/// A curve data structure which holds data for a list of keyframes in a number of distinct /// "channels" equal to its `width`. This is sampled through `sample_into`, which places the data -/// into an external buffer. +/// into an external buffer. If `T: Default`, this may also be used as a `Curve` directly, but a new +/// `Vec` will be allocated for each call, which may be undesirable. pub struct DynamicArrayCurve where T: Interpolable, @@ -382,3 +415,12 @@ where output } } + +impl MultiCurve for DynamicArrayCurve +where + T: Interpolable + Default, +{ + fn sample_into(&self, buffer: &mut [T], t: f32) { + self.sample_into(buffer, t); + } +} From ec13959945be56f58c5463749e0f055e1dbe0af3 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sat, 27 Apr 2024 06:51:16 -0400 Subject: [PATCH 08/44] Iteration on MultiCurve --- crates/bevy_animation/src/curves.rs | 171 ++++++++++++++++++++++++++-- 1 file changed, 159 insertions(+), 12 deletions(-) diff --git a/crates/bevy_animation/src/curves.rs b/crates/bevy_animation/src/curves.rs index 66afae2bd9f38..78c81a167a2ab 100644 --- a/crates/bevy_animation/src/curves.rs +++ b/crates/bevy_animation/src/curves.rs @@ -1,3 +1,5 @@ +use std::marker::PhantomData; + use bevy_math::{ cubic_splines::{CubicGenerator, CubicHermite}, curve::{interval, ConstantCurve, Curve, Interpolable, Interval, MapCurve, UnevenSampleCurve}, @@ -7,7 +9,7 @@ use bevy_math::{ /// A wrapper struct that gives the enclosed type the property of being [`Interpolable`] with /// naïve step interpolation. `self.interpolate(other, t)` is such that `self` is returned when /// `t` is less than `1.0`, while `other` is returned for values `1.0` and greater. -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Default)] pub struct Stepped(pub T) where T: Clone; @@ -28,7 +30,7 @@ impl Interpolable for Stepped { /// /// Note that outside of the interval `[0, 1]`, this uses global extrapolation based on the /// out-tangent of the left-hand point and the in-tangent of the right-hand point. -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Default)] pub struct TwoSidedHermite { /// The position of the datum in space. pub point: V, @@ -215,6 +217,42 @@ pub enum WeightsCurve { CubicSpline(DynamicArrayCurve>), } +impl Curve> for WeightsCurve { + fn domain(&self) -> Interval { + match self { + WeightsCurve::Constant(c) => c.domain(), + WeightsCurve::Linear(c) => c.domain(), + WeightsCurve::Step(c) => c.domain(), + WeightsCurve::CubicSpline(c) => c.domain(), + } + } + + fn sample(&self, t: f32) -> Vec { + match self { + WeightsCurve::Constant(c) => c.sample(t), + WeightsCurve::Linear(c) => c.sample(t), + WeightsCurve::Step(c) => c.map(|v| v.into_iter().map(|x| x.0).collect()).sample(t), + WeightsCurve::CubicSpline(c) => c + .map(|v| v.into_iter().map(|x| x.point).collect()) + .sample(t), + } + } +} + +impl MultiCurve for WeightsCurve { + fn map_sample_into(&self, t: f32, buffer: &mut [S], f: &impl Fn(f32) -> S) + where + S: Interpolable, + { + match self { + WeightsCurve::Constant(c) => todo!(), + WeightsCurve::Linear(c) => todo!(), + WeightsCurve::Step(c) => todo!(), + WeightsCurve::CubicSpline(c) => todo!(), + } + } +} + /// A curve for animating either a the component of a [`Transform`] (translation, rotation, scale) /// or the [`MorphWeights`] of morph targets for a mesh. /// @@ -233,23 +271,78 @@ pub enum VariableCurve { Weights(WeightsCurve), } -// TODO: Actually implement `MultiCurve` for this. - //--------------// // EXPERIMENTAL // //--------------// +/// A trait for a curve that takes many interpolable values simultaneously, providing a function +/// to place those values into a buffer rather than allocating while sampling. pub trait MultiCurve: Curve> where T: Interpolable, { - fn sample_into(&self, buffer: &mut [T], t: f32); + /// Sample a number of simultaneous values from this curve into a buffer. + fn sample_into(&self, t: f32, buffer: &mut [T]) { + self.map_sample_into(t, buffer, &|x| x) + } + + /// Map the collection of samples by `f` before putting them into the given buffer. + fn map_sample_into(&self, t: f32, buffer: &mut [S], f: &impl Fn(T) -> S) + where + S: Interpolable; } -// Blanket for `MapCurve` +pub struct MultiMapCurve +where + S: Interpolable, + T: Interpolable, + C: MultiCurve, + F: Fn(S) -> T, +{ + preimage: C, + f: F, + _phantom: PhantomData<(S, T)>, +} + +impl Curve> for MultiMapCurve +where + S: Interpolable, + T: Interpolable, + C: MultiCurve, + F: Fn(S) -> T, +{ + fn domain(&self) -> Interval { + self.preimage.domain() + } + + fn sample(&self, t: f32) -> Vec { + self.preimage + .sample(t) + .into_iter() + .map(|x| (self.f)(x)) + .collect() + } +} + +impl MultiCurve for MultiMapCurve +where + S: Interpolable, + T: Interpolable, + C: MultiCurve, + F: Fn(S) -> T, +{ + fn sample_into(&self, t: f32, buffer: &mut [T]) { + self.preimage.map_sample_into(t, buffer, &self.f); + } -// Idea: Perhaps this thing can be combined explicitly with a fixed buffer to create a curve that -// does not allocate. + fn map_sample_into(&self, t: f32, buffer: &mut [R], g: &impl Fn(T) -> R) + where + R: Interpolable, + { + let gf = |x| g((self.f)(x)); + self.preimage.map_sample_into(t, buffer, &gf); + } +} /// A curve data structure which holds data for a list of keyframes in a number of distinct /// "channels" equal to its `width`. This is sampled through `sample_into`, which places the data @@ -358,7 +451,7 @@ where /// /// # Panics /// Panics if the provided buffer is not at least as large as `width`. - pub fn sample_into(&self, buffer: &mut [T], t: f32) { + pub fn sample_into(&self, t: f32, buffer: &mut [T]) { assert!(buffer.len() >= self.width); let t = self.domain().clamp(t); @@ -396,6 +489,53 @@ where buffer[offset] = lower_value.interpolate(upper_value, lerp_param); } } + + /// Sample the interpolated data at time `t` into a given `buffer` after mapping it through + /// a function `f`. + /// + /// # Panics + /// Panics if the provided buffer is not at least as large as `width`. + pub fn map_sample_into(&self, t: f32, buffer: &mut [S], f: &impl Fn(T) -> S) + where + S: Interpolable, + { + assert!(buffer.len() >= self.width); + + let t = self.domain().clamp(t); + + let Some(lower_index) = self.find_keyframe(t) else { + // After clamping, `find_keyframe` will only return None if we landed on the + // last keyframe. + let index = self.times.len() - 1; + + // Jump to where the values for the last keyframe are: + let morph_index = index * self.width; + + // Copy the values for the last keyframe into the buffer: + for offset in 0..self.width { + buffer[offset] = f(self.values[morph_index + offset].clone()); + } + + return; + }; + + // Get the adjacent timestamps and the lerp parameter of `t` between them: + let upper_index = lower_index + 1; + let lower_timestamp = self.times[lower_index]; + let upper_timestamp = self.times[upper_index]; + let lerp_param = f32::inverse_lerp(lower_timestamp, upper_timestamp, t); + + // The indices in `self.values` where the values actually start: + let lower_morph_index = lower_index * self.width; + let upper_morph_index = upper_index * self.width; + + // Interpolate and dump the results into the given buffer: + for offset in 0..self.width { + let lower_value = &self.values[lower_morph_index + offset]; + let upper_value = &self.values[upper_morph_index + offset]; + buffer[offset] = f(lower_value.interpolate(upper_value, lerp_param)); + } + } } // Note that the `sample` function always allocates its output, whereas `sample_into` can dump @@ -411,7 +551,7 @@ where fn sample(&self, t: f32) -> Vec { let mut output: Vec = vec![::default(); self.width]; - self.sample_into(output.as_mut_slice(), t); + self.sample_into(t, output.as_mut_slice()); output } } @@ -420,7 +560,14 @@ impl MultiCurve for DynamicArrayCurve where T: Interpolable + Default, { - fn sample_into(&self, buffer: &mut [T], t: f32) { - self.sample_into(buffer, t); + fn sample_into(&self, t: f32, buffer: &mut [T]) { + self.sample_into(t, buffer); + } + + fn map_sample_into(&self, t: f32, buffer: &mut [S], f: &impl Fn(T) -> S) + where + S: Interpolable, + { + self.map_sample_into(t, buffer, f) } } From c7fde9e57832f4b8dea6536422b28e98f5ec2d43 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sat, 4 May 2024 16:00:57 -0400 Subject: [PATCH 09/44] Change UnevenSampleCurve to SoA --- crates/bevy_math/src/curve.rs | 118 +++++++++++++++------------------- 1 file changed, 51 insertions(+), 67 deletions(-) diff --git a/crates/bevy_math/src/curve.rs b/crates/bevy_math/src/curve.rs index 4ceb80f2ce338..4f20eea492252 100644 --- a/crates/bevy_math/src/curve.rs +++ b/crates/bevy_math/src/curve.rs @@ -231,8 +231,8 @@ where return Err(ResamplingError::NotEnoughSamples(times.len())); } times.sort_by(|t1, t2| t1.partial_cmp(t2).unwrap()); - let timed_samples = times.into_iter().map(|t| (t, self.sample(t))).collect(); - Ok(UnevenSampleCurve { timed_samples }) + let samples = times.iter().copied().map(|t| self.sample(t)).collect(); + Ok(UnevenSampleCurve { times, samples }) } /// Create a new curve by mapping the values of this curve via a function `f`; i.e., if the @@ -455,7 +455,8 @@ impl SampleCurve where T: Interpolable, { - /// Like [`Curve::map`], but with a concrete return type. + /// Like [`Curve::map`], but with a concrete return type. Unlike that function, this one is + /// not lazy, and `f` is evaluated immediately on samples to produce the result. pub fn map_concrete(self, f: impl Fn(T) -> S) -> SampleCurve where S: Interpolable, @@ -501,21 +502,6 @@ where let f = (t_shifted / step).fract(); self.samples[lower_index].interpolate(&self.samples[upper_index], f) } - - fn map(self, f: impl Fn(T) -> S) -> impl Curve - where - Self: Sized, - S: Interpolable, - { - self.map_concrete(f) - } - - fn graph(self) -> impl Curve<(f32, T)> - where - Self: Sized, - { - self.graph_concrete() - } } /// A [`Curve`] that is defined by interpolation over unevenly spaced samples. @@ -523,41 +509,42 @@ pub struct UnevenSampleCurve where T: Interpolable, { - /// The timed that make up this [`UnevenSampleCurve`] by interpolation. + /// The times for the samples of this curve. /// - /// Invariants: this must always have a length of at least 2, be sorted by time, and have no + /// Invariants: This must always have a length of at least 2, be sorted, and have no /// duplicated or non-finite times. - timed_samples: Vec<(f32, T)>, + times: Vec, + + /// The samples corresponding to the times for this curve. + /// + /// Invariants: This must always have the same length as `times`. + samples: Vec, + //timed_samples: Vec<(f32, T)>, } impl UnevenSampleCurve where T: Interpolable, { - /// Like [`Curve::map`], but with a concrete return type.. + /// Like [`Curve::map`], but with a concrete return type. Unlike that function, this one is + /// not lazy, and `f` is evaluated immediately on samples to produce the result. pub fn map_concrete(self, f: impl Fn(T) -> S) -> UnevenSampleCurve where S: Interpolable, { - let new_samples: Vec<(f32, S)> = self - .timed_samples - .into_iter() - .map(|(t, x)| (t, f(x))) - .collect(); + let new_samples: Vec = self.samples.into_iter().map(|x| f(x)).collect(); UnevenSampleCurve { - timed_samples: new_samples, + times: self.times, + samples: new_samples, } } /// Like [`Curve::graph`], but with a concrete return type. pub fn graph_concrete(self) -> UnevenSampleCurve<(f32, T)> { - let new_samples: Vec<(f32, (f32, T))> = self - .timed_samples - .into_iter() - .map(|(t, x)| (t, (t, x))) - .collect(); + let new_samples = self.times.iter().copied().zip(self.samples).collect(); UnevenSampleCurve { - timed_samples: new_samples, + times: self.times, + samples: new_samples, } } @@ -568,10 +555,16 @@ where /// The samples are resorted 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(mut self, f: impl Fn(f32) -> f32) -> UnevenSampleCurve { - self.timed_samples.iter_mut().for_each(|(t, _)| *t = f(*t)); - self.timed_samples.dedup_by(|(t1, _), (t2, _)| (*t1).eq(t2)); - self.timed_samples - .sort_by(|(t1, _), (t2, _)| t1.partial_cmp(t2).unwrap()); + let mut timed_samples: Vec<(f32, T)> = self + .times + .into_iter() + .map(|t| f(t)) + .zip(self.samples) + .collect(); + timed_samples.dedup_by(|(t1, _), (t2, _)| (*t1).eq(t2)); + timed_samples.sort_by(|(t1, _), (t2, _)| t1.partial_cmp(t2).unwrap()); + self.times = timed_samples.iter().map(|(t, _)| t).copied().collect(); + self.samples = timed_samples.into_iter().map(|(_, x)| x).collect(); self } } @@ -582,47 +575,34 @@ where { #[inline] fn domain(&self) -> Interval { - let start = self.timed_samples.first().unwrap().0; - let end = self.timed_samples.last().unwrap().0; - Interval::new(start, end).unwrap() + let start = self.times.first().unwrap(); + let end = self.times.last().unwrap(); + Interval::new(*start, *end).unwrap() } #[inline] fn sample(&self, t: f32) -> T { match self - .timed_samples - .binary_search_by(|(pt, _)| pt.partial_cmp(&t).unwrap()) + .times + .binary_search_by(|pt| pt.partial_cmp(&t).unwrap()) { - Ok(index) => self.timed_samples[index].1.clone(), + Ok(index) => self.samples[index].clone(), Err(index) => { if index == 0 { - self.timed_samples.first().unwrap().1.clone() - } else if index == self.timed_samples.len() { - self.timed_samples.last().unwrap().1.clone() + self.samples.first().unwrap().clone() + } else if index == self.times.len() { + self.samples.last().unwrap().clone() } else { - let (t_lower, v_lower) = self.timed_samples.get(index - 1).unwrap(); - let (t_upper, v_upper) = self.timed_samples.get(index).unwrap(); + let t_lower = self.times[index - 1]; + let v_lower = self.samples.get(index - 1).unwrap(); + let t_upper = self.times[index]; + let v_upper = self.samples.get(index).unwrap(); let s = (t - t_lower) / (t_upper - t_lower); - v_lower.interpolate(v_upper, s) + v_lower.interpolate(&v_upper, s) } } } } - - fn map(self, f: impl Fn(T) -> S) -> impl Curve - where - Self: Sized, - S: Interpolable, - { - self.map_concrete(f) - } - - fn graph(self) -> impl Curve<(f32, T)> - where - Self: Sized, - { - self.graph_concrete() - } } /// A [`Curve`] whose samples are defined by mapping samples from another curve through a @@ -656,7 +636,7 @@ where (self.f)(self.preimage.sample(t)) } - // Specialized implementation of [`Curve::map`] that reuses data. + #[inline] fn map(self, g: impl Fn(T) -> R) -> impl Curve where Self: Sized, @@ -670,6 +650,7 @@ where } } + #[inline] fn reparametrize(self, domain: Interval, g: impl Fn(f32) -> f32) -> impl Curve where Self: Sized, @@ -713,7 +694,7 @@ where self.base.sample((self.f)(t)) } - // Specialized implementation of [`Curve::reparametrize`] that reuses data. + #[inline] fn reparametrize(self, domain: Interval, g: impl Fn(f32) -> f32) -> impl Curve where Self: Sized, @@ -727,6 +708,7 @@ where } } + #[inline] fn map(self, g: impl Fn(T) -> S) -> impl Curve where Self: Sized, @@ -780,6 +762,7 @@ where (self.forward_map)(self.base.sample((self.reparam_map)(t))) } + #[inline] fn map(self, g: impl Fn(T) -> R) -> impl Curve where Self: Sized, @@ -795,6 +778,7 @@ where } } + #[inline] fn reparametrize(self, domain: Interval, g: impl Fn(f32) -> f32) -> impl Curve where Self: Sized, From b9e113d5951cf19e720df0d2e951c340521fd3eb Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sun, 5 May 2024 15:57:57 -0400 Subject: [PATCH 10/44] Derive Error on error types --- crates/bevy_math/src/curve.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/bevy_math/src/curve.rs b/crates/bevy_math/src/curve.rs index 4f20eea492252..aa836e59b5fa5 100644 --- a/crates/bevy_math/src/curve.rs +++ b/crates/bevy_math/src/curve.rs @@ -7,6 +7,7 @@ use std::{ marker::PhantomData, ops::{Deref, RangeInclusive}, }; +use thiserror::Error; /// A nonempty closed interval, possibly infinite in either direction. #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] @@ -16,11 +17,13 @@ pub struct Interval { } /// An error that indicates that an operation would have returned an invalid [`Interval`]. -#[derive(Debug)] +#[derive(Debug, Error)] +#[error("The resulting interval would be invalid (empty or with a NaN endpoint)")] pub struct InvalidIntervalError; /// An error indicating that an infinite interval was used where it was inappropriate. -#[derive(Debug)] +#[derive(Debug, Error)] +#[error("This operation does not make sense in the context of an infinite interval")] pub struct InfiniteIntervalError; impl Interval { @@ -88,7 +91,7 @@ impl Interval { Ok(move |x| (x - self.start) * scale + other.start) } - /// Get an iterator over `points` equally-spaced points from this interval in increasing order. + /// Get an iterator over equally-spaced points from this interval in increasing order. /// Returns `None` if `points` is less than 2; the spaced points always include the endpoints. pub fn spaced_points(self, points: usize) -> Option> { if points < 2 { @@ -144,11 +147,14 @@ impl Interpolable for Quat { /// An error indicating that a resampling operation could not be performed because of /// malformed inputs. -#[derive(Debug)] // TODO: Make this an actual Error. +#[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 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")] InfiniteInterval(InfiniteIntervalError), } From b0c5f44ba0e8acc361ac5f55c12f8b75b38d061d Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sun, 5 May 2024 16:01:43 -0400 Subject: [PATCH 11/44] Lints --- crates/bevy_math/src/curve.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/crates/bevy_math/src/curve.rs b/crates/bevy_math/src/curve.rs index aa836e59b5fa5..bef64a498a4d2 100644 --- a/crates/bevy_math/src/curve.rs +++ b/crates/bevy_math/src/curve.rs @@ -538,7 +538,7 @@ where where S: Interpolable, { - let new_samples: Vec = self.samples.into_iter().map(|x| f(x)).collect(); + let new_samples: Vec = self.samples.into_iter().map(f).collect(); UnevenSampleCurve { times: self.times, samples: new_samples, @@ -561,12 +561,8 @@ where /// The samples are resorted 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(mut self, f: impl Fn(f32) -> f32) -> UnevenSampleCurve { - let mut timed_samples: Vec<(f32, T)> = self - .times - .into_iter() - .map(|t| f(t)) - .zip(self.samples) - .collect(); + let mut timed_samples: Vec<(f32, T)> = + self.times.into_iter().map(f).zip(self.samples).collect(); timed_samples.dedup_by(|(t1, _), (t2, _)| (*t1).eq(t2)); timed_samples.sort_by(|(t1, _), (t2, _)| t1.partial_cmp(t2).unwrap()); self.times = timed_samples.iter().map(|(t, _)| t).copied().collect(); @@ -604,7 +600,7 @@ where let t_upper = self.times[index]; let v_upper = self.samples.get(index).unwrap(); let s = (t - t_lower) / (t_upper - t_lower); - v_lower.interpolate(&v_upper, s) + v_lower.interpolate(v_upper, s) } } } From 9083553979464801eb1a671196108651b30488b1 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Tue, 7 May 2024 10:45:28 -0400 Subject: [PATCH 12/44] Restrict Interpolable constraint to resampling methods --- crates/bevy_math/src/curve.rs | 49 +++++++---------------------------- 1 file changed, 10 insertions(+), 39 deletions(-) diff --git a/crates/bevy_math/src/curve.rs b/crates/bevy_math/src/curve.rs index bef64a498a4d2..64237f75cd5a3 100644 --- a/crates/bevy_math/src/curve.rs +++ b/crates/bevy_math/src/curve.rs @@ -161,10 +161,7 @@ pub enum ResamplingError { /// A trait for a type that can represent values of type `T` parametrized over a fixed interval. /// Typical examples of this are actual geometric curves where `T: VectorSpace`, but other kinds /// of interpolable data can be represented instead (or in addition). -pub trait Curve -where - T: Interpolable, -{ +pub trait Curve { /// The interval over which this curve is parametrized. fn domain(&self) -> Interval; @@ -191,7 +188,10 @@ where /// spaced values. A total of `samples` samples are used, although at least two samples are /// required in order to produce well-formed output. If fewer than two samples are provided, /// or if this curve has an unbounded domain, then a [`ResamplingError`] is returned. - fn resample(&self, samples: usize) -> Result, ResamplingError> { + fn resample(&self, samples: usize) -> Result, ResamplingError> + where + T: Interpolable, + { if samples < 2 { return Err(ResamplingError::NotEnoughSamples(samples)); } @@ -227,6 +227,7 @@ where ) -> Result, ResamplingError> where Self: Sized, + T: Interpolable, { let mut times: Vec = sample_times .into_iter() @@ -247,7 +248,6 @@ where fn map(self, f: impl Fn(T) -> S) -> impl Curve where Self: Sized, - S: Interpolable, { MapCurve { preimage: self, @@ -346,7 +346,6 @@ where fn zip(self, other: C) -> Result, InvalidIntervalError> where Self: Sized, - S: Interpolable, C: Curve + Sized, { let domain = self.domain().intersect(other.domain())?; @@ -382,7 +381,6 @@ where impl Curve for D where - T: Interpolable, C: Curve + ?Sized, D: Deref, { @@ -398,7 +396,7 @@ where /// A [`Curve`] which takes a constant value over its domain. pub struct ConstantCurve where - T: Interpolable, + T: Clone, { domain: Interval, value: T, @@ -406,7 +404,7 @@ where impl Curve for ConstantCurve where - T: Interpolable, + T: Clone, { #[inline] fn domain(&self) -> Interval { @@ -422,7 +420,6 @@ where /// A [`Curve`] defined by a function. pub struct FunctionCurve where - T: Interpolable, F: Fn(f32) -> T, { domain: Interval, @@ -431,7 +428,6 @@ where impl Curve for FunctionCurve where - T: Interpolable, F: Fn(f32) -> T, { #[inline] @@ -525,7 +521,6 @@ where /// /// Invariants: This must always have the same length as `times`. samples: Vec, - //timed_samples: Vec<(f32, T)>, } impl UnevenSampleCurve @@ -611,8 +606,6 @@ where /// given function. pub struct MapCurve where - S: Interpolable, - T: Interpolable, C: Curve, F: Fn(S) -> T, { @@ -623,8 +616,6 @@ where impl Curve for MapCurve where - S: Interpolable, - T: Interpolable, C: Curve, F: Fn(S) -> T, { @@ -642,7 +633,6 @@ where fn map(self, g: impl Fn(T) -> R) -> impl Curve where Self: Sized, - R: Interpolable, { let gf = move |x| g((self.f)(x)); MapCurve { @@ -670,7 +660,6 @@ where /// A [`Curve`] whose sample space is mapped onto that of some base curve's before sampling. pub struct ReparamCurve where - T: Interpolable, C: Curve, F: Fn(f32) -> f32, { @@ -682,7 +671,6 @@ where impl Curve for ReparamCurve where - T: Interpolable, C: Curve, F: Fn(f32) -> f32, { @@ -714,7 +702,6 @@ where fn map(self, g: impl Fn(T) -> S) -> impl Curve where Self: Sized, - S: Interpolable, { MapReparamCurve { reparam_domain: self.domain, @@ -733,8 +720,6 @@ where /// itself inside new structs. pub struct MapReparamCurve where - S: Interpolable, - T: Interpolable, C: Curve, F: Fn(S) -> T, G: Fn(f32) -> f32, @@ -748,8 +733,6 @@ where impl Curve for MapReparamCurve where - S: Interpolable, - T: Interpolable, C: Curve, F: Fn(S) -> T, G: Fn(f32) -> f32, @@ -768,7 +751,6 @@ where fn map(self, g: impl Fn(T) -> R) -> impl Curve where Self: Sized, - R: Interpolable, { let gf = move |x| g((self.forward_map)(x)); MapReparamCurve { @@ -799,7 +781,6 @@ where /// A [`Curve`] that is the graph of another curve over its parameter space. pub struct GraphCurve where - T: Interpolable, C: Curve, { base: C, @@ -808,7 +789,6 @@ where impl Curve<(f32, T)> for GraphCurve where - T: Interpolable, C: Curve, { #[inline] @@ -825,8 +805,6 @@ where /// A [`Curve`] that combines the data from two constituent curves into a tuple output type. pub struct ProductCurve where - S: Interpolable, - T: Interpolable, C: Curve, D: Curve, { @@ -838,8 +816,6 @@ where impl Curve<(S, T)> for ProductCurve where - S: Interpolable, - T: Interpolable, C: Curve, D: Curve, { @@ -867,7 +843,7 @@ pub fn everywhere() -> Interval { } /// Create a [`Curve`] that constantly takes the given `value` over the given `domain`. -pub fn constant_curve(domain: Interval, value: T) -> impl Curve { +pub fn constant_curve(domain: Interval, value: T) -> impl Curve { ConstantCurve { domain, value } } @@ -875,17 +851,12 @@ pub fn constant_curve(domain: Interval, value: T) -> impl Curve /// evaluating the function. pub fn function_curve(domain: Interval, f: F) -> impl Curve where - T: Interpolable, F: Fn(f32) -> T, { FunctionCurve { domain, f } } /// Flip a curve that outputs tuples so that the tuples are arranged the other way. -pub fn flip(curve: impl Curve<(S, T)>) -> impl Curve<(T, S)> -where - S: Interpolable, - T: Interpolable, -{ +pub fn flip(curve: impl Curve<(S, T)>) -> impl Curve<(T, S)> { curve.map(|(s, t)| (t, s)) } From 02de000a24d39c0c909b9de21083aa62e800804c Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Tue, 7 May 2024 15:11:13 -0400 Subject: [PATCH 13/44] Address early review comments --- crates/bevy_math/src/curve.rs | 52 +++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/crates/bevy_math/src/curve.rs b/crates/bevy_math/src/curve.rs index 64237f75cd5a3..20a88289be527 100644 --- a/crates/bevy_math/src/curve.rs +++ b/crates/bevy_math/src/curve.rs @@ -26,6 +26,19 @@ pub struct InvalidIntervalError; #[error("This operation does not make sense in the context of an infinite interval")] pub struct InfiniteIntervalError; +/// An error indicating that spaced points on an interval could not be formed. +#[derive(Debug, Error)] +#[error("Could not sample evenly-spaced points with these inputs")] +pub enum SpacedPointsError { + /// This operation failed because fewer than two points were requested. + #[error("Parameter `points` must be at least 2")] + NotEnoughPoints, + + /// This operation failed because the underlying interval is unbounded. + #[error("Cannot sample evenly-spaced points on an infinite interval")] + InfiniteInterval(InfiniteIntervalError), +} + impl Interval { /// Create a new [`Interval`] with the specified `start` and `end`. The interval can be infinite /// but cannot be empty and neither endpoint can be NaN; invalid parameters will result in an error. @@ -93,12 +106,18 @@ impl Interval { /// Get an iterator over equally-spaced points from this interval in increasing order. /// Returns `None` if `points` is less than 2; the spaced points always include the endpoints. - pub fn spaced_points(self, points: usize) -> Option> { + pub fn spaced_points( + self, + points: usize, + ) -> Result, SpacedPointsError> { if points < 2 { - return None; + return Err(SpacedPointsError::NotEnoughPoints); + } + if !self.is_finite() { + return Err(SpacedPointsError::InfiniteInterval(InfiniteIntervalError)); } let step = self.length() / (points - 1) as f32; - Some((0..points).map(move |x| self.start + x as f32 * step)) + Ok((0..points).map(move |x| self.start + x as f32 * step)) } } @@ -153,6 +172,7 @@ pub enum ResamplingError { /// This resampling operation was not provided with enough samples to have well-formed output. #[error("Not enough 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")] InfiniteInterval(InfiniteIntervalError), @@ -492,17 +512,27 @@ where #[inline] fn sample(&self, t: f32) -> T { - // We clamp `t` to the domain. - let t = self.domain.clamp(t); - - // Inside the curve itself, interpolate between the two nearest sample values. + // Inside the curve itself, we interpolate between the two nearest sample values. let subdivs = self.samples.len() - 1; let step = self.domain.length() / subdivs as f32; let t_shifted = t - self.domain.start(); - let lower_index = (t_shifted / step).floor() as usize; - let upper_index = (t_shifted / step).ceil() as usize; - let f = (t_shifted / step).fract(); - self.samples[lower_index].interpolate(&self.samples[upper_index], f) + let steps_taken = t_shifted / step; + + // Using `steps_taken` as the source of truth, clamp to the range of valid indices. + if steps_taken <= 0.0 { + self.samples.first().unwrap().clone() + } else if steps_taken >= (self.samples.len() - 1) as f32 { + self.samples.last().unwrap().clone() + } else { + // Here we use only the floor and the fractional part of `steps_taken` to interpolate + // between the two nearby sample points; `lower_index + 1` is known to be a valid index + // because otherwise, `steps_taken.floor()` must be at least `self.samples.len() - 1`, + // but the previous branch captures all such values. + let lower_index = steps_taken.floor() as usize; + let upper_index = lower_index + 1; + let fract = steps_taken.fract(); + self.samples[lower_index].interpolate(&self.samples[upper_index], fract) + } } } From 6ea34166c6de130f70828ba2e468f808dd4b5904 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Wed, 8 May 2024 06:49:49 -0400 Subject: [PATCH 14/44] Explicitly clamp index in UnevenSampleCurve::sample --- crates/bevy_math/src/curve.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/bevy_math/src/curve.rs b/crates/bevy_math/src/curve.rs index 20a88289be527..a0edc4e59ff74 100644 --- a/crates/bevy_math/src/curve.rs +++ b/crates/bevy_math/src/curve.rs @@ -525,10 +525,11 @@ where self.samples.last().unwrap().clone() } else { // Here we use only the floor and the fractional part of `steps_taken` to interpolate - // between the two nearby sample points; `lower_index + 1` is known to be a valid index - // because otherwise, `steps_taken.floor()` must be at least `self.samples.len() - 1`, - // but the previous branch captures all such values. + // between the two nearby sample points. let lower_index = steps_taken.floor() as usize; + + // Explicitly clamp the lower index just in case. + let lower_index = lower_index.min(self.samples.len() - 2); let upper_index = lower_index + 1; let fract = steps_taken.fract(); self.samples[lower_index].interpolate(&self.samples[upper_index], fract) From 36476b11d84c4f0f6d13143c3e3c99d580b6ac2e Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Wed, 8 May 2024 12:55:56 -0400 Subject: [PATCH 15/44] Reorganized, added some tests --- crates/bevy_math/src/curve/interpolable.rs | 39 ++ crates/bevy_math/src/curve/interval.rs | 298 ++++++++++++++++ .../bevy_math/src/{curve.rs => curve/mod.rs} | 334 +++++++++--------- 3 files changed, 496 insertions(+), 175 deletions(-) create mode 100644 crates/bevy_math/src/curve/interpolable.rs create mode 100644 crates/bevy_math/src/curve/interval.rs rename crates/bevy_math/src/{curve.rs => curve/mod.rs} (77%) diff --git a/crates/bevy_math/src/curve/interpolable.rs b/crates/bevy_math/src/curve/interpolable.rs new file mode 100644 index 0000000000000..9458e1528c8e6 --- /dev/null +++ b/crates/bevy_math/src/curve/interpolable.rs @@ -0,0 +1,39 @@ +//! The [`Interpolable`] trait for types that support interpolation between two values. + +use crate::{Quat, VectorSpace}; + +/// A trait for types whose values can be intermediately interpolated between two given values +/// with an auxiliary parameter. +pub trait Interpolable: Clone { + /// Interpolate between this value and the `other` given value using the parameter `t`. + /// Note that the parameter `t` is not necessarily clamped to lie between `0` and `1`. + fn interpolate(&self, other: &Self, t: f32) -> Self; +} + +impl Interpolable for (S, T) +where + S: Interpolable, + T: Interpolable, +{ + fn interpolate(&self, other: &Self, t: f32) -> Self { + ( + self.0.interpolate(&other.0, t), + self.1.interpolate(&other.1, t), + ) + } +} + +impl Interpolable for T +where + T: VectorSpace, +{ + fn interpolate(&self, other: &Self, t: f32) -> Self { + self.lerp(*other, t) + } +} + +impl Interpolable for Quat { + fn interpolate(&self, other: &Self, t: f32) -> Self { + self.slerp(*other, t) + } +} diff --git a/crates/bevy_math/src/curve/interval.rs b/crates/bevy_math/src/curve/interval.rs new file mode 100644 index 0000000000000..72d8993e7be1d --- /dev/null +++ b/crates/bevy_math/src/curve/interval.rs @@ -0,0 +1,298 @@ +//! The [`Interval`] type for nonempty intervals used by the [`Curve`](super::Curve) trait. + +use std::{ + cmp::{max_by, min_by}, + ops::RangeInclusive, +}; +use thiserror::Error; + +/// A nonempty closed interval, possibly infinite in either direction. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +pub struct Interval { + start: f32, + end: f32, +} + +/// An error that indicates that an operation would have returned an invalid [`Interval`]. +#[derive(Debug, Error)] +#[error("The resulting interval would be invalid (empty or with a NaN endpoint)")] +pub struct InvalidIntervalError; + +/// An error indicating that an infinite interval was used where it was inappropriate. +#[derive(Debug, Error)] +#[error("This operation does not make sense in the context of an infinite interval")] +pub struct InfiniteIntervalError; + +/// An error indicating that spaced points on an interval could not be formed. +#[derive(Debug, Error)] +#[error("Could not sample evenly-spaced points with these inputs")] +pub enum SpacedPointsError { + /// This operation failed because fewer than two points were requested. + #[error("Parameter `points` must be at least 2")] + NotEnoughPoints, + + /// This operation failed because the underlying interval is unbounded. + #[error("Cannot sample evenly-spaced points on an infinite interval")] + InfiniteInterval(InfiniteIntervalError), +} + +impl Interval { + /// Create a new [`Interval`] with the specified `start` and `end`. The interval can be infinite + /// but cannot be empty and neither endpoint can be NaN; invalid parameters will result in an error. + pub fn new(start: f32, end: f32) -> Result { + if start >= end || start.is_nan() || end.is_nan() { + Err(InvalidIntervalError) + } else { + Ok(Self { start, end }) + } + } + + /// Get the start of this interval. + #[inline] + pub fn start(self) -> f32 { + self.start + } + + /// Get the end of this interval. + #[inline] + pub fn end(self) -> f32 { + self.end + } + + /// Create an [`Interval`] by intersecting this interval with another. Returns an error if the + /// intersection would be empty (hence an invalid interval). + pub fn intersect(self, other: Interval) -> Result { + let lower = max_by(self.start, other.start, |x, y| x.partial_cmp(y).unwrap()); + let upper = min_by(self.end, other.end, |x, y| x.partial_cmp(y).unwrap()); + Self::new(lower, upper) + } + + /// Get the length of this interval. Note that the result may be infinite (`f32::INFINITY`). + #[inline] + pub fn length(self) -> f32 { + self.end - self.start + } + + /// Returns `true` if this interval is finite. + #[inline] + pub fn is_finite(self) -> bool { + self.length().is_finite() + } + + /// Returns `true` if `item` is contained in this interval. + #[inline] + pub fn contains(self, item: f32) -> bool { + (self.start..=self.end).contains(&item) + } + + /// Clamp the given `value` to lie within this interval. + #[inline] + pub fn clamp(self, value: f32) -> f32 { + value.clamp(self.start, self.end) + } + + /// Get the linear map which maps this curve onto the `other` one. Returns an error if either + /// interval is infinite. + pub fn linear_map_to(self, other: Self) -> Result f32, InfiniteIntervalError> { + if !self.is_finite() || !other.is_finite() { + return Err(InfiniteIntervalError); + } + let scale = other.length() / self.length(); + Ok(move |x| (x - self.start) * scale + other.start) + } + + /// Get an iterator over equally-spaced points from this interval in increasing order. + /// Returns `None` if `points` is less than 2; the spaced points always include the endpoints. + pub fn spaced_points( + self, + points: usize, + ) -> Result, SpacedPointsError> { + if points < 2 { + return Err(SpacedPointsError::NotEnoughPoints); + } + if !self.is_finite() { + return Err(SpacedPointsError::InfiniteInterval(InfiniteIntervalError)); + } + let step = self.length() / (points - 1) as f32; + Ok((0..points).map(move |x| self.start + x as f32 * step)) + } +} + +impl TryFrom> for Interval { + type Error = InvalidIntervalError; + fn try_from(range: RangeInclusive) -> Result { + Interval::new(*range.start(), *range.end()) + } +} + +/// Create an [`Interval`] with a given `start` and `end`. Alias of [`Interval::new`]. +pub fn interval(start: f32, end: f32) -> Result { + Interval::new(start, end) +} + +/// The [`Interval`] from negative infinity to infinity. +pub fn everywhere() -> Interval { + Interval::new(f32::NEG_INFINITY, f32::INFINITY).unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + use approx::{assert_abs_diff_eq, AbsDiffEq}; + + #[test] + fn make_intervals() { + let ivl = Interval::new(2.0, -1.0); + assert!(ivl.is_err()); + + let ivl = Interval::new(-0.0, 0.0); + assert!(ivl.is_err()); + + let ivl = Interval::new(f32::NEG_INFINITY, 15.5); + assert!(ivl.is_ok()); + + let ivl = Interval::new(-2.0, f32::INFINITY); + assert!(ivl.is_ok()); + + let ivl = Interval::new(f32::NEG_INFINITY, f32::INFINITY); + assert!(ivl.is_ok()); + + let ivl = Interval::new(f32::INFINITY, f32::NEG_INFINITY); + assert!(ivl.is_err()); + + let ivl = Interval::new(-1.0, f32::NAN); + assert!(ivl.is_err()); + + let ivl = Interval::new(f32::NAN, -42.0); + assert!(ivl.is_err()); + + let ivl = Interval::new(f32::NAN, f32::NAN); + assert!(ivl.is_err()); + + let ivl = Interval::new(0.0, 1.0); + assert!(ivl.is_ok()); + } + + #[test] + fn lengths() { + let ivl = interval(-5.0, 10.0).unwrap(); + assert!((ivl.length() - 15.0).abs() <= f32::EPSILON); + + let ivl = interval(5.0, 100.0).unwrap(); + assert!((ivl.length() - 95.0).abs() <= f32::EPSILON); + + let ivl = interval(0.0, f32::INFINITY).unwrap(); + assert_eq!(ivl.length(), f32::INFINITY); + + let ivl = interval(f32::NEG_INFINITY, 0.0).unwrap(); + assert_eq!(ivl.length(), f32::INFINITY); + + let ivl = everywhere(); + assert_eq!(ivl.length(), f32::INFINITY); + } + + #[test] + fn intersections() { + let ivl1 = interval(-1.0, 1.0).unwrap(); + let ivl2 = interval(0.0, 2.0).unwrap(); + let ivl3 = interval(-3.0, 0.0).unwrap(); + let ivl4 = interval(0.0, f32::INFINITY).unwrap(); + let ivl5 = interval(f32::NEG_INFINITY, 0.0).unwrap(); + let ivl6 = everywhere(); + + assert!(ivl1 + .intersect(ivl2) + .is_ok_and(|ivl| ivl == interval(0.0, 1.0).unwrap())); + assert!(ivl1 + .intersect(ivl3) + .is_ok_and(|ivl| ivl == interval(-1.0, 0.0).unwrap())); + assert!(ivl2.intersect(ivl3).is_err()); + assert!(ivl1 + .intersect(ivl4) + .is_ok_and(|ivl| ivl == interval(0.0, 1.0).unwrap())); + assert!(ivl1 + .intersect(ivl5) + .is_ok_and(|ivl| ivl == interval(-1.0, 0.0).unwrap())); + assert!(ivl4.intersect(ivl5).is_err()); + assert_eq!(ivl1.intersect(ivl6).unwrap(), ivl1); + assert_eq!(ivl4.intersect(ivl6).unwrap(), ivl4); + assert_eq!(ivl5.intersect(ivl6).unwrap(), ivl5); + } + + #[test] + fn containment() { + let ivl = interval(0.0, 1.0).unwrap(); + assert!(ivl.contains(0.0)); + assert!(ivl.contains(1.0)); + assert!(ivl.contains(0.5)); + assert!(!ivl.contains(-0.1)); + assert!(!ivl.contains(1.1)); + assert!(!ivl.contains(f32::NAN)); + + let ivl = interval(3.0, f32::INFINITY).unwrap(); + assert!(ivl.contains(3.0)); + assert!(ivl.contains(2.0e5)); + assert!(ivl.contains(3.5e6)); + assert!(!ivl.contains(2.5)); + assert!(!ivl.contains(-1e5)); + assert!(!ivl.contains(f32::NAN)); + } + + #[test] + fn finiteness() { + assert!(!everywhere().is_finite()); + assert!(interval(0.0, 3.5e5).unwrap().is_finite()); + assert!(!interval(-2.0, f32::INFINITY).unwrap().is_finite()); + assert!(!interval(f32::NEG_INFINITY, 5.0).unwrap().is_finite()); + } + + #[test] + fn linear_maps() { + let ivl1 = interval(-3.0, 5.0).unwrap(); + let ivl2 = interval(0.0, 1.0).unwrap(); + let map = ivl1.linear_map_to(ivl2); + assert!(map.is_ok_and(|f| f(-3.0).abs_diff_eq(&0.0, f32::EPSILON) + && f(5.0).abs_diff_eq(&1.0, f32::EPSILON) + && f(1.0).abs_diff_eq(&0.5, f32::EPSILON))); + + let ivl1 = interval(0.0, 1.0).unwrap(); + let ivl2 = everywhere(); + assert!(ivl1.linear_map_to(ivl2).is_err()); + + let ivl1 = interval(f32::NEG_INFINITY, -4.0).unwrap(); + let ivl2 = interval(0.0, 1.0).unwrap(); + assert!(ivl1.linear_map_to(ivl2).is_err()); + } + + #[test] + fn spaced_points() { + let ivl = interval(0.0, 50.0).unwrap(); + let points_iter = ivl.spaced_points(1); + assert!(points_iter.is_err()); + let points_iter: Vec = ivl.spaced_points(2).unwrap().collect(); + assert_abs_diff_eq!(points_iter[0], 0.0); + assert_abs_diff_eq!(points_iter[1], 50.0); + let points_iter = ivl.spaced_points(21).unwrap(); + let step = ivl.length() / 20.0; + for (index, point) in points_iter.enumerate() { + let expected = ivl.start() + step * index as f32; + assert_abs_diff_eq!(point, expected); + } + + let ivl = interval(-21.0, 79.0).unwrap(); + let points_iter = ivl.spaced_points(10000).unwrap(); + let step = ivl.length() / 9999.0; + for (index, point) in points_iter.enumerate() { + let expected = ivl.start() + step * index as f32; + assert_abs_diff_eq!(point, expected); + } + + let ivl = interval(-1.0, f32::INFINITY).unwrap(); + let points_iter = ivl.spaced_points(25); + assert!(points_iter.is_err()); + + let ivl = interval(f32::NEG_INFINITY, -25.0).unwrap(); + let points_iter = ivl.spaced_points(9); + assert!(points_iter.is_err()); + } +} diff --git a/crates/bevy_math/src/curve.rs b/crates/bevy_math/src/curve/mod.rs similarity index 77% rename from crates/bevy_math/src/curve.rs rename to crates/bevy_math/src/curve/mod.rs index a0edc4e59ff74..97a66eb0d3ec5 100644 --- a/crates/bevy_math/src/curve.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -1,168 +1,13 @@ -//! Houses the [`Curve`] trait together with the [`Interpolable`] trait and the [`Interval`] -//! struct that it depends on. - -use crate::{Quat, VectorSpace}; -use std::{ - cmp::{max_by, min_by}, - marker::PhantomData, - ops::{Deref, RangeInclusive}, -}; -use thiserror::Error; - -/// A nonempty closed interval, possibly infinite in either direction. -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] -pub struct Interval { - start: f32, - end: f32, -} - -/// An error that indicates that an operation would have returned an invalid [`Interval`]. -#[derive(Debug, Error)] -#[error("The resulting interval would be invalid (empty or with a NaN endpoint)")] -pub struct InvalidIntervalError; - -/// An error indicating that an infinite interval was used where it was inappropriate. -#[derive(Debug, Error)] -#[error("This operation does not make sense in the context of an infinite interval")] -pub struct InfiniteIntervalError; - -/// An error indicating that spaced points on an interval could not be formed. -#[derive(Debug, Error)] -#[error("Could not sample evenly-spaced points with these inputs")] -pub enum SpacedPointsError { - /// This operation failed because fewer than two points were requested. - #[error("Parameter `points` must be at least 2")] - NotEnoughPoints, - - /// This operation failed because the underlying interval is unbounded. - #[error("Cannot sample evenly-spaced points on an infinite interval")] - InfiniteInterval(InfiniteIntervalError), -} - -impl Interval { - /// Create a new [`Interval`] with the specified `start` and `end`. The interval can be infinite - /// but cannot be empty and neither endpoint can be NaN; invalid parameters will result in an error. - pub fn new(start: f32, end: f32) -> Result { - if start >= end || start.is_nan() || end.is_nan() { - Err(InvalidIntervalError) - } else { - Ok(Self { start, end }) - } - } - - /// Get the start of this interval. - #[inline] - pub fn start(self) -> f32 { - self.start - } - - /// Get the end of this interval. - #[inline] - pub fn end(self) -> f32 { - self.end - } - - /// Create an [`Interval`] by intersecting this interval with another. Returns an error if the - /// intersection would be empty (hence an invalid interval). - pub fn intersect(self, other: Interval) -> Result { - let lower = max_by(self.start, other.start, |x, y| x.partial_cmp(y).unwrap()); - let upper = min_by(self.end, other.end, |x, y| x.partial_cmp(y).unwrap()); - Self::new(lower, upper) - } - - /// Get the length of this interval. Note that the result may be infinite (`f32::INFINITY`). - #[inline] - pub fn length(self) -> f32 { - self.end - self.start - } - - /// Returns `true` if this interval is finite. - #[inline] - pub fn is_finite(self) -> bool { - self.length().is_finite() - } - - /// Returns `true` if `item` is contained in this interval. - #[inline] - pub fn contains(self, item: f32) -> bool { - (self.start..=self.end).contains(&item) - } - - /// Clamp the given `value` to lie within this interval. - #[inline] - pub fn clamp(self, value: f32) -> f32 { - value.clamp(self.start, self.end) - } - - /// Get the linear map which maps this curve onto the `other` one. Returns an error if either - /// interval is infinite. - pub fn linear_map_to(self, other: Self) -> Result f32, InfiniteIntervalError> { - if !self.is_finite() || !other.is_finite() { - return Err(InfiniteIntervalError); - } - let scale = other.length() / self.length(); - Ok(move |x| (x - self.start) * scale + other.start) - } - - /// Get an iterator over equally-spaced points from this interval in increasing order. - /// Returns `None` if `points` is less than 2; the spaced points always include the endpoints. - pub fn spaced_points( - self, - points: usize, - ) -> Result, SpacedPointsError> { - if points < 2 { - return Err(SpacedPointsError::NotEnoughPoints); - } - if !self.is_finite() { - return Err(SpacedPointsError::InfiniteInterval(InfiniteIntervalError)); - } - let step = self.length() / (points - 1) as f32; - Ok((0..points).map(move |x| self.start + x as f32 * step)) - } -} - -impl TryFrom> for Interval { - type Error = InvalidIntervalError; - fn try_from(range: RangeInclusive) -> Result { - Interval::new(*range.start(), *range.end()) - } -} +//! The [`Curve`] trait, used to describe curves in a number of different domains. This module also +//! contains the [`Interpolable`] trait and the [`Interval`] type. -/// A trait for types whose values can be intermediately interpolated between two given values -/// with an auxiliary parameter. -pub trait Interpolable: Clone { - /// Interpolate between this value and the `other` given value using the parameter `t`. - /// Note that the parameter `t` is not necessarily clamped to lie between `0` and `1`. - fn interpolate(&self, other: &Self, t: f32) -> Self; -} +pub mod interpolable; +pub mod interval; -impl Interpolable for (S, T) -where - S: Interpolable, - T: Interpolable, -{ - fn interpolate(&self, other: &Self, t: f32) -> Self { - ( - self.0.interpolate(&other.0, t), - self.1.interpolate(&other.1, t), - ) - } -} - -impl Interpolable for T -where - T: VectorSpace, -{ - fn interpolate(&self, other: &Self, t: f32) -> Self { - self.lerp(*other, t) - } -} - -impl Interpolable for Quat { - fn interpolate(&self, other: &Self, t: f32) -> Self { - self.slerp(*other, t) - } -} +use interpolable::Interpolable; +use interval::*; +use std::{marker::PhantomData, ops::Deref}; +use thiserror::Error; /// An error indicating that a resampling operation could not be performed because of /// malformed inputs. @@ -861,18 +706,6 @@ where } } -// Library functions: - -/// Create an [`Interval`] with a given `start` and `end`. Alias of [`Interval::new`]. -pub fn interval(start: f32, end: f32) -> Result { - Interval::new(start, end) -} - -/// The [`Interval`] from negative infinity to infinity. -pub fn everywhere() -> Interval { - Interval::new(f32::NEG_INFINITY, f32::INFINITY).unwrap() -} - /// Create a [`Curve`] that constantly takes the given `value` over the given `domain`. pub fn constant_curve(domain: Interval, value: T) -> impl Curve { ConstantCurve { domain, value } @@ -891,3 +724,154 @@ where pub fn flip(curve: impl Curve<(S, T)>) -> impl Curve<(T, S)> { curve.map(|(s, t)| (t, s)) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::Quat; + use approx::{assert_abs_diff_eq, AbsDiffEq}; + use std::f32::consts::TAU; + + #[test] + fn constant_curves() { + let curve = constant_curve(everywhere(), 5.0); + assert!(curve.sample(-35.0) == 5.0); + + let curve = constant_curve(interval(0.0, 1.0).unwrap(), true); + assert!(curve.sample(2.0) == true); + assert!(curve.sample_checked(2.0).is_none()); + } + + #[test] + fn function_curves() { + let curve = function_curve(everywhere(), |t| t * t); + assert!(curve.sample(2.0).abs_diff_eq(&4.0, f32::EPSILON)); + assert!(curve.sample(-3.0).abs_diff_eq(&9.0, f32::EPSILON)); + + let curve = function_curve(interval(0.0, f32::INFINITY).unwrap(), |t| t.log2()); + assert_eq!(curve.sample(3.5), f32::log2(3.5)); + assert!(curve.sample(-1.0).is_nan()); + assert!(curve.sample_checked(-1.0).is_none()); + } + + #[test] + fn mapping() { + let curve = function_curve(everywhere(), |t| t * 3.0 + 1.0); + let mapped_curve = curve.map(|x| x / 7.0); + assert_eq!(mapped_curve.sample(3.5), (3.5 * 3.0 + 1.0) / 7.0); + assert_eq!(mapped_curve.sample(-1.0), (-1.0 * 3.0 + 1.0) / 7.0); + assert_eq!(mapped_curve.domain(), everywhere()); + + let curve = function_curve(interval(0.0, 1.0).unwrap(), |t| t * TAU); + let mapped_curve = curve.map(|x| Quat::from_rotation_z(x)); + assert_eq!(mapped_curve.sample(0.0), Quat::IDENTITY); + assert!(mapped_curve.sample(1.0).is_near_identity()); + assert_eq!(mapped_curve.domain(), interval(0.0, 1.0).unwrap()); + } + + #[test] + fn reparametrization() { + let curve = function_curve(interval(1.0, f32::INFINITY).unwrap(), |t| t.log2()); + let reparametrized_curve = curve + .by_ref() + .reparametrize(interval(0.0, f32::INFINITY).unwrap(), |t| t.exp2()); + assert_abs_diff_eq!(reparametrized_curve.sample(3.5), 3.5); + assert_abs_diff_eq!(reparametrized_curve.sample(100.0), 100.0); + assert_eq!( + reparametrized_curve.domain(), + interval(0.0, f32::INFINITY).unwrap() + ); + + let reparametrized_curve = curve + .by_ref() + .reparametrize(interval(0.0, 1.0).unwrap(), |t| t + 1.0); + assert_abs_diff_eq!(reparametrized_curve.sample(0.0), 0.0); + assert_abs_diff_eq!(reparametrized_curve.sample(1.0), 1.0); + assert_eq!(reparametrized_curve.domain(), interval(0.0, 1.0).unwrap()); + } + + #[test] + fn multiple_maps() { + // Make sure these actually happen in the right order. + let curve = function_curve(interval(0.0, 1.0).unwrap(), |t| t.exp2()); + let first_mapped = curve.map(|x| x.log2()); + let second_mapped = first_mapped.map(|x| x * -2.0); + assert_abs_diff_eq!(second_mapped.sample(0.0), 0.0); + assert_abs_diff_eq!(second_mapped.sample(0.5), -1.0); + assert_abs_diff_eq!(second_mapped.sample(1.0), -2.0); + } + + #[test] + fn multiple_reparams() { + // Make sure these happen in the right order too. + let curve = function_curve(interval(0.0, 1.0).unwrap(), |t| t.exp2()); + let first_reparam = curve.reparametrize(interval(1.0, 2.0).unwrap(), |t| t.log2()); + let second_reparam = first_reparam.reparametrize(interval(0.0, 1.0).unwrap(), |t| t + 1.0); + assert_abs_diff_eq!(second_reparam.sample(0.0), 1.0); + assert_abs_diff_eq!(second_reparam.sample(0.5), 1.5); + assert_abs_diff_eq!(second_reparam.sample(1.0), 2.0); + } + + #[test] + fn resampling() { + let curve = function_curve(interval(1.0, 4.0).unwrap(), |t| t.log2()); + + // Need at least two points to sample. + let nice_try = curve.by_ref().resample(1); + 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(101).unwrap(); + let step = curve.domain().length() / 100.0; + for index in 0..101 { + let test_pt = curve.domain().start() + index as f32 * step; + let expected = curve.sample(test_pt); + assert_abs_diff_eq!(resampled_curve.sample(test_pt), expected, epsilon = 1e-6); + } + + // Another example. + let curve = function_curve(interval(0.0, TAU).unwrap(), |t| t.cos()); + let resampled_curve = curve.by_ref().resample(1001).unwrap(); + let step = curve.domain().length() / 1000.0; + for index in 0..1001 { + let test_pt = curve.domain().start() + index as f32 * step; + let expected = curve.sample(test_pt); + assert_abs_diff_eq!(resampled_curve.sample(test_pt), expected, epsilon = 1e-6); + } + } + + #[test] + fn uneven_resampling() { + let curve = function_curve(interval(0.0, f32::INFINITY).unwrap(), |t| t.exp()); + + // Need at least two points to resample. + let nice_try = curve.by_ref().resample_uneven([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).into_iter().map(|idx| idx as f32 * 0.1); + let resampled_curve = curve.by_ref().resample_uneven(sample_points).unwrap(); + for idx in 0..100 { + let test_pt = idx as f32 * 0.1; + let expected = curve.sample(test_pt); + assert_eq!(resampled_curve.sample(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(), |t| t.log2()); + let sample_points = (0..10).into_iter().map(|idx| (idx as f32).exp2()); + let resampled_curve = curve.by_ref().resample_uneven(sample_points).unwrap(); + for idx in 0..10 { + let test_pt = (idx as f32).exp2(); + let expected = curve.sample(test_pt); + assert_eq!(resampled_curve.sample(test_pt), expected); + } + assert_abs_diff_eq!(resampled_curve.domain().start(), 1.0); + assert_abs_diff_eq!(resampled_curve.domain().end(), 512.0); + } +} From 715fdcaf6024eb861f30b2e8818a27e55010b0e1 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Wed, 8 May 2024 13:11:06 -0400 Subject: [PATCH 16/44] Fixes to docs/lints, some re-exports --- crates/bevy_math/src/curve/mod.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 97a66eb0d3ec5..d1fbf05a17bb8 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -4,8 +4,10 @@ pub mod interpolable; pub mod interval; -use interpolable::Interpolable; -use interval::*; +pub use interpolable::Interpolable; +pub use interval::{everywhere, interval, Interval}; + +use interval::{InfiniteIntervalError, InvalidIntervalError}; use std::{marker::PhantomData, ops::Deref}; use thiserror::Error; @@ -738,7 +740,7 @@ mod tests { assert!(curve.sample(-35.0) == 5.0); let curve = constant_curve(interval(0.0, 1.0).unwrap(), true); - assert!(curve.sample(2.0) == true); + assert!(curve.sample(2.0)); assert!(curve.sample_checked(2.0).is_none()); } @@ -763,7 +765,7 @@ mod tests { assert_eq!(mapped_curve.domain(), everywhere()); let curve = function_curve(interval(0.0, 1.0).unwrap(), |t| t * TAU); - let mapped_curve = curve.map(|x| Quat::from_rotation_z(x)); + let mapped_curve = curve.map(Quat::from_rotation_z); assert_eq!(mapped_curve.sample(0.0), Quat::IDENTITY); assert!(mapped_curve.sample(1.0).is_near_identity()); assert_eq!(mapped_curve.domain(), interval(0.0, 1.0).unwrap()); @@ -852,7 +854,7 @@ mod tests { // 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).into_iter().map(|idx| idx as f32 * 0.1); + let sample_points = (0..100).map(|idx| idx as f32 * 0.1); let resampled_curve = curve.by_ref().resample_uneven(sample_points).unwrap(); for idx in 0..100 { let test_pt = idx as f32 * 0.1; @@ -864,7 +866,7 @@ mod tests { // Another example. let curve = function_curve(interval(1.0, f32::INFINITY).unwrap(), |t| t.log2()); - let sample_points = (0..10).into_iter().map(|idx| (idx as f32).exp2()); + let sample_points = (0..10).map(|idx| (idx as f32).exp2()); let resampled_curve = curve.by_ref().resample_uneven(sample_points).unwrap(); for idx in 0..10 { let test_pt = (idx as f32).exp2(); From eabbd08b4bab6f0cfc4d7cc17ea8413f617a7f88 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Thu, 9 May 2024 08:41:12 -0400 Subject: [PATCH 17/44] Added IterableCurve abstraction --- crates/bevy_animation/src/curves.rs | 106 +++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 3 deletions(-) diff --git a/crates/bevy_animation/src/curves.rs b/crates/bevy_animation/src/curves.rs index 78c81a167a2ab..018f93dc93f8a 100644 --- a/crates/bevy_animation/src/curves.rs +++ b/crates/bevy_animation/src/curves.rs @@ -2,14 +2,15 @@ use std::marker::PhantomData; use bevy_math::{ cubic_splines::{CubicGenerator, CubicHermite}, - curve::{interval, ConstantCurve, Curve, Interpolable, Interval, MapCurve, UnevenSampleCurve}, + curve::{interval, ConstantCurve, Curve, Interpolable, Interval, UnevenSampleCurve}, FloatExt, Quat, Vec3, Vec4, VectorSpace, }; +use bevy_reflect::Reflect; /// A wrapper struct that gives the enclosed type the property of being [`Interpolable`] with /// naïve step interpolation. `self.interpolate(other, t)` is such that `self` is returned when /// `t` is less than `1.0`, while `other` is returned for values `1.0` and greater. -#[derive(Clone, Copy, Default)] +#[derive(Reflect, Clone, Copy, Default, Debug)] pub struct Stepped(pub T) where T: Clone; @@ -30,7 +31,7 @@ impl Interpolable for Stepped { /// /// Note that outside of the interval `[0, 1]`, this uses global extrapolation based on the /// out-tangent of the left-hand point and the in-tangent of the right-hand point. -#[derive(Clone, Copy, Default)] +#[derive(Reflect, Clone, Copy, Default, Debug)] pub struct TwoSidedHermite { /// The position of the datum in space. pub point: V, @@ -71,6 +72,7 @@ where /// however, linear interpolation is intrinsic to `Vec3` itself, so the interpolation metadata /// itself will be lost if the curve is resampled. On the other hand, the variant curves each /// properly know their own modes of interpolation. +#[derive(Clone, Debug)] pub enum TranslationCurve { /// A curve which takes a constant value over its domain. Notably, this is how animations with /// only a single keyframe are interpreted. @@ -114,6 +116,7 @@ impl Curve for TranslationCurve { /// however, linear interpolation is intrinsic to `Vec3` itself, so the interpolation metadata /// itself will be lost if the curve is resampled. On the other hand, the variant curves each /// properly know their own modes of interpolation. +#[derive(Clone, Debug)] pub enum ScaleCurve { /// A curve which takes a constant value over its domain. Notably, this is how animations with /// only a single keyframe are interpreted. @@ -157,6 +160,7 @@ impl Curve for ScaleCurve { /// however, spherical linear interpolation is intrinsic to `Vec3` itself, so the interpolation /// metadata itself will be lost if the curve is resampled. On the other hand, the variant curves each /// properly know their own modes of interpolation. +#[derive(Clone, Debug)] pub enum RotationCurve { /// A curve which takes a constant value over its domain. Notably, this is how animations with /// only a single keyframe are interpreted. @@ -201,6 +205,7 @@ impl Curve for RotationCurve { /// This type is, itself, a `Curve>`; however, in order to avoid allocation, it is /// recommended to use its implementation of the [`MultiCurve`] subtrait, which allows dumping /// cross-channel sample data into an external buffer, avoiding allocation. +#[derive(Reflect, Clone, Debug)] 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. @@ -257,6 +262,7 @@ impl MultiCurve for WeightsCurve { /// or the [`MorphWeights`] of morph targets for a mesh. /// /// Each variant yields a [`Curve`] over the data that it parametrizes. +#[derive(Reflect, Clone, Debug)] pub enum VariableCurve { /// A [`TranslationCurve`] for animating the `translation` component of a [`Transform`]. Translation(TranslationCurve), @@ -348,6 +354,7 @@ where /// "channels" equal to its `width`. This is sampled through `sample_into`, which places the data /// into an external buffer. If `T: Default`, this may also be used as a `Curve` directly, but a new /// `Vec` will be allocated for each call, which may be undesirable. +#[derive(Clone, Debug)] pub struct DynamicArrayCurve where T: Interpolable, @@ -571,3 +578,96 @@ where self.map_sample_into(t, buffer, f) } } + +// Another experiment: iterable curves. + +/// A curve which provides samples in the form of [`Iterator`]s. +/// +/// This is an abstraction that provides an interface for curves which look like `Curve>` +/// but side-stepping issues with allocation on sampling. This happens when the size of an output +/// array cannot be known statically. +pub trait IterableCurve +where + T: Interpolable, +{ + fn domain(&self) -> Interval; + + /// Sample this curve at a specified time `t`, producing an iterator over sampled values. + fn sample_iter<'a>(&self, t: f32) -> impl Iterator + where + Self: 'a; +} + +impl IterableCurve for DynamicArrayCurve +where + T: Interpolable, +{ + fn domain(&self) -> Interval { + self.domain() + } + + fn sample_iter<'a>(&self, t: f32) -> impl Iterator + where + Self: 'a, + { + let t = self.domain().clamp(t); + + let Some(lower_index) = self.find_keyframe(t) else { + // After clamping, `find_keyframe` will only return None if we landed on the + // last keyframe. + let index = self.times.len() - 1; + + // Jump to where the values for the last keyframe are: + let morph_index = index * self.width; + + // Return an iterator that just clones the last keyframe. + return IteratorDisjunction::Left( + self.values[morph_index..(morph_index + self.width)] + .iter() + .cloned(), + ); + }; + + // Get the adjacent timestamps and the lerp parameter of `t` between them: + let upper_index = lower_index + 1; + let lower_timestamp = self.times[lower_index]; + let upper_timestamp = self.times[upper_index]; + let lerp_param = f32::inverse_lerp(lower_timestamp, upper_timestamp, t); + + // The indices in `self.values` where the values actually start: + let lower_morph_index = lower_index * self.width; + let upper_morph_index = upper_index * self.width; + + // Return an iterator that lerps adjacent keyframes together. + IteratorDisjunction::Right( + self.values[lower_morph_index..(lower_morph_index + self.width)] + .iter() + .zip(self.values[upper_morph_index..(upper_morph_index + self.width)].iter()) + .map(move |(x, y)| x.interpolate(y, lerp_param)), + ) + } +} + +enum IteratorDisjunction +where + A: Iterator, + B: Iterator, +{ + Left(A), + Right(B), +} + +impl Iterator for IteratorDisjunction +where + A: Iterator, + B: Iterator, +{ + type Item = T; + + fn next(&mut self) -> Option { + match self { + IteratorDisjunction::Left(a) => a.next(), + IteratorDisjunction::Right(b) => b.next(), + } + } +} From 8e31af482314aa79f6dccba2107f18907e847047 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Thu, 9 May 2024 12:51:41 -0400 Subject: [PATCH 18/44] More work in direction of IterableCurve --- crates/bevy_animation/src/curves.rs | 280 ++++++---------------------- crates/bevy_math/src/curve/mod.rs | 33 ++++ 2 files changed, 91 insertions(+), 222 deletions(-) diff --git a/crates/bevy_animation/src/curves.rs b/crates/bevy_animation/src/curves.rs index e7eba04781256..d41f27dbb2627 100644 --- a/crates/bevy_animation/src/curves.rs +++ b/crates/bevy_animation/src/curves.rs @@ -2,7 +2,7 @@ use std::marker::PhantomData; use bevy_math::{ cubic_splines::{CubicGenerator, CubicHermite}, - curve::{interval, ConstantCurve, Curve, Interpolable, Interval, UnevenSampleCurve}, + curve::*, FloatExt, Quat, Vec3, Vec4, VectorSpace, }; use bevy_reflect::Reflect; @@ -222,39 +222,40 @@ pub enum WeightsCurve { CubicSpline(DynamicArrayCurve>), } -impl Curve> for WeightsCurve { +impl IterableCurve for WeightsCurve { fn domain(&self) -> Interval { match self { - WeightsCurve::Constant(c) => c.domain(), + WeightsCurve::Constant(c) => IterableCurve::domain(c), WeightsCurve::Linear(c) => c.domain(), WeightsCurve::Step(c) => c.domain(), WeightsCurve::CubicSpline(c) => c.domain(), } } - fn sample(&self, t: f32) -> Vec { + fn sample_iter<'a>(&self, t: f32) -> impl Iterator + where + Self: 'a, + { match self { - WeightsCurve::Constant(c) => c.sample(t), - WeightsCurve::Linear(c) => c.sample(t), - WeightsCurve::Step(c) => c.map(|v| v.into_iter().map(|x| x.0).collect()).sample(t), - WeightsCurve::CubicSpline(c) => c - .map(|v| v.into_iter().map(|x| x.point).collect()) - .sample(t), + WeightsCurve::Constant(c) => QuaternaryIteratorDisjunction::First(c.sample_iter(t)), + WeightsCurve::Linear(c) => QuaternaryIteratorDisjunction::Second(c.sample_iter(t)), + WeightsCurve::Step(c) => { + QuaternaryIteratorDisjunction::Third(c.sample_iter(t).map(|v| v.0)) + } + WeightsCurve::CubicSpline(c) => { + QuaternaryIteratorDisjunction::Fourth(c.sample_iter(t).map(|v| v.point)) + } } } } -impl MultiCurve for WeightsCurve { - fn map_sample_into(&self, t: f32, buffer: &mut [S], f: &impl Fn(f32) -> S) - where - S: Interpolable, - { - match self { - WeightsCurve::Constant(c) => todo!(), - WeightsCurve::Linear(c) => todo!(), - WeightsCurve::Step(c) => todo!(), - WeightsCurve::CubicSpline(c) => todo!(), - } +impl Curve> for WeightsCurve { + fn domain(&self) -> Interval { + IterableCurve::domain(self) + } + + fn sample(&self, t: f32) -> Vec { + self.sample_iter(t).collect() } } @@ -277,78 +278,9 @@ pub enum VariableCurve { Weights(WeightsCurve), } -//--------------// -// EXPERIMENTAL // -//--------------// - -/// A trait for a curve that takes many interpolable values simultaneously, providing a function -/// to place those values into a buffer rather than allocating while sampling. -pub trait MultiCurve: Curve> -where - T: Interpolable, -{ - /// Sample a number of simultaneous values from this curve into a buffer. - fn sample_into(&self, t: f32, buffer: &mut [T]) { - self.map_sample_into(t, buffer, &|x| x) - } - - /// Map the collection of samples by `f` before putting them into the given buffer. - fn map_sample_into(&self, t: f32, buffer: &mut [S], f: &impl Fn(T) -> S) - where - S: Interpolable; -} - -pub struct MultiMapCurve -where - S: Interpolable, - T: Interpolable, - C: MultiCurve, - F: Fn(S) -> T, -{ - preimage: C, - f: F, - _phantom: PhantomData<(S, T)>, -} - -impl Curve> for MultiMapCurve -where - S: Interpolable, - T: Interpolable, - C: MultiCurve, - F: Fn(S) -> T, -{ - fn domain(&self) -> Interval { - self.preimage.domain() - } - - fn sample(&self, t: f32) -> Vec { - self.preimage - .sample(t) - .into_iter() - .map(|x| (self.f)(x)) - .collect() - } -} - -impl MultiCurve for MultiMapCurve -where - S: Interpolable, - T: Interpolable, - C: MultiCurve, - F: Fn(S) -> T, -{ - fn sample_into(&self, t: f32, buffer: &mut [T]) { - self.preimage.map_sample_into(t, buffer, &self.f); - } - - fn map_sample_into(&self, t: f32, buffer: &mut [R], g: &impl Fn(T) -> R) - where - R: Interpolable, - { - let gf = |x| g((self.f)(x)); - self.preimage.map_sample_into(t, buffer, &gf); - } -} +//-----------------// +// NEW CURVE STUFF // +//-----------------// /// A curve data structure which holds data for a list of keyframes in a number of distinct /// "channels" equal to its `width`. This is sampled through `sample_into`, which places the data @@ -453,101 +385,10 @@ where let end = self.times.last().unwrap(); interval(*start, *end).unwrap() } - - /// Sample the interpolated data at time `t` into a given `buffer`. - /// - /// # Panics - /// Panics if the provided buffer is not at least as large as `width`. - pub fn sample_into(&self, t: f32, buffer: &mut [T]) { - assert!(buffer.len() >= self.width); - - let t = self.domain().clamp(t); - - let Some(lower_index) = self.find_keyframe(t) else { - // After clamping, `find_keyframe` will only return None if we landed on the - // last keyframe. - let index = self.times.len() - 1; - - // Jump to where the values for the last keyframe are: - let morph_index = index * self.width; - - // Copy the values for the last keyframe into the buffer: - for offset in 0..self.width { - buffer[offset] = self.values[morph_index + offset].clone(); - } - - return; - }; - - // Get the adjacent timestamps and the lerp parameter of `t` between them: - let upper_index = lower_index + 1; - let lower_timestamp = self.times[lower_index]; - let upper_timestamp = self.times[upper_index]; - let lerp_param = f32::inverse_lerp(lower_timestamp, upper_timestamp, t); - - // The indices in `self.values` where the values actually start: - let lower_morph_index = lower_index * self.width; - let upper_morph_index = upper_index * self.width; - - // Interpolate and dump the results into the given buffer: - for offset in 0..self.width { - let lower_value = &self.values[lower_morph_index + offset]; - let upper_value = &self.values[upper_morph_index + offset]; - buffer[offset] = lower_value.interpolate(upper_value, lerp_param); - } - } - - /// Sample the interpolated data at time `t` into a given `buffer` after mapping it through - /// a function `f`. - /// - /// # Panics - /// Panics if the provided buffer is not at least as large as `width`. - pub fn map_sample_into(&self, t: f32, buffer: &mut [S], f: &impl Fn(T) -> S) - where - S: Interpolable, - { - assert!(buffer.len() >= self.width); - - let t = self.domain().clamp(t); - - let Some(lower_index) = self.find_keyframe(t) else { - // After clamping, `find_keyframe` will only return None if we landed on the - // last keyframe. - let index = self.times.len() - 1; - - // Jump to where the values for the last keyframe are: - let morph_index = index * self.width; - - // Copy the values for the last keyframe into the buffer: - for offset in 0..self.width { - buffer[offset] = f(self.values[morph_index + offset].clone()); - } - - return; - }; - - // Get the adjacent timestamps and the lerp parameter of `t` between them: - let upper_index = lower_index + 1; - let lower_timestamp = self.times[lower_index]; - let upper_timestamp = self.times[upper_index]; - let lerp_param = f32::inverse_lerp(lower_timestamp, upper_timestamp, t); - - // The indices in `self.values` where the values actually start: - let lower_morph_index = lower_index * self.width; - let upper_morph_index = upper_index * self.width; - - // Interpolate and dump the results into the given buffer: - for offset in 0..self.width { - let lower_value = &self.values[lower_morph_index + offset]; - let upper_value = &self.values[upper_morph_index + offset]; - buffer[offset] = f(lower_value.interpolate(upper_value, lerp_param)); - } - } } // Note that the `sample` function always allocates its output, whereas `sample_into` can dump // the sample data into an external buffer, bypassing the need to allocate. - impl Curve> for DynamicArrayCurve where T: Interpolable + Default, @@ -557,45 +398,8 @@ where } fn sample(&self, t: f32) -> Vec { - let mut output: Vec = vec![::default(); self.width]; - self.sample_into(t, output.as_mut_slice()); - output - } -} - -impl MultiCurve for DynamicArrayCurve -where - T: Interpolable + Default, -{ - fn sample_into(&self, t: f32, buffer: &mut [T]) { - self.sample_into(t, buffer); + self.sample_iter(t).collect() } - - fn map_sample_into(&self, t: f32, buffer: &mut [S], f: &impl Fn(T) -> S) - where - S: Interpolable, - { - self.map_sample_into(t, buffer, f) - } -} - -// Another experiment: iterable curves. - -/// A curve which provides samples in the form of [`Iterator`]s. -/// -/// This is an abstraction that provides an interface for curves which look like `Curve>` -/// but side-stepping issues with allocation on sampling. This happens when the size of an output -/// array cannot be known statically. -pub trait IterableCurve -where - T: Interpolable, -{ - fn domain(&self) -> Interval; - - /// Sample this curve at a specified time `t`, producing an iterator over sampled values. - fn sample_iter<'a>(&self, t: f32) -> impl Iterator - where - Self: 'a; } impl IterableCurve for DynamicArrayCurve @@ -671,3 +475,35 @@ where } } } + +enum QuaternaryIteratorDisjunction +where + A: Iterator, + B: Iterator, + C: Iterator, + D: Iterator, +{ + First(A), + Second(B), + Third(C), + Fourth(D), +} + +impl Iterator for QuaternaryIteratorDisjunction +where + A: Iterator, + B: Iterator, + C: Iterator, + D: Iterator, +{ + type Item = T; + + fn next(&mut self) -> Option { + match self { + QuaternaryIteratorDisjunction::First(a) => a.next(), + QuaternaryIteratorDisjunction::Second(b) => b.next(), + QuaternaryIteratorDisjunction::Third(c) => c.next(), + QuaternaryIteratorDisjunction::Fourth(d) => d.next(), + } + } +} diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index d1fbf05a17bb8..073d985172464 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -877,3 +877,36 @@ mod tests { assert_abs_diff_eq!(resampled_curve.domain().end(), 512.0); } } + +// Haha... you thought the file was over! + +/// A curve which provides samples in the form of [`Iterator`]s. +/// +/// This is an abstraction that provides an interface for curves which look like `Curve>` +/// but side-stepping issues with allocation on sampling. This happens when the size of an output +/// array cannot be known statically. +pub trait IterableCurve { + /// The interval over which this curve is parametrized. + fn domain(&self) -> Interval; + + /// Sample this curve at a specified time `t`, producing an iterator over sampled values. + fn sample_iter<'a>(&self, t: f32) -> impl Iterator + where + Self: 'a; +} + +impl IterableCurve for ConstantCurve> +where + T: Clone, +{ + fn domain(&self) -> Interval { + self.domain + } + + fn sample_iter<'a>(&self, _t: f32) -> impl Iterator + where + Self: 'a, + { + self.value.iter().cloned() + } +} From dd205d4457b56dee24eaa39ef1aa143b899413a8 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Tue, 21 May 2024 07:58:31 -0400 Subject: [PATCH 19/44] Placeholder for reflecting curve types --- crates/bevy_animation/src/curves.rs | 3 --- crates/bevy_reflect/src/impls/math/curve.rs | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) create mode 100644 crates/bevy_reflect/src/impls/math/curve.rs diff --git a/crates/bevy_animation/src/curves.rs b/crates/bevy_animation/src/curves.rs index d41f27dbb2627..bdb6daf87ba5d 100644 --- a/crates/bevy_animation/src/curves.rs +++ b/crates/bevy_animation/src/curves.rs @@ -1,11 +1,8 @@ -use std::marker::PhantomData; - use bevy_math::{ cubic_splines::{CubicGenerator, CubicHermite}, curve::*, FloatExt, Quat, Vec3, Vec4, VectorSpace, }; -use bevy_reflect::Reflect; /// A wrapper struct that gives the enclosed type the property of being [`Interpolable`] with /// naïve step interpolation. `self.interpolate(other, t)` is such that `self` is returned when diff --git a/crates/bevy_reflect/src/impls/math/curve.rs b/crates/bevy_reflect/src/impls/math/curve.rs new file mode 100644 index 0000000000000..b3a20d555e3f1 --- /dev/null +++ b/crates/bevy_reflect/src/impls/math/curve.rs @@ -0,0 +1 @@ +impl_reflect!(); From 65d85c8e38feef834c9ba2b0803ade1e3b0b6edf Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Tue, 21 May 2024 10:09:08 -0400 Subject: [PATCH 20/44] Add derived traits to structs --- crates/bevy_math/src/curve/interval.rs | 1 + crates/bevy_math/src/curve/mod.rs | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/crates/bevy_math/src/curve/interval.rs b/crates/bevy_math/src/curve/interval.rs index 72d8993e7be1d..37adc47ec3066 100644 --- a/crates/bevy_math/src/curve/interval.rs +++ b/crates/bevy_math/src/curve/interval.rs @@ -8,6 +8,7 @@ use thiserror::Error; /// A nonempty closed interval, possibly infinite in either direction. #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct Interval { start: f32, end: f32, diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index d1fbf05a17bb8..f9c76c819a206 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -261,6 +261,8 @@ where } /// A [`Curve`] which takes a constant value over its domain. +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct ConstantCurve where T: Clone, @@ -285,6 +287,7 @@ where } /// A [`Curve`] defined by a function. +#[derive(Clone, Debug)] pub struct FunctionCurve where F: Fn(f32) -> T, @@ -309,6 +312,8 @@ where } /// 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))] pub struct SampleCurve where T: Interpolable, @@ -385,6 +390,8 @@ where } /// A [`Curve`] that is defined by interpolation over unevenly spaced samples. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct UnevenSampleCurve where T: Interpolable, @@ -482,6 +489,7 @@ where /// A [`Curve`] whose samples are defined by mapping samples from another curve through a /// given function. +#[derive(Clone, Debug)] pub struct MapCurve where C: Curve, @@ -536,6 +544,7 @@ where } /// A [`Curve`] whose sample space is mapped onto that of some base curve's before sampling. +#[derive(Clone, Debug)] pub struct ReparamCurve where C: Curve, @@ -596,6 +605,7 @@ where /// /// Briefly, the point is that the curve just absorbs new functions instead of rebasing /// itself inside new structs. +#[derive(Clone, Debug)] pub struct MapReparamCurve where C: Curve, @@ -657,6 +667,8 @@ where } /// A [`Curve`] that is the graph of another curve over its parameter space. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct GraphCurve where C: Curve, @@ -681,6 +693,8 @@ where } /// A [`Curve`] that combines the data from two constituent curves into a tuple output type. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct ProductCurve where C: Curve, From 6cd1d4bb85be99a1e23b53b83bf75d7d402e7deb Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Wed, 5 Jun 2024 16:54:25 -0400 Subject: [PATCH 21/44] Add explicit interpolation --- crates/bevy_math/src/curve/mod.rs | 233 ++++++++++++++++++++++++++---- 1 file changed, 204 insertions(+), 29 deletions(-) diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index f9c76c819a206..b790b7c503c79 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -55,7 +55,7 @@ pub trait Curve { /// spaced values. A total of `samples` samples are used, although at least two samples are /// required in order to produce well-formed output. If fewer than two samples are provided, /// or if this curve has an unbounded domain, then a [`ResamplingError`] is returned. - fn resample(&self, samples: usize) -> Result, ResamplingError> + fn resample_auto(&self, samples: usize) -> Result, ResamplingError> where T: Interpolable, { @@ -66,6 +66,39 @@ pub trait Curve { return Err(ResamplingError::InfiniteInterval(InfiniteIntervalError)); } + let samples: Vec = self + .domain() + .spaced_points(samples) + .unwrap() + .map(|t| self.sample(t)) + .collect(); + Ok(SampleAutoCurve { + domain: self.domain(), + samples, + }) + } + + /// Resample this [`Curve`] to produce a new one that is defined by interpolation over equally + /// spaced values, using the provided `interpolation` to interpolate between adjacent samples. + /// A total of `samples` samples are used, although at least two samples are required to produce + /// well-formed output. If fewer than two samples are provided, or if this curve has an unbounded + /// domain, then a [`ResamplingError`] is returned. + fn resample( + &self, + samples: usize, + interpolation: I, + ) -> Result, ResamplingError> + where + Self: Sized, + I: Fn(&T, &T, f32) -> T, + { + if samples < 2 { + return Err(ResamplingError::NotEnoughSamples(samples)); + } + if !self.domain().is_finite() { + return Err(ResamplingError::InfiniteInterval(InfiniteIntervalError)); + } + let samples: Vec = self .domain() .spaced_points(samples) @@ -75,23 +108,24 @@ pub trait Curve { Ok(SampleCurve { domain: self.domain(), samples, + interpolation, }) } /// Resample this [`Curve`] to produce a new one that is defined by 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 range. + /// two valid times within the curve's domain interval. /// - /// Irredundant sample times, non-finite sample times, and sample times outside of the domain + /// 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 [`UnevenSampleCurve`] stretches between the first and last + /// The domain of the produced [`UnevenSampleAutoCurve`] stretches between the first and last /// sample times of the iterator. - fn resample_uneven( + fn resample_uneven_auto( &self, sample_times: impl IntoIterator, - ) -> Result, ResamplingError> + ) -> Result, ResamplingError> where Self: Sized, T: Interpolable, @@ -106,7 +140,44 @@ pub trait Curve { } times.sort_by(|t1, t2| t1.partial_cmp(t2).unwrap()); let samples = times.iter().copied().map(|t| self.sample(t)).collect(); - Ok(UnevenSampleCurve { times, samples }) + Ok(UnevenSampleAutoCurve { times, samples }) + } + + /// 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. + fn resample_uneven( + &self, + sample_times: impl IntoIterator, + interpolation: I, + ) -> Result, ResamplingError> + where + Self: Sized, + I: Fn(&T, &T, f32) -> T, + { + let mut times: Vec = sample_times + .into_iter() + .filter(|t| t.is_finite() && self.domain().contains(*t)) + .collect(); + times.dedup_by(|t1, t2| (*t1).eq(t2)); + if times.len() < 2 { + return Err(ResamplingError::NotEnoughSamples(times.len())); + } + times.sort_by(|t1, t2| t1.partial_cmp(t2).unwrap()); + let samples = times.iter().copied().map(|t| self.sample(t)).collect(); + Ok(UnevenSampleCurve { + times, + samples, + interpolation, + }) } /// Create a new curve by mapping the values of this curve via a function `f`; i.e., if the @@ -314,7 +385,7 @@ where /// 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))] -pub struct SampleCurve +pub struct SampleAutoCurve where T: Interpolable, { @@ -325,35 +396,35 @@ where samples: Vec, } -impl SampleCurve +impl SampleAutoCurve where T: Interpolable, { /// Like [`Curve::map`], but with a concrete return type. Unlike that function, this one is /// not lazy, and `f` is evaluated immediately on samples to produce the result. - pub fn map_concrete(self, f: impl Fn(T) -> S) -> SampleCurve + pub fn map_concrete(self, f: impl Fn(T) -> S) -> SampleAutoCurve where S: Interpolable, { let new_samples: Vec = self.samples.into_iter().map(f).collect(); - SampleCurve { + SampleAutoCurve { domain: self.domain, samples: new_samples, } } /// Like [`Curve::graph`], but with a concrete return type. - pub fn graph_concrete(self) -> SampleCurve<(f32, T)> { + pub fn graph_concrete(self) -> SampleAutoCurve<(f32, T)> { let times = self.domain().spaced_points(self.samples.len()).unwrap(); let new_samples: Vec<(f32, T)> = times.zip(self.samples).collect(); - SampleCurve { + SampleAutoCurve { domain: self.domain, samples: new_samples, } } } -impl Curve for SampleCurve +impl Curve for SampleAutoCurve where T: Interpolable, { @@ -389,10 +460,61 @@ where } } +/// A [`Curve`] that is defined by explicit neighbor interpolation over a set of samples. +pub struct SampleCurve { + domain: Interval, + /// The samples that make up this curve by interpolation. + /// + /// Invariant: this must always have a length of at least 2. + samples: Vec, + interpolation: I, +} + +impl Curve for SampleCurve +where + T: Clone, + I: Fn(&T, &T, f32) -> T, +{ + #[inline] + fn domain(&self) -> Interval { + self.domain + } + + #[inline] + fn sample(&self, t: f32) -> T { + // Inside the curve itself, we interpolate between the two nearest sample values. + let subdivs = self.samples.len() - 1; + let step = self.domain.length() / subdivs as f32; + let t_shifted = t - self.domain.start(); + let steps_taken = t_shifted / step; + + // Using `steps_taken` as the source of truth, clamp to the range of valid indices. + if steps_taken <= 0.0 { + self.samples.first().unwrap().clone() + } else if steps_taken >= (self.samples.len() - 1) as f32 { + self.samples.last().unwrap().clone() + } else { + // Here we use only the floor and the fractional part of `steps_taken` to interpolate + // between the two nearby sample points. + let lower_index = steps_taken.floor() as usize; + + // Explicitly clamp the lower index just in case. + let lower_index = lower_index.min(self.samples.len() - 2); + let upper_index = lower_index + 1; + let fract = steps_taken.fract(); + (self.interpolation)( + &self.samples[lower_index], + &self.samples[upper_index], + fract, + ) + } + } +} + /// A [`Curve`] that is defined by interpolation over unevenly spaced samples. #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -pub struct UnevenSampleCurve +pub struct UnevenSampleAutoCurve where T: Interpolable, { @@ -408,39 +530,39 @@ where samples: Vec, } -impl UnevenSampleCurve +impl UnevenSampleAutoCurve where T: Interpolable, { /// Like [`Curve::map`], but with a concrete return type. Unlike that function, this one is /// not lazy, and `f` is evaluated immediately on samples to produce the result. - pub fn map_concrete(self, f: impl Fn(T) -> S) -> UnevenSampleCurve + pub fn map_concrete(self, f: impl Fn(T) -> S) -> UnevenSampleAutoCurve where S: Interpolable, { let new_samples: Vec = self.samples.into_iter().map(f).collect(); - UnevenSampleCurve { + UnevenSampleAutoCurve { times: self.times, samples: new_samples, } } /// Like [`Curve::graph`], but with a concrete return type. - pub fn graph_concrete(self) -> UnevenSampleCurve<(f32, T)> { + pub fn graph_concrete(self) -> UnevenSampleAutoCurve<(f32, T)> { let new_samples = self.times.iter().copied().zip(self.samples).collect(); - UnevenSampleCurve { + UnevenSampleAutoCurve { times: self.times, samples: new_samples, } } - /// This [`UnevenSampleCurve`], but with the sample times moved by the map `f`. + /// 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 resorted 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(mut self, f: impl Fn(f32) -> f32) -> UnevenSampleCurve { + pub fn map_sample_times(mut self, f: impl Fn(f32) -> f32) -> UnevenSampleAutoCurve { let mut timed_samples: Vec<(f32, T)> = self.times.into_iter().map(f).zip(self.samples).collect(); timed_samples.dedup_by(|(t1, _), (t2, _)| (*t1).eq(t2)); @@ -451,7 +573,7 @@ where } } -impl Curve for UnevenSampleCurve +impl Curve for UnevenSampleAutoCurve where T: Interpolable, { @@ -487,6 +609,59 @@ where } } +/// A [`Curve`] that is defined by interpolation over unevenly spaced samples with explicit +/// interpolation. +pub struct UnevenSampleCurve { + /// 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. + times: Vec, + + /// The samples corresponding to the times for this curve. + /// + /// Invariants: This must always have the same length as `times`. + samples: Vec, + interpolation: I, +} + +impl Curve for UnevenSampleCurve +where + T: Clone, + I: Fn(&T, &T, f32) -> T, +{ + #[inline] + fn domain(&self) -> Interval { + let start = self.times.first().unwrap(); + let end = self.times.last().unwrap(); + Interval::new(*start, *end).unwrap() + } + + #[inline] + fn sample(&self, t: f32) -> T { + match self + .times + .binary_search_by(|pt| pt.partial_cmp(&t).unwrap()) + { + Ok(index) => self.samples[index].clone(), + Err(index) => { + if index == 0 { + self.samples.first().unwrap().clone() + } else if index == self.times.len() { + self.samples.last().unwrap().clone() + } else { + let t_lower = self.times[index - 1]; + let v_lower = self.samples.get(index - 1).unwrap(); + let t_upper = self.times[index]; + let v_upper = self.samples.get(index).unwrap(); + let s = (t - t_lower) / (t_upper - t_lower); + (self.interpolation)(v_lower, v_upper, s) + } + } + } + } +} + /// A [`Curve`] whose samples are defined by mapping samples from another curve through a /// given function. #[derive(Clone, Debug)] @@ -833,13 +1008,13 @@ mod tests { let curve = function_curve(interval(1.0, 4.0).unwrap(), |t| t.log2()); // Need at least two points to sample. - let nice_try = curve.by_ref().resample(1); + let nice_try = curve.by_ref().resample_auto(1); 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(101).unwrap(); + let resampled_curve = curve.by_ref().resample_auto(101).unwrap(); let step = curve.domain().length() / 100.0; for index in 0..101 { let test_pt = curve.domain().start() + index as f32 * step; @@ -849,7 +1024,7 @@ mod tests { // Another example. let curve = function_curve(interval(0.0, TAU).unwrap(), |t| t.cos()); - let resampled_curve = curve.by_ref().resample(1001).unwrap(); + let resampled_curve = curve.by_ref().resample_auto(1001).unwrap(); let step = curve.domain().length() / 1000.0; for index in 0..1001 { let test_pt = curve.domain().start() + index as f32 * step; @@ -863,13 +1038,13 @@ mod tests { let curve = function_curve(interval(0.0, f32::INFINITY).unwrap(), |t| t.exp()); // Need at least two points to resample. - let nice_try = curve.by_ref().resample_uneven([1.0; 1]); + 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(sample_points).unwrap(); + 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(test_pt); @@ -881,7 +1056,7 @@ mod tests { // Another example. let curve = function_curve(interval(1.0, f32::INFINITY).unwrap(), |t| t.log2()); let sample_points = (0..10).map(|idx| (idx as f32).exp2()); - let resampled_curve = curve.by_ref().resample_uneven(sample_points).unwrap(); + let resampled_curve = curve.by_ref().resample_uneven_auto(sample_points).unwrap(); for idx in 0..10 { let test_pt = (idx as f32).exp2(); let expected = curve.sample(test_pt); From 5f83f25d8bdb9d14d71bc708cde7d71b1cad0ca3 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Thu, 6 Jun 2024 18:12:06 -0400 Subject: [PATCH 22/44] Move to explicitly interpolated sampling --- crates/bevy_math/src/curve/interval.rs | 2 +- crates/bevy_math/src/curve/mod.rs | 171 +++++++++++++++++-------- 2 files changed, 120 insertions(+), 53 deletions(-) diff --git a/crates/bevy_math/src/curve/interval.rs b/crates/bevy_math/src/curve/interval.rs index 37adc47ec3066..0a1d8b2739c94 100644 --- a/crates/bevy_math/src/curve/interval.rs +++ b/crates/bevy_math/src/curve/interval.rs @@ -103,7 +103,7 @@ impl Interval { } /// Get an iterator over equally-spaced points from this interval in increasing order. - /// Returns `None` if `points` is less than 2; the spaced points always include the endpoints. + /// Returns an error if `points` is less than 2 or if the interval is unbounded. pub fn spaced_points( self, points: usize, diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index b790b7c503c79..6bdc63f8d1139 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -83,6 +83,19 @@ pub trait Curve { /// A total of `samples` samples are used, although at least two samples are required to produce /// well-formed output. If fewer than two samples are provided, 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| Rotation2d::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( &self, samples: usize, @@ -112,6 +125,25 @@ pub trait Curve { }) } + /// 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, ResamplingError> { + if samples < 2 { + return Err(ResamplingError::NotEnoughSamples(samples)); + } + if !self.domain().is_finite() { + return Err(ResamplingError::InfiniteInterval(InfiniteIntervalError)); + } + + // Unwrap on `spaced_points` always succeeds because its error conditions are handled + // above. + Ok(self + .domain() + .spaced_points(samples) + .unwrap() + .map(|t| self.sample(t))) + } + /// Resample this [`Curve`] to produce a new one that is defined by 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. @@ -154,6 +186,10 @@ pub trait Curve { /// /// 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( &self, sample_times: impl IntoIterator, @@ -305,7 +341,7 @@ pub trait Curve { /// 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 /// // ownership of its input. - /// let samples = my_curve.by_ref().map(|x| x * 2.0).resample(100).unwrap(); + /// let samples = my_curve.by_ref().map(|x| x * 2.0).resample_auto(100).unwrap(); /// // Do something else with `my_curve` since we retained ownership: /// let new_curve = my_curve.reparametrize_linear(interval(-1.0, 1.0).unwrap()).unwrap(); /// ``` @@ -396,34 +432,6 @@ where samples: Vec, } -impl SampleAutoCurve -where - T: Interpolable, -{ - /// Like [`Curve::map`], but with a concrete return type. Unlike that function, this one is - /// not lazy, and `f` is evaluated immediately on samples to produce the result. - pub fn map_concrete(self, f: impl Fn(T) -> S) -> SampleAutoCurve - where - S: Interpolable, - { - let new_samples: Vec = self.samples.into_iter().map(f).collect(); - SampleAutoCurve { - domain: self.domain, - samples: new_samples, - } - } - - /// Like [`Curve::graph`], but with a concrete return type. - pub fn graph_concrete(self) -> SampleAutoCurve<(f32, T)> { - let times = self.domain().spaced_points(self.samples.len()).unwrap(); - let new_samples: Vec<(f32, T)> = times.zip(self.samples).collect(); - SampleAutoCurve { - domain: self.domain, - samples: new_samples, - } - } -} - impl Curve for SampleAutoCurve where T: Interpolable, @@ -511,6 +519,38 @@ where } } +impl SampleCurve { + /// 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 Into>, + interpolation: I, + ) -> Result + where + I: Fn(&T, &T, f32) -> T, + { + let samples: Vec = samples.into(); + if samples.len() < 2 { + return Err(ResamplingError::NotEnoughSamples(samples.len())); + } + if !domain.is_finite() { + return Err(ResamplingError::InfiniteInterval(InfiniteIntervalError)); + } + + Ok(SampleCurve { + domain, + samples, + interpolation, + }) + } +} + /// A [`Curve`] that is defined by interpolation over unevenly spaced samples. #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -534,33 +574,11 @@ impl UnevenSampleAutoCurve where T: Interpolable, { - /// Like [`Curve::map`], but with a concrete return type. Unlike that function, this one is - /// not lazy, and `f` is evaluated immediately on samples to produce the result. - pub fn map_concrete(self, f: impl Fn(T) -> S) -> UnevenSampleAutoCurve - where - S: Interpolable, - { - let new_samples: Vec = self.samples.into_iter().map(f).collect(); - UnevenSampleAutoCurve { - times: self.times, - samples: new_samples, - } - } - - /// Like [`Curve::graph`], but with a concrete return type. - pub fn graph_concrete(self) -> UnevenSampleAutoCurve<(f32, T)> { - let new_samples = self.times.iter().copied().zip(self.samples).collect(); - UnevenSampleAutoCurve { - times: self.times, - samples: new_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 resorted by time after mapping and deduplicated by output time, so + /// 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(mut self, f: impl Fn(f32) -> f32) -> UnevenSampleAutoCurve { let mut timed_samples: Vec<(f32, T)> = @@ -662,6 +680,55 @@ where } } +impl UnevenSampleCurve { + /// 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 Into>, + interpolation: I, + ) -> Result { + let mut timed_samples: Vec<(f32, T)> = timed_samples.into(); + // Use default Equal to not do anything in case NAN appears; it will get removed in the + // next step anyway. + timed_samples + .sort_by(|(t0, _), (t1, _)| t0.partial_cmp(t1).unwrap_or(std::cmp::Ordering::Equal)); + let (times, samples): (Vec, Vec) = timed_samples + .into_iter() + .filter(|(t, _)| t.is_finite()) + .unzip(); + if times.len() < 2 { + return Err(ResamplingError::NotEnoughSamples(times.len())); + } + Ok(UnevenSampleCurve { + times, + 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(mut self, f: impl Fn(f32) -> f32) -> UnevenSampleCurve { + let mut timed_samples: Vec<(f32, T)> = + self.times.into_iter().map(f).zip(self.samples).collect(); + timed_samples.dedup_by(|(t1, _), (t2, _)| (*t1).eq(t2)); + timed_samples.sort_by(|(t1, _), (t2, _)| t1.partial_cmp(t2).unwrap()); + self.times = timed_samples.iter().map(|(t, _)| t).copied().collect(); + self.samples = timed_samples.into_iter().map(|(_, x)| x).collect(); + self + } +} + /// A [`Curve`] whose samples are defined by mapping samples from another curve through a /// given function. #[derive(Clone, Debug)] From 83d744f63a275852c6672586a01ef37ea9502a67 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 10 Jun 2024 07:25:54 -0400 Subject: [PATCH 23/44] Refactor SampleCurve/UnevenSampleCurve into core builders --- crates/bevy_math/src/curve/builders.rs | 285 +++++++++++++++++ crates/bevy_math/src/curve/mod.rs | 408 +++++++++---------------- 2 files changed, 428 insertions(+), 265 deletions(-) create mode 100644 crates/bevy_math/src/curve/builders.rs diff --git a/crates/bevy_math/src/curve/builders.rs b/crates/bevy_math/src/curve/builders.rs new file mode 100644 index 0000000000000..9af37df0ce43f --- /dev/null +++ b/crates/bevy_math/src/curve/builders.rs @@ -0,0 +1,285 @@ +//! Core data structures to be used internally in Curve implementations. + +use super::interval::Interval; +use thiserror::Error; + +/// 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. +/// +/// [the provided constructor]: SampleCore::new +/// [`domain`]: SampleCore::domain +/// [`sample_with`]: SampleCore::sample_with +/// +/// # Example +/// ```rust +/// # use bevy_math::curve::*; +/// # use bevy_math::curve::builders::*; +/// enum InterpolationMode { +/// Linear, +/// Step, +/// } +/// +/// trait LinearInterpolate { +/// fn lerp(&self, other: &Self, t: f32) -> Self; +/// } +/// +/// fn step(first: &T, second: &T, t: f32) -> T { +/// if t >= 1.0 { +/// second.clone() +/// } else { +/// first.clone() +/// } +/// } +/// +/// struct MyCurve { +/// core: SampleCore, +/// interpolation_mode: InterpolationMode, +/// } +/// +/// impl Curve for MyCurve +/// where +/// T: LinearInterpolate + Clone, +/// { +/// fn domain(&self) -> Interval { +/// self.core.domain() +/// } +/// +/// fn sample(&self, t: f32) -> T { +/// match self.interpolation_mode { +/// InterpolationMode::Linear => self.core.sample_with(t, ::lerp), +/// InterpolationMode::Step => self.core.sample_with(t, step), +/// } +/// } +/// } +/// ``` +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct SampleCore { + /// 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, +} + +/// An error indicating that a [`SampleCore`] could not be constructed. +#[derive(Debug, Error)] +pub enum SampleCoreError { + /// Not enough samples were provided. + #[error("Need at least two samples to create a SampleCore, but {samples} were provided")] + NotEnoughSamples { + /// The number of samples that were provided. + samples: usize, + }, + + /// Unbounded domains are not compatible with `SampleCore`. + #[error("Cannot create a SampleCore over a domain with an infinite endpoint")] + InfiniteDomain, +} + +impl SampleCore { + /// Create a new [`SampleCore`] from the specified `domain` and `samples`. 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 Into>) -> Result { + let samples: Vec = samples.into(); + if samples.len() < 2 { + return Err(SampleCoreError::NotEnoughSamples { + samples: samples.len(), + }); + } + if !domain.is_finite() { + return Err(SampleCoreError::InfiniteDomain); + } + + Ok(SampleCore { 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(&self, t: f32, interpolation: I) -> T + where + T: Clone, + I: Fn(&T, &T, f32) -> T, + { + // Inside the curve itself, we interpolate between the two nearest sample values. + let subdivs = self.samples.len() - 1; + let step = self.domain.length() / subdivs as f32; + let t_shifted = t - self.domain.start(); + let steps_taken = t_shifted / step; + + // Using `steps_taken` as the source of truth, clamp to the range of valid indices. + if steps_taken <= 0.0 { + self.samples.first().unwrap().clone() + } else if steps_taken >= (self.samples.len() - 1) as f32 { + self.samples.last().unwrap().clone() + } else { + // Here we use only the floor and the fractional part of `steps_taken` to interpolate + // between the two nearby sample points. + let lower_index = steps_taken.floor() as usize; + + // Explicitly clamp the lower index just in case. + let lower_index = lower_index.min(self.samples.len() - 2); + let upper_index = lower_index + 1; + let fract = steps_taken.fract(); + interpolation( + &self.samples[lower_index], + &self.samples[upper_index], + fract, + ) + } + } +} + +/// 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`]. +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct UnevenSampleCore { + /// 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, + + /// The samples corresponding to the times for this curve. + /// + /// # Invariants + /// This must always have the same length as `times`. + pub samples: Vec, +} + +/// An error indicating that an [`UnevenSampleCore`] could not be constructed. +#[derive(Debug, Error)] +pub enum UnevenSampleCoreError { + /// Not enough samples were provided. + #[error( + "Need at least two samples to create an UnevenSampleCore, but {samples} were provided" + )] + NotEnoughSamples { + /// The number of samples that were provided. + samples: usize, + }, +} + +impl UnevenSampleCore { + /// Create a new [`UnevenSampleCore`] 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 Into>) -> Result { + let mut timed_samples: Vec<(f32, T)> = timed_samples.into(); + // Use default Equal to not do anything in case NAN appears; it will get removed in the + // next step anyway. + timed_samples + .sort_by(|(t0, _), (t1, _)| t0.partial_cmp(t1).unwrap_or(std::cmp::Ordering::Equal)); + let (times, samples): (Vec, Vec) = timed_samples + .into_iter() + .filter(|(t, _)| t.is_finite()) + .unzip(); + if times.len() < 2 { + return Err(UnevenSampleCoreError::NotEnoughSamples { + samples: times.len(), + }); + } + Ok(UnevenSampleCore { 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(&self, t: f32, interpolation: I) -> T + where + T: Clone, + I: Fn(&T, &T, f32) -> T, + { + match self + .times + .binary_search_by(|pt| pt.partial_cmp(&t).unwrap()) + { + Ok(index) => self.samples[index].clone(), + Err(index) => { + if index == 0 { + self.samples.first().unwrap().clone() + } else if index == self.times.len() { + self.samples.last().unwrap().clone() + } else { + let t_lower = self.times[index - 1]; + let v_lower = self.samples.get(index - 1).unwrap(); + let t_upper = self.times[index]; + let v_upper = self.samples.get(index).unwrap(); + let s = (t - t_lower) / (t_upper - t_lower); + interpolation(v_lower, v_upper, s) + } + } + } + } + + /// 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 sample times of the curve. + pub fn map_sample_times(mut self, f: impl Fn(f32) -> f32) -> UnevenSampleCore { + let mut timed_samples: Vec<(f32, T)> = + self.times.into_iter().map(f).zip(self.samples).collect(); + timed_samples.dedup_by(|(t1, _), (t2, _)| (*t1).eq(t2)); + timed_samples.sort_by(|(t1, _), (t2, _)| t1.partial_cmp(t2).unwrap()); + self.times = timed_samples.iter().map(|(t, _)| t).copied().collect(); + self.samples = timed_samples.into_iter().map(|(_, x)| x).collect(); + self + } +} + +/// The data core of a curve using uneven samples taken more than one at a time. +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct ChunkedUnevenSampleCore { + times: Vec, + samples_serial: Vec, + chunk_width: usize, +} diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 6bdc63f8d1139..4d8df0d4611b9 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -1,12 +1,14 @@ //! The [`Curve`] trait, used to describe curves in a number of different domains. This module also //! contains the [`Interpolable`] trait and the [`Interval`] type. +pub mod builders; pub mod interpolable; pub mod interval; pub use interpolable::Interpolable; pub use interval::{everywhere, interval, Interval}; +use builders::{SampleCore, SampleCoreError, UnevenSampleCore, UnevenSampleCoreError}; use interval::{InfiniteIntervalError, InvalidIntervalError}; use std::{marker::PhantomData, ops::Deref}; use thiserror::Error; @@ -51,33 +53,6 @@ pub trait Curve { self.sample(t) } - /// Resample this [`Curve`] to produce a new one that is defined by interpolation over equally - /// spaced values. A total of `samples` samples are used, although at least two samples are - /// required in order to produce well-formed output. If fewer than two samples are provided, - /// or if this curve has an unbounded domain, then a [`ResamplingError`] is returned. - fn resample_auto(&self, samples: usize) -> Result, ResamplingError> - where - T: Interpolable, - { - if samples < 2 { - return Err(ResamplingError::NotEnoughSamples(samples)); - } - if !self.domain().is_finite() { - return Err(ResamplingError::InfiniteInterval(InfiniteIntervalError)); - } - - let samples: Vec = self - .domain() - .spaced_points(samples) - .unwrap() - .map(|t| self.sample(t)) - .collect(); - Ok(SampleAutoCurve { - domain: self.domain(), - samples, - }) - } - /// Resample this [`Curve`] to produce a new one that is defined by interpolation over equally /// spaced values, using the provided `interpolation` to interpolate between adjacent samples. /// A total of `samples` samples are used, although at least two samples are required to produce @@ -119,12 +94,43 @@ pub trait Curve { .map(|t| self.sample(t)) .collect(); Ok(SampleCurve { - domain: self.domain(), - samples, + core: SampleCore { + domain: self.domain(), + samples, + }, interpolation, }) } + /// Resample this [`Curve`] to produce a new one that is defined by interpolation over equally + /// spaced values. A total of `samples` samples are used, although at least two samples are + /// required in order to produce well-formed output. If fewer than two samples are provided, + /// or if this curve has an unbounded domain, then a [`ResamplingError`] is returned. + fn resample_auto(&self, samples: usize) -> Result, ResamplingError> + where + T: Interpolable, + { + if samples < 2 { + return Err(ResamplingError::NotEnoughSamples(samples)); + } + if !self.domain().is_finite() { + return Err(ResamplingError::InfiniteInterval(InfiniteIntervalError)); + } + + let samples: Vec = self + .domain() + .spaced_points(samples) + .unwrap() + .map(|t| self.sample(t)) + .collect(); + Ok(SampleAutoCurve { + core: SampleCore { + 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, ResamplingError> { @@ -145,22 +151,28 @@ pub trait Curve { } /// Resample this [`Curve`] to produce a new one that is defined by 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. + /// 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 [`UnevenSampleAutoCurve`] stretches between the first and last - /// sample times of the iterator. - fn resample_uneven_auto( + /// 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( &self, sample_times: impl IntoIterator, - ) -> Result, ResamplingError> + interpolation: I, + ) -> Result, ResamplingError> where Self: Sized, - T: Interpolable, + I: Fn(&T, &T, f32) -> T, { let mut times: Vec = sample_times .into_iter() @@ -172,32 +184,29 @@ pub trait Curve { } times.sort_by(|t1, t2| t1.partial_cmp(t2).unwrap()); let samples = times.iter().copied().map(|t| self.sample(t)).collect(); - Ok(UnevenSampleAutoCurve { times, samples }) + Ok(UnevenSampleCurve { + core: UnevenSampleCore { times, samples }, + interpolation, + }) } /// 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. + /// 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 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( + /// The domain of the produced [`UnevenSampleAutoCurve`] stretches between the first and last + /// sample times of the iterator. + fn resample_uneven_auto( &self, sample_times: impl IntoIterator, - interpolation: I, - ) -> Result, ResamplingError> + ) -> Result, ResamplingError> where Self: Sized, - I: Fn(&T, &T, f32) -> T, + T: Interpolable, { let mut times: Vec = sample_times .into_iter() @@ -209,10 +218,8 @@ pub trait Curve { } times.sort_by(|t1, t2| t1.partial_cmp(t2).unwrap()); let samples = times.iter().copied().map(|t| self.sample(t)).collect(); - Ok(UnevenSampleCurve { - times, - samples, - interpolation, + Ok(UnevenSampleAutoCurve { + core: UnevenSampleCore { times, samples }, }) } @@ -418,63 +425,10 @@ where } } -/// 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))] -pub struct SampleAutoCurve -where - T: Interpolable, -{ - domain: Interval, - /// The samples that make up this [`SampleCurve`] by interpolation. - /// - /// Invariant: this must always have a length of at least 2. - samples: Vec, -} - -impl Curve for SampleAutoCurve -where - T: Interpolable, -{ - #[inline] - fn domain(&self) -> Interval { - self.domain - } - - #[inline] - fn sample(&self, t: f32) -> T { - // Inside the curve itself, we interpolate between the two nearest sample values. - let subdivs = self.samples.len() - 1; - let step = self.domain.length() / subdivs as f32; - let t_shifted = t - self.domain.start(); - let steps_taken = t_shifted / step; - - // Using `steps_taken` as the source of truth, clamp to the range of valid indices. - if steps_taken <= 0.0 { - self.samples.first().unwrap().clone() - } else if steps_taken >= (self.samples.len() - 1) as f32 { - self.samples.last().unwrap().clone() - } else { - // Here we use only the floor and the fractional part of `steps_taken` to interpolate - // between the two nearby sample points. - let lower_index = steps_taken.floor() as usize; - - // Explicitly clamp the lower index just in case. - let lower_index = lower_index.min(self.samples.len() - 2); - let upper_index = lower_index + 1; - let fract = steps_taken.fract(); - self.samples[lower_index].interpolate(&self.samples[upper_index], fract) - } - } -} - /// A [`Curve`] that is defined by explicit neighbor interpolation over a set of samples. +#[derive(Clone, Debug)] pub struct SampleCurve { - domain: Interval, - /// The samples that make up this curve by interpolation. - /// - /// Invariant: this must always have a length of at least 2. - samples: Vec, + core: SampleCore, interpolation: I, } @@ -485,37 +439,12 @@ where { #[inline] fn domain(&self) -> Interval { - self.domain + self.core.domain() } #[inline] fn sample(&self, t: f32) -> T { - // Inside the curve itself, we interpolate between the two nearest sample values. - let subdivs = self.samples.len() - 1; - let step = self.domain.length() / subdivs as f32; - let t_shifted = t - self.domain.start(); - let steps_taken = t_shifted / step; - - // Using `steps_taken` as the source of truth, clamp to the range of valid indices. - if steps_taken <= 0.0 { - self.samples.first().unwrap().clone() - } else if steps_taken >= (self.samples.len() - 1) as f32 { - self.samples.last().unwrap().clone() - } else { - // Here we use only the floor and the fractional part of `steps_taken` to interpolate - // between the two nearby sample points. - let lower_index = steps_taken.floor() as usize; - - // Explicitly clamp the lower index just in case. - let lower_index = lower_index.min(self.samples.len() - 2); - let upper_index = lower_index + 1; - let fract = steps_taken.fract(); - (self.interpolation)( - &self.samples[lower_index], - &self.samples[upper_index], - fract, - ) - } + self.core.sample_with(t, &self.interpolation) } } @@ -531,115 +460,55 @@ impl SampleCurve { domain: Interval, samples: impl Into>, interpolation: I, - ) -> Result + ) -> Result where I: Fn(&T, &T, f32) -> T, { - let samples: Vec = samples.into(); - if samples.len() < 2 { - return Err(ResamplingError::NotEnoughSamples(samples.len())); - } - if !domain.is_finite() { - return Err(ResamplingError::InfiniteInterval(InfiniteIntervalError)); - } - - Ok(SampleCurve { - domain, - samples, + Ok(Self { + core: SampleCore::new(domain, samples)?, interpolation, }) } } -/// A [`Curve`] that is defined by interpolation over unevenly spaced samples. +/// 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))] -pub struct UnevenSampleAutoCurve -where - T: Interpolable, -{ - /// 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. - times: Vec, - - /// The samples corresponding to the times for this curve. - /// - /// Invariants: This must always have the same length as `times`. - samples: Vec, -} - -impl UnevenSampleAutoCurve -where - T: Interpolable, -{ - /// 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(mut self, f: impl Fn(f32) -> f32) -> UnevenSampleAutoCurve { - let mut timed_samples: Vec<(f32, T)> = - self.times.into_iter().map(f).zip(self.samples).collect(); - timed_samples.dedup_by(|(t1, _), (t2, _)| (*t1).eq(t2)); - timed_samples.sort_by(|(t1, _), (t2, _)| t1.partial_cmp(t2).unwrap()); - self.times = timed_samples.iter().map(|(t, _)| t).copied().collect(); - self.samples = timed_samples.into_iter().map(|(_, x)| x).collect(); - self - } +pub struct SampleAutoCurve { + core: SampleCore, } -impl Curve for UnevenSampleAutoCurve +impl Curve for SampleAutoCurve where T: Interpolable, { #[inline] fn domain(&self) -> Interval { - let start = self.times.first().unwrap(); - let end = self.times.last().unwrap(); - Interval::new(*start, *end).unwrap() + self.core.domain() } #[inline] fn sample(&self, t: f32) -> T { - match self - .times - .binary_search_by(|pt| pt.partial_cmp(&t).unwrap()) - { - Ok(index) => self.samples[index].clone(), - Err(index) => { - if index == 0 { - self.samples.first().unwrap().clone() - } else if index == self.times.len() { - self.samples.last().unwrap().clone() - } else { - let t_lower = self.times[index - 1]; - let v_lower = self.samples.get(index - 1).unwrap(); - let t_upper = self.times[index]; - let v_upper = self.samples.get(index).unwrap(); - let s = (t - t_lower) / (t_upper - t_lower); - v_lower.interpolate(v_upper, s) - } - } - } + self.core.sample_with(t, ::interpolate) + } +} + +impl SampleAutoCurve { + /// 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 Into>) -> Result { + Ok(Self { + core: SampleCore::new(domain, samples)?, + }) } } /// A [`Curve`] that is defined by interpolation over unevenly spaced samples with explicit /// interpolation. +#[derive(Clone, Debug)] pub struct UnevenSampleCurve { - /// 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. - times: Vec, - - /// The samples corresponding to the times for this curve. - /// - /// Invariants: This must always have the same length as `times`. - samples: Vec, + core: UnevenSampleCore, interpolation: I, } @@ -650,33 +519,12 @@ where { #[inline] fn domain(&self) -> Interval { - let start = self.times.first().unwrap(); - let end = self.times.last().unwrap(); - Interval::new(*start, *end).unwrap() + self.core.domain() } #[inline] fn sample(&self, t: f32) -> T { - match self - .times - .binary_search_by(|pt| pt.partial_cmp(&t).unwrap()) - { - Ok(index) => self.samples[index].clone(), - Err(index) => { - if index == 0 { - self.samples.first().unwrap().clone() - } else if index == self.times.len() { - self.samples.last().unwrap().clone() - } else { - let t_lower = self.times[index - 1]; - let v_lower = self.samples.get(index - 1).unwrap(); - let t_upper = self.times[index]; - let v_upper = self.samples.get(index).unwrap(); - let s = (t - t_lower) / (t_upper - t_lower); - (self.interpolation)(v_lower, v_upper, s) - } - } - } + self.core.sample_with(t, &self.interpolation) } } @@ -692,22 +540,9 @@ impl UnevenSampleCurve { pub fn new( timed_samples: impl Into>, interpolation: I, - ) -> Result { - let mut timed_samples: Vec<(f32, T)> = timed_samples.into(); - // Use default Equal to not do anything in case NAN appears; it will get removed in the - // next step anyway. - timed_samples - .sort_by(|(t0, _), (t1, _)| t0.partial_cmp(t1).unwrap_or(std::cmp::Ordering::Equal)); - let (times, samples): (Vec, Vec) = timed_samples - .into_iter() - .filter(|(t, _)| t.is_finite()) - .unzip(); - if times.len() < 2 { - return Err(ResamplingError::NotEnoughSamples(times.len())); - } - Ok(UnevenSampleCurve { - times, - samples, + ) -> Result { + Ok(Self { + core: UnevenSampleCore::new(timed_samples)?, interpolation, }) } @@ -718,14 +553,57 @@ impl UnevenSampleCurve { /// /// 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(mut self, f: impl Fn(f32) -> f32) -> UnevenSampleCurve { - let mut timed_samples: Vec<(f32, T)> = - self.times.into_iter().map(f).zip(self.samples).collect(); - timed_samples.dedup_by(|(t1, _), (t2, _)| (*t1).eq(t2)); - timed_samples.sort_by(|(t1, _), (t2, _)| t1.partial_cmp(t2).unwrap()); - self.times = timed_samples.iter().map(|(t, _)| t).copied().collect(); - self.samples = timed_samples.into_iter().map(|(_, x)| x).collect(); - self + pub fn map_sample_times(self, f: impl Fn(f32) -> f32) -> UnevenSampleCurve { + 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))] +pub struct UnevenSampleAutoCurve { + core: UnevenSampleCore, +} + +impl Curve for UnevenSampleAutoCurve +where + T: Interpolable, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample(&self, t: f32) -> T { + self.core.sample_with(t, ::interpolate) + } +} + +impl UnevenSampleAutoCurve { + /// 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 Into>) -> Result { + Ok(Self { + core: UnevenSampleCore::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 { + Self { + core: self.core.map_sample_times(f), + } } } From d8e45daa5541dab2c37e93ac2b30ec8c70001b1f Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 10 Jun 2024 16:19:43 -0400 Subject: [PATCH 24/44] Rename builders -> cores, refactoring to Betweenness --- crates/bevy_math/src/curve/builders.rs | 285 -------------- crates/bevy_math/src/curve/cores.rs | 503 +++++++++++++++++++++++++ crates/bevy_math/src/curve/mod.rs | 36 +- 3 files changed, 521 insertions(+), 303 deletions(-) delete mode 100644 crates/bevy_math/src/curve/builders.rs create mode 100644 crates/bevy_math/src/curve/cores.rs diff --git a/crates/bevy_math/src/curve/builders.rs b/crates/bevy_math/src/curve/builders.rs deleted file mode 100644 index 9af37df0ce43f..0000000000000 --- a/crates/bevy_math/src/curve/builders.rs +++ /dev/null @@ -1,285 +0,0 @@ -//! Core data structures to be used internally in Curve implementations. - -use super::interval::Interval; -use thiserror::Error; - -/// 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. -/// -/// [the provided constructor]: SampleCore::new -/// [`domain`]: SampleCore::domain -/// [`sample_with`]: SampleCore::sample_with -/// -/// # Example -/// ```rust -/// # use bevy_math::curve::*; -/// # use bevy_math::curve::builders::*; -/// enum InterpolationMode { -/// Linear, -/// Step, -/// } -/// -/// trait LinearInterpolate { -/// fn lerp(&self, other: &Self, t: f32) -> Self; -/// } -/// -/// fn step(first: &T, second: &T, t: f32) -> T { -/// if t >= 1.0 { -/// second.clone() -/// } else { -/// first.clone() -/// } -/// } -/// -/// struct MyCurve { -/// core: SampleCore, -/// interpolation_mode: InterpolationMode, -/// } -/// -/// impl Curve for MyCurve -/// where -/// T: LinearInterpolate + Clone, -/// { -/// fn domain(&self) -> Interval { -/// self.core.domain() -/// } -/// -/// fn sample(&self, t: f32) -> T { -/// match self.interpolation_mode { -/// InterpolationMode::Linear => self.core.sample_with(t, ::lerp), -/// InterpolationMode::Step => self.core.sample_with(t, step), -/// } -/// } -/// } -/// ``` -#[derive(Debug, Clone)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -pub struct SampleCore { - /// 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, -} - -/// An error indicating that a [`SampleCore`] could not be constructed. -#[derive(Debug, Error)] -pub enum SampleCoreError { - /// Not enough samples were provided. - #[error("Need at least two samples to create a SampleCore, but {samples} were provided")] - NotEnoughSamples { - /// The number of samples that were provided. - samples: usize, - }, - - /// Unbounded domains are not compatible with `SampleCore`. - #[error("Cannot create a SampleCore over a domain with an infinite endpoint")] - InfiniteDomain, -} - -impl SampleCore { - /// Create a new [`SampleCore`] from the specified `domain` and `samples`. 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 Into>) -> Result { - let samples: Vec = samples.into(); - if samples.len() < 2 { - return Err(SampleCoreError::NotEnoughSamples { - samples: samples.len(), - }); - } - if !domain.is_finite() { - return Err(SampleCoreError::InfiniteDomain); - } - - Ok(SampleCore { 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(&self, t: f32, interpolation: I) -> T - where - T: Clone, - I: Fn(&T, &T, f32) -> T, - { - // Inside the curve itself, we interpolate between the two nearest sample values. - let subdivs = self.samples.len() - 1; - let step = self.domain.length() / subdivs as f32; - let t_shifted = t - self.domain.start(); - let steps_taken = t_shifted / step; - - // Using `steps_taken` as the source of truth, clamp to the range of valid indices. - if steps_taken <= 0.0 { - self.samples.first().unwrap().clone() - } else if steps_taken >= (self.samples.len() - 1) as f32 { - self.samples.last().unwrap().clone() - } else { - // Here we use only the floor and the fractional part of `steps_taken` to interpolate - // between the two nearby sample points. - let lower_index = steps_taken.floor() as usize; - - // Explicitly clamp the lower index just in case. - let lower_index = lower_index.min(self.samples.len() - 2); - let upper_index = lower_index + 1; - let fract = steps_taken.fract(); - interpolation( - &self.samples[lower_index], - &self.samples[upper_index], - fract, - ) - } - } -} - -/// 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`]. -#[derive(Debug, Clone)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -pub struct UnevenSampleCore { - /// 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, - - /// The samples corresponding to the times for this curve. - /// - /// # Invariants - /// This must always have the same length as `times`. - pub samples: Vec, -} - -/// An error indicating that an [`UnevenSampleCore`] could not be constructed. -#[derive(Debug, Error)] -pub enum UnevenSampleCoreError { - /// Not enough samples were provided. - #[error( - "Need at least two samples to create an UnevenSampleCore, but {samples} were provided" - )] - NotEnoughSamples { - /// The number of samples that were provided. - samples: usize, - }, -} - -impl UnevenSampleCore { - /// Create a new [`UnevenSampleCore`] 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 Into>) -> Result { - let mut timed_samples: Vec<(f32, T)> = timed_samples.into(); - // Use default Equal to not do anything in case NAN appears; it will get removed in the - // next step anyway. - timed_samples - .sort_by(|(t0, _), (t1, _)| t0.partial_cmp(t1).unwrap_or(std::cmp::Ordering::Equal)); - let (times, samples): (Vec, Vec) = timed_samples - .into_iter() - .filter(|(t, _)| t.is_finite()) - .unzip(); - if times.len() < 2 { - return Err(UnevenSampleCoreError::NotEnoughSamples { - samples: times.len(), - }); - } - Ok(UnevenSampleCore { 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(&self, t: f32, interpolation: I) -> T - where - T: Clone, - I: Fn(&T, &T, f32) -> T, - { - match self - .times - .binary_search_by(|pt| pt.partial_cmp(&t).unwrap()) - { - Ok(index) => self.samples[index].clone(), - Err(index) => { - if index == 0 { - self.samples.first().unwrap().clone() - } else if index == self.times.len() { - self.samples.last().unwrap().clone() - } else { - let t_lower = self.times[index - 1]; - let v_lower = self.samples.get(index - 1).unwrap(); - let t_upper = self.times[index]; - let v_upper = self.samples.get(index).unwrap(); - let s = (t - t_lower) / (t_upper - t_lower); - interpolation(v_lower, v_upper, s) - } - } - } - } - - /// 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 sample times of the curve. - pub fn map_sample_times(mut self, f: impl Fn(f32) -> f32) -> UnevenSampleCore { - let mut timed_samples: Vec<(f32, T)> = - self.times.into_iter().map(f).zip(self.samples).collect(); - timed_samples.dedup_by(|(t1, _), (t2, _)| (*t1).eq(t2)); - timed_samples.sort_by(|(t1, _), (t2, _)| t1.partial_cmp(t2).unwrap()); - self.times = timed_samples.iter().map(|(t, _)| t).copied().collect(); - self.samples = timed_samples.into_iter().map(|(_, x)| x).collect(); - self - } -} - -/// The data core of a curve using uneven samples taken more than one at a time. -#[derive(Debug, Clone)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -pub struct ChunkedUnevenSampleCore { - times: Vec, - samples_serial: Vec, - chunk_width: usize, -} diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs new file mode 100644 index 0000000000000..57a13cdeee49b --- /dev/null +++ b/crates/bevy_math/src/curve/cores.rs @@ -0,0 +1,503 @@ +//! Core data structures to be used internally in Curve implementations, encapsulating storage +//! and access patterns for reuse. + +use super::interval::Interval; +use thiserror::Error; + +/// This type expresses the relationship of a value to a linear collection of values. It is a kind +/// of summary used intermediately by sampling operations. +pub enum Betweenness { + /// This value lies exactly on another. + 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 Betweenness { + /// Map all values using a given function `f`, leaving the interpolation parameters in any + /// [`Between`] variants unchanged. + /// + /// [`Between`]: `Betweenness::Between` + #[must_use] + pub fn map(self, f: impl Fn(T) -> S) -> Betweenness { + match self { + Betweenness::Exact(v) => Betweenness::Exact(f(v)), + Betweenness::LeftTail(v) => Betweenness::LeftTail(f(v)), + Betweenness::RightTail(v) => Betweenness::RightTail(f(v)), + Betweenness::Between(u, v, s) => Betweenness::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. +/// +/// [the provided constructor]: EvenCore::new +/// [`domain`]: EvenCore::domain +/// [`sample_with`]: EvenCore::sample_with +/// +/// # Example +/// ```rust +/// # use bevy_math::curve::*; +/// # use bevy_math::curve::builders::*; +/// enum InterpolationMode { +/// Linear, +/// Step, +/// } +/// +/// trait LinearInterpolate { +/// fn lerp(&self, other: &Self, t: f32) -> Self; +/// } +/// +/// fn step(first: &T, second: &T, t: f32) -> T { +/// if t >= 1.0 { +/// second.clone() +/// } else { +/// first.clone() +/// } +/// } +/// +/// struct MyCurve { +/// core: EvenCore, +/// interpolation_mode: InterpolationMode, +/// } +/// +/// impl Curve for MyCurve +/// where +/// T: LinearInterpolate + Clone, +/// { +/// fn domain(&self) -> Interval { +/// self.core.domain() +/// } +/// +/// fn sample(&self, t: f32) -> T { +/// match self.interpolation_mode { +/// InterpolationMode::Linear => self.core.sample_with(t, ::lerp), +/// InterpolationMode::Step => self.core.sample_with(t, step), +/// } +/// } +/// } +/// ``` +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct EvenCore { + /// 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, +} + +/// An error indicating that a [`EvenCore`] could not be constructed. +#[derive(Debug, Error)] +pub enum EvenCoreError { + /// Not enough samples were provided. + #[error("Need at least two samples to create a 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 a domain with an infinite endpoint")] + InfiniteDomain, +} + +impl EvenCore { + /// Create a new [`EvenCore`] from the specified `domain` and `samples`. 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 Into>) -> Result { + let samples: Vec = samples.into(); + if samples.len() < 2 { + return Err(EvenCoreError::NotEnoughSamples { + samples: samples.len(), + }); + } + if !domain.is_finite() { + return Err(EvenCoreError::InfiniteDomain); + } + + 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(&self, t: f32, interpolation: I) -> T + where + T: Clone, + I: Fn(&T, &T, f32) -> T, + { + match even_betweenness(self.domain, self.samples.len(), t) { + Betweenness::Exact(idx) | Betweenness::LeftTail(idx) | Betweenness::RightTail(idx) => { + self.samples[idx].clone() + } + Betweenness::Between(lower_idx, upper_idx, s) => { + interpolation(&self.samples[lower_idx], &self.samples[upper_idx], s) + } + } + } + + /// Given a time `t`, obtain a [`Betweenness`] 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`]: `Betweenness::Between` + pub fn sample_betweenness(&self, t: f32) -> Betweenness<&T> { + even_betweenness(self.domain, self.samples.len(), t).map(|idx| &self.samples[idx]) + } +} + +/// Given a domain and a number of samples taken over that interval, return a [`Betweenness`] +/// that governs how samples are extracted relative to the stored data. +/// +/// `domain` must be a bounded interval (i.e. `domain.is_finite() == 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_betweenness(domain: Interval, samples: usize, t: f32) -> Betweenness { + 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. + Betweenness::LeftTail(0) + } else if steps_taken >= subdivs as f32 { + // To the right side of all the samples + Betweenness::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(); + Betweenness::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`]. +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct UnevenCore { + /// 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, + + /// The samples corresponding to the times for this curve. + /// + /// # Invariants + /// This must always have the same length as `times`. + pub samples: Vec, +} + +/// An error indicating that an [`UnevenCore`] could not be constructed. +#[derive(Debug, Error)] +pub enum UnevenCoreError { + /// Not enough samples were provided. + #[error("Need at least two samples to create an UnevenCore, but {samples} were provided")] + NotEnoughSamples { + /// The number of samples that were provided. + samples: usize, + }, +} + +impl UnevenCore { + /// 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. + /// + /// 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 Into>) -> Result { + let timed_samples: Vec<(f32, T)> = timed_samples.into(); + + // Filter out non-finite sample times first so they don't interfere with sorting/deduplication. + let mut timed_samples: Vec<(f32, T)> = timed_samples + .into_iter() + .filter(|(t, _)| t.is_finite()) + .collect(); + timed_samples + .sort_by(|(t0, _), (t1, _)| t0.partial_cmp(t1).unwrap_or(std::cmp::Ordering::Equal)); + timed_samples.dedup_by_key(|(t, _)| *t); + + let (times, samples): (Vec, Vec) = timed_samples.into_iter().unzip(); + + if times.len() < 2 { + return Err(UnevenCoreError::NotEnoughSamples { + samples: times.len(), + }); + } + 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(&self, t: f32, interpolation: I) -> T + where + T: Clone, + I: Fn(&T, &T, f32) -> T, + { + match uneven_betweenness(&self.times, t) { + Betweenness::Exact(idx) | Betweenness::LeftTail(idx) | Betweenness::RightTail(idx) => { + self.samples[idx].clone() + } + Betweenness::Between(lower_idx, upper_idx, s) => { + interpolation(&self.samples[lower_idx], &self.samples[upper_idx], s) + } + } + } + + /// Given a time `t`, obtain a [`Betweenness`] 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`]: `Betweenness::Between` + pub fn sample_betweenness(&self, t: f32) -> Betweenness<&T> { + uneven_betweenness(&self.times, t).map(|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 sample times of the curve. + pub fn map_sample_times(mut self, f: impl Fn(f32) -> f32) -> UnevenCore { + let mut timed_samples: Vec<(f32, T)> = + self.times.into_iter().map(f).zip(self.samples).collect(); + timed_samples.dedup_by(|(t1, _), (t2, _)| (*t1).eq(t2)); + timed_samples.sort_by(|(t1, _), (t2, _)| t1.partial_cmp(t2).unwrap()); + self.times = timed_samples.iter().map(|(t, _)| t).copied().collect(); + self.samples = timed_samples.into_iter().map(|(_, x)| x).collect(); + 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 array. +/// +/// [sampling width]: ChunkedUnevenCore::width +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct ChunkedUnevenCore { + /// 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, + + /// The values that are used in sampling. Each `width` of these correspond to a single sample. + /// + /// # Invariants + /// This must always have a length of `width` times that of `times`. + pub values: Vec, + + /// The sampling width, determining how many consecutive elements of `values` are taken in a + /// single sample. + /// + /// # Invariants + /// This must never be zero. + pub width: usize, +} + +/// An error that indicates that a [`ChunkedUnevenCore`] could not be formed. +#[derive(Debug, Error)] +pub enum ChunkedUnevenSampleCoreError { + /// 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 samples to create an UnevenCore, 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 ChunkedUnevenCore { + /// 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 entries. + /// - `values` has the incorrect length relative to `times`. + /// + /// [type-level documentation]: ChunkedUnevenCore + pub fn new( + times: impl Into>, + values: impl Into>, + width: usize, + ) -> Result { + let times: Vec = times.into(); + let values: Vec = values.into(); + + if width == 0 { + return Err(ChunkedUnevenSampleCoreError::ZeroWidth); + } + + let times = filter_sort_dedup_times(times); + + if times.len() < 2 { + return Err(ChunkedUnevenSampleCoreError::NotEnoughSamples { + samples: times.len(), + }); + } + + if values.len() != times.len() * width { + return Err(ChunkedUnevenSampleCoreError::MismatchedLengths { + expected: times.len() * width, + actual: values.len(), + }); + } + + Ok(Self { + times, + values, + width, + }) + } + + /// 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() + } + + /// Given a time `t`, obtain a [`Betweenness`] 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`]: `Betweenness::Between` + #[inline] + pub fn sample_betweenness(&self, t: f32) -> Betweenness<&[T]> { + uneven_betweenness(&self.times, t).map(|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 lower_idx = self.width * idx; + let upper_idx = lower_idx + self.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: Vec) -> Vec { + // Filter before sorting/deduplication so that NAN doesn't interfere with them. + let mut times: Vec = times.into_iter().filter(|t| t.is_finite()).collect(); + times.sort_by(|t0, t1| t0.partial_cmp(t1).unwrap()); + times.dedup(); + times +} + +/// Given a list of `times` and a target value, get the betweenness 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_betweenness(times: &[f32], t: f32) -> Betweenness { + match times.binary_search_by(|pt| pt.partial_cmp(&t).unwrap()) { + Ok(index) => Betweenness::Exact(index), + Err(index) => { + if index == 0 { + // This is before the first keyframe. + Betweenness::LeftTail(0) + } else if index >= times.len() { + // This is after the last keyframe. + Betweenness::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); + Betweenness::Between(index - 1, index, s) + } + } + } +} diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 4d8df0d4611b9..d273e77ae81f1 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -1,14 +1,14 @@ //! The [`Curve`] trait, used to describe curves in a number of different domains. This module also //! contains the [`Interpolable`] trait and the [`Interval`] type. -pub mod builders; +pub mod cores; pub mod interpolable; pub mod interval; pub use interpolable::Interpolable; pub use interval::{everywhere, interval, Interval}; -use builders::{SampleCore, SampleCoreError, UnevenSampleCore, UnevenSampleCoreError}; +use cores::{EvenCore, EvenCoreError, UnevenCore, UnevenCoreError}; use interval::{InfiniteIntervalError, InvalidIntervalError}; use std::{marker::PhantomData, ops::Deref}; use thiserror::Error; @@ -94,7 +94,7 @@ pub trait Curve { .map(|t| self.sample(t)) .collect(); Ok(SampleCurve { - core: SampleCore { + core: EvenCore { domain: self.domain(), samples, }, @@ -124,7 +124,7 @@ pub trait Curve { .map(|t| self.sample(t)) .collect(); Ok(SampleAutoCurve { - core: SampleCore { + core: EvenCore { domain: self.domain(), samples, }, @@ -185,7 +185,7 @@ pub trait Curve { times.sort_by(|t1, t2| t1.partial_cmp(t2).unwrap()); let samples = times.iter().copied().map(|t| self.sample(t)).collect(); Ok(UnevenSampleCurve { - core: UnevenSampleCore { times, samples }, + core: UnevenCore { times, samples }, interpolation, }) } @@ -219,7 +219,7 @@ pub trait Curve { times.sort_by(|t1, t2| t1.partial_cmp(t2).unwrap()); let samples = times.iter().copied().map(|t| self.sample(t)).collect(); Ok(UnevenSampleAutoCurve { - core: UnevenSampleCore { times, samples }, + core: UnevenCore { times, samples }, }) } @@ -428,7 +428,7 @@ where /// A [`Curve`] that is defined by explicit neighbor interpolation over a set of samples. #[derive(Clone, Debug)] pub struct SampleCurve { - core: SampleCore, + core: EvenCore, interpolation: I, } @@ -460,12 +460,12 @@ impl SampleCurve { domain: Interval, samples: impl Into>, interpolation: I, - ) -> Result + ) -> Result where I: Fn(&T, &T, f32) -> T, { Ok(Self { - core: SampleCore::new(domain, samples)?, + core: EvenCore::new(domain, samples)?, interpolation, }) } @@ -475,7 +475,7 @@ impl SampleCurve { #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct SampleAutoCurve { - core: SampleCore, + core: EvenCore, } impl Curve for SampleAutoCurve @@ -497,9 +497,9 @@ impl SampleAutoCurve { /// 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 Into>) -> Result { + pub fn new(domain: Interval, samples: impl Into>) -> Result { Ok(Self { - core: SampleCore::new(domain, samples)?, + core: EvenCore::new(domain, samples)?, }) } } @@ -508,7 +508,7 @@ impl SampleAutoCurve { /// interpolation. #[derive(Clone, Debug)] pub struct UnevenSampleCurve { - core: UnevenSampleCore, + core: UnevenCore, interpolation: I, } @@ -540,9 +540,9 @@ impl UnevenSampleCurve { pub fn new( timed_samples: impl Into>, interpolation: I, - ) -> Result { + ) -> Result { Ok(Self { - core: UnevenSampleCore::new(timed_samples)?, + core: UnevenCore::new(timed_samples)?, interpolation, }) } @@ -565,7 +565,7 @@ impl UnevenSampleCurve { #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct UnevenSampleAutoCurve { - core: UnevenSampleCore, + core: UnevenCore, } impl Curve for UnevenSampleAutoCurve @@ -588,9 +588,9 @@ impl UnevenSampleAutoCurve { /// 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 Into>) -> Result { + pub fn new(timed_samples: impl Into>) -> Result { Ok(Self { - core: UnevenSampleCore::new(timed_samples)?, + core: UnevenCore::new(timed_samples)?, }) } From 52f14c1a3d547ec335698255628938298490bbbb Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 10 Jun 2024 16:31:21 -0400 Subject: [PATCH 25/44] Kill Interpolable and transition to StableInterpolate --- crates/bevy_math/src/curve/interpolable.rs | 39 ---------------------- crates/bevy_math/src/curve/mod.rs | 20 ++++++----- 2 files changed, 11 insertions(+), 48 deletions(-) delete mode 100644 crates/bevy_math/src/curve/interpolable.rs diff --git a/crates/bevy_math/src/curve/interpolable.rs b/crates/bevy_math/src/curve/interpolable.rs deleted file mode 100644 index 9458e1528c8e6..0000000000000 --- a/crates/bevy_math/src/curve/interpolable.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! The [`Interpolable`] trait for types that support interpolation between two values. - -use crate::{Quat, VectorSpace}; - -/// A trait for types whose values can be intermediately interpolated between two given values -/// with an auxiliary parameter. -pub trait Interpolable: Clone { - /// Interpolate between this value and the `other` given value using the parameter `t`. - /// Note that the parameter `t` is not necessarily clamped to lie between `0` and `1`. - fn interpolate(&self, other: &Self, t: f32) -> Self; -} - -impl Interpolable for (S, T) -where - S: Interpolable, - T: Interpolable, -{ - fn interpolate(&self, other: &Self, t: f32) -> Self { - ( - self.0.interpolate(&other.0, t), - self.1.interpolate(&other.1, t), - ) - } -} - -impl Interpolable for T -where - T: VectorSpace, -{ - fn interpolate(&self, other: &Self, t: f32) -> Self { - self.lerp(*other, t) - } -} - -impl Interpolable for Quat { - fn interpolate(&self, other: &Self, t: f32) -> Self { - self.slerp(*other, t) - } -} diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index d273e77ae81f1..ac070254f1170 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -1,13 +1,13 @@ //! The [`Curve`] trait, used to describe curves in a number of different domains. This module also -//! contains the [`Interpolable`] trait and the [`Interval`] type. +//! contains the [`Interval`] type, along with a selection of core data structures used to back +//! curves that are interpolated from samples. pub mod cores; -pub mod interpolable; pub mod interval; -pub use interpolable::Interpolable; pub use interval::{everywhere, interval, Interval}; +use crate::StableInterpolate; use cores::{EvenCore, EvenCoreError, UnevenCore, UnevenCoreError}; use interval::{InfiniteIntervalError, InvalidIntervalError}; use std::{marker::PhantomData, ops::Deref}; @@ -108,7 +108,7 @@ pub trait Curve { /// or if this curve has an unbounded domain, then a [`ResamplingError`] is returned. fn resample_auto(&self, samples: usize) -> Result, ResamplingError> where - T: Interpolable, + T: StableInterpolate, { if samples < 2 { return Err(ResamplingError::NotEnoughSamples(samples)); @@ -206,7 +206,7 @@ pub trait Curve { ) -> Result, ResamplingError> where Self: Sized, - T: Interpolable, + T: StableInterpolate, { let mut times: Vec = sample_times .into_iter() @@ -480,7 +480,7 @@ pub struct SampleAutoCurve { impl Curve for SampleAutoCurve where - T: Interpolable, + T: StableInterpolate, { #[inline] fn domain(&self) -> Interval { @@ -489,7 +489,8 @@ where #[inline] fn sample(&self, t: f32) -> T { - self.core.sample_with(t, ::interpolate) + self.core + .sample_with(t, ::interpolate_stable) } } @@ -570,7 +571,7 @@ pub struct UnevenSampleAutoCurve { impl Curve for UnevenSampleAutoCurve where - T: Interpolable, + T: StableInterpolate, { #[inline] fn domain(&self) -> Interval { @@ -579,7 +580,8 @@ where #[inline] fn sample(&self, t: f32) -> T { - self.core.sample_with(t, ::interpolate) + self.core + .sample_with(t, ::interpolate_stable) } } From 8113c7fb8aa541d40ce7627a969a42e6e2c7ac1d Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 10 Jun 2024 17:36:29 -0400 Subject: [PATCH 26/44] Derive Reflect on many things --- crates/bevy_math/src/curve/cores.rs | 22 ++++++++++-- crates/bevy_math/src/curve/interval.rs | 10 ++++++ crates/bevy_math/src/curve/mod.rs | 48 ++++++++++++++++++-------- 3 files changed, 63 insertions(+), 17 deletions(-) diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs index 57a13cdeee49b..829321e31a338 100644 --- a/crates/bevy_math/src/curve/cores.rs +++ b/crates/bevy_math/src/curve/cores.rs @@ -2,12 +2,19 @@ //! and access patterns for reuse. use super::interval::Interval; +use core::fmt::Debug; use thiserror::Error; +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::Reflect; + /// This type expresses the relationship of a value to a linear 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 Betweenness { - /// This value lies exactly on another. + /// 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. @@ -90,8 +97,9 @@ impl Betweenness { /// } /// } /// ``` -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub struct EvenCore { /// The domain over which the samples are taken, which corresponds to the domain of the curve /// formed by interpolating them. @@ -108,7 +116,9 @@ pub struct EvenCore { } /// An error indicating that a [`EvenCore`] could not be constructed. -#[derive(Debug, Error)] +#[derive(Debug, Error, PartialEq, Eq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub enum EvenCoreError { /// Not enough samples were provided. #[error("Need at least two samples to create a EvenCore, but {samples} were provided")] @@ -214,6 +224,7 @@ pub fn even_betweenness(domain: Interval, samples: usize, t: f32) -> Betweenness /// order to implement the curve interface using [`domain`] and [`sample_with`]. #[derive(Debug, Clone)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub struct UnevenCore { /// The times for the samples of this curve. /// @@ -231,6 +242,8 @@ pub struct UnevenCore { /// An error indicating that an [`UnevenCore`] could not be constructed. #[derive(Debug, Error)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub enum UnevenCoreError { /// Not enough samples were provided. #[error("Need at least two samples to create an UnevenCore, but {samples} were provided")] @@ -338,6 +351,7 @@ impl UnevenCore { /// [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 { /// The times, one for each sample. /// @@ -362,6 +376,8 @@ pub struct ChunkedUnevenCore { /// An error that indicates that a [`ChunkedUnevenCore`] could not be formed. #[derive(Debug, Error)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub enum ChunkedUnevenSampleCoreError { /// The width of a `ChunkedUnevenCore` cannot be zero. #[error("Chunk width must be at least 1")] diff --git a/crates/bevy_math/src/curve/interval.rs b/crates/bevy_math/src/curve/interval.rs index 0a1d8b2739c94..834ded8443007 100644 --- a/crates/bevy_math/src/curve/interval.rs +++ b/crates/bevy_math/src/curve/interval.rs @@ -6,9 +6,19 @@ use std::{ }; use thiserror::Error; +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::Reflect; +#[cfg(all(feature = "serialize", feature = "bevy_reflect"))] +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; + /// A nonempty closed interval, possibly infinite in either direction. #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] pub struct Interval { start: f32, end: f32, diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index ac070254f1170..99cd2914ac412 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -13,19 +13,8 @@ use interval::{InfiniteIntervalError, InvalidIntervalError}; use std::{marker::PhantomData, ops::Deref}; use thiserror::Error; -/// 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 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")] - InfiniteInterval(InfiniteIntervalError), -} +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::Reflect; /// A trait for a type that can represent values of type `T` parametrized over a fixed interval. /// Typical examples of this are actual geometric curves where `T: VectorSpace`, but other kinds @@ -67,7 +56,7 @@ pub trait Curve { /// ``` /// # use bevy_math::*; /// # use bevy_math::curve::*; - /// let quarter_rotation = function_curve(interval(0.0, 90.0).unwrap(), |t| Rotation2d::degrees(t)); + /// 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)); /// ``` @@ -374,9 +363,24 @@ where } } +/// 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 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")] + InfiniteInterval(InfiniteIntervalError), +} + /// A [`Curve`] which takes a constant value over its domain. #[derive(Clone, Copy, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub struct ConstantCurve where T: Clone, @@ -402,6 +406,8 @@ where /// A [`Curve`] defined by a function. #[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub struct FunctionCurve where F: Fn(f32) -> T, @@ -427,6 +433,8 @@ 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 { core: EvenCore, interpolation: I, @@ -474,6 +482,7 @@ impl SampleCurve { /// 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 { core: EvenCore, } @@ -508,6 +517,8 @@ impl SampleAutoCurve { /// 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 { core: UnevenCore, interpolation: I, @@ -565,6 +576,7 @@ impl UnevenSampleCurve { /// 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 { core: UnevenCore, } @@ -612,6 +624,8 @@ impl UnevenSampleAutoCurve { /// A [`Curve`] whose samples are defined by mapping samples from another curve through a /// given function. #[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub struct MapCurve where C: Curve, @@ -667,6 +681,8 @@ where /// A [`Curve`] whose sample space is mapped onto that of some base curve's before sampling. #[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub struct ReparamCurve where C: Curve, @@ -728,6 +744,8 @@ where /// Briefly, the point is that the curve just absorbs new functions instead of rebasing /// itself inside new structs. #[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub struct MapReparamCurve where C: Curve, @@ -791,6 +809,7 @@ where /// A [`Curve`] that is the graph of another curve over its parameter space. #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub struct GraphCurve where C: Curve, @@ -817,6 +836,7 @@ where /// A [`Curve`] that combines the data from two constituent curves into a tuple output type. #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub struct ProductCurve where C: Curve, From 0e58d03b9457e20b1fcc60c98c7da60c73dc4df2 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 10 Jun 2024 17:53:05 -0400 Subject: [PATCH 27/44] Fix docs --- crates/bevy_math/src/curve/cores.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs index 829321e31a338..916c26c000e63 100644 --- a/crates/bevy_math/src/curve/cores.rs +++ b/crates/bevy_math/src/curve/cores.rs @@ -58,7 +58,7 @@ impl Betweenness { /// # Example /// ```rust /// # use bevy_math::curve::*; -/// # use bevy_math::curve::builders::*; +/// # use bevy_math::curve::cores::*; /// enum InterpolationMode { /// Linear, /// Step, @@ -222,6 +222,9 @@ pub fn even_betweenness(domain: Interval, samples: usize, t: f32) -> Betweenness /// 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`]. +/// +/// [`domain`]: UnevenCore::domain +/// [`sample_with`]: UnevenCore::sample_with #[derive(Debug, Clone)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] @@ -332,6 +335,8 @@ impl UnevenCore { /// /// 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. + /// + /// [`Curve::reparametrize`]: crate::curve::Curve::reparametrize pub fn map_sample_times(mut self, f: impl Fn(f32) -> f32) -> UnevenCore { let mut timed_samples: Vec<(f32, T)> = self.times.into_iter().map(f).zip(self.samples).collect(); From 24d6abbb0b263dd3323449cb9ebcf74cc879e00a Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 10 Jun 2024 19:55:47 -0400 Subject: [PATCH 28/44] Beginning of rewrite on top of curve cores --- crates/bevy_animation/src/curves.rs | 336 +++++++--------------------- 1 file changed, 87 insertions(+), 249 deletions(-) diff --git a/crates/bevy_animation/src/curves.rs b/crates/bevy_animation/src/curves.rs index bdb6daf87ba5d..9c549beafd675 100644 --- a/crates/bevy_animation/src/curves.rs +++ b/crates/bevy_animation/src/curves.rs @@ -1,61 +1,72 @@ -use bevy_math::{ - cubic_splines::{CubicGenerator, CubicHermite}, - curve::*, - FloatExt, Quat, Vec3, Vec4, VectorSpace, -}; - -/// A wrapper struct that gives the enclosed type the property of being [`Interpolable`] with -/// naïve step interpolation. `self.interpolate(other, t)` is such that `self` is returned when -/// `t` is less than `1.0`, while `other` is returned for values `1.0` and greater. -#[derive(Clone, Copy, Default, Debug)] -pub struct Stepped(pub T) +use crate::cubic_spline_interpolation; +use bevy_math::{curve::cores::*, curve::*, Quat, Vec3, Vec4, VectorSpace}; +use bevy_reflect::Reflect; + +/// A keyframe-defined curve that "interpolates" by stepping at `t = 1.0` to the next keyframe. +#[derive(Debug, Clone, Reflect)] +pub struct SteppedKeyframeCurve { + core: UnevenCore, +} + +impl Curve for SteppedKeyframeCurve where - T: Clone; - -impl Interpolable for Stepped { - fn interpolate(&self, other: &Self, t: f32) -> Self { - if t < 1.0 { - self.clone() - } else { - other.clone() - } + T: Clone, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample(&self, t: f32) -> T { + self.core + .sample_with(t, |x, y, t| if t >= 1.0 { y.clone() } else { x.clone() }) } } -/// A struct that wraps a vector space type together with data needed for cubic spline (Hermite) -/// interpolation. The resulting type is [`Interpolable`], with the interior position and velocity -/// between adjacent points determined by the Hermite spline connecting them. -/// -/// Note that outside of the interval `[0, 1]`, this uses global extrapolation based on the -/// out-tangent of the left-hand point and the in-tangent of the right-hand point. -#[derive(Clone, Copy, Default, Debug)] -pub struct TwoSidedHermite { - /// The position of the datum in space. - pub point: V, - - /// The incoming tangent vector used for interpolation. - pub in_tangent: V, - - /// The outgoing tangent vector used for interpolation. - pub out_tangent: V, +/// A keyframe-defined curve that uses linear interpolation for [`VectorSpace`] types. +#[derive(Debug, Clone, Reflect)] +pub struct LinearKeyframeCurve { + core: UnevenCore, +} + +impl Curve for LinearKeyframeCurve +where + V: VectorSpace, +{ + fn domain(&self) -> Interval { + self.core.domain() + } + + fn sample(&self, t: f32) -> V { + self.core.sample_with(t, |x, y, t| x.lerp(*y, t)) + } +} + +/// A keyframe-defined curve that uses cubic spline interpolation, backed by a contiguous buffer. +#[derive(Debug, Clone, Reflect)] +pub struct CubicSplineKeyframeCurve { + // Note: the sample width here should be 3. + core: ChunkedUnevenCore, } -impl Interpolable for TwoSidedHermite +impl Curve for CubicSplineKeyframeCurve where V: VectorSpace, { - fn interpolate(&self, other: &Self, t: f32) -> Self { - let control_points = [self.point, other.point]; - let tangents = [self.out_tangent, other.in_tangent]; - // We should probably have a better way of constructing these... - let curve_segment = CubicHermite::new(control_points, tangents) - .to_curve() - .segments()[0]; - // (For interior points, the in-tangents and out-tangents are just the tangent.) - Self { - point: curve_segment.position(t), - in_tangent: curve_segment.velocity(t), - out_tangent: curve_segment.velocity(t), + fn domain(&self) -> Interval { + self.core.domain() + } + + fn sample(&self, t: f32) -> V { + match self.core.sample_betweenness(t) { + // In all the cases where only one frame matters, defer to the position within it. + Betweenness::Exact(v) => v[1], + Betweenness::LeftTail(v) => v[1], + Betweenness::RightTail(v) => v[1], + Betweenness::Between(u, v, s) => { + cubic_spline_interpolation(u[1], u[2], v[0], v[1], s, todo!()) + } } } } @@ -65,25 +76,23 @@ where /// A curve specifying the translation component of a [`Transform`] in animation. The variants are /// broken down by interpolation mode (with the exception of `Constant`, which never interpolates). /// -/// This type is, itself, a `Curve`, and it internally uses the provided sampling modes; -/// however, linear interpolation is intrinsic to `Vec3` itself, so the interpolation metadata -/// itself will be lost if the curve is resampled. On the other hand, the variant curves each -/// properly know their own modes of interpolation. -//#[derive(Clone, Debug)] +/// This type is, itself, a `Curve`, and it internally uses the provided sampling modes; each +/// variant "knows" its own sampling type +#[derive(Clone, Debug, Reflect)] pub enum TranslationCurve { /// A curve which takes a constant value over its domain. Notably, this is how animations with /// only a single keyframe are interpreted. Constant(ConstantCurve), /// A curve which interpolates linearly between keyframes. - Linear(UnevenSampleCurve), + Linear(UnevenSampleAutoCurve), /// A curve which interpolates between keyframes in steps. - Step(UnevenSampleCurve>), + Step(SteppedKeyframeCurve), /// 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(UnevenSampleCurve>), + CubicSpline(CubicSplineKeyframeCurve), } impl Curve for TranslationCurve { @@ -100,8 +109,8 @@ impl Curve for TranslationCurve { match self { TranslationCurve::Constant(c) => c.sample(t), TranslationCurve::Linear(c) => c.sample(t), - TranslationCurve::Step(c) => c.sample(t).0, - TranslationCurve::CubicSpline(c) => c.map(|x| x.point).sample(t), + TranslationCurve::Step(c) => c.sample(t), + TranslationCurve::CubicSpline(c) => c.sample(t), } } } @@ -113,21 +122,21 @@ impl Curve for TranslationCurve { /// however, linear interpolation is intrinsic to `Vec3` itself, so the interpolation metadata /// itself will be lost if the curve is resampled. On the other hand, the variant curves each /// properly know their own modes of interpolation. -//#[derive(Clone, Debug)] +#[derive(Clone, Debug, Reflect)] pub enum ScaleCurve { /// A curve which takes a constant value over its domain. Notably, this is how animations with /// only a single keyframe are interpreted. Constant(ConstantCurve), /// A curve which interpolates linearly between keyframes. - Linear(UnevenSampleCurve), + Linear(LinearKeyframeCurve), /// A curve which interpolates between keyframes in steps. - Step(UnevenSampleCurve>), + Step(SteppedKeyframeCurve), /// 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(UnevenSampleCurve>), + CubicSpline(CubicSplineKeyframeCurve), } impl Curve for ScaleCurve { @@ -144,8 +153,8 @@ impl Curve for ScaleCurve { match self { ScaleCurve::Constant(c) => c.sample(t), ScaleCurve::Linear(c) => c.sample(t), - ScaleCurve::Step(c) => c.map(|x| x.0).sample(t), - ScaleCurve::CubicSpline(c) => c.map(|x| x.point).sample(t), + ScaleCurve::Step(c) => c.sample(t), + ScaleCurve::CubicSpline(c) => c.sample(t), } } } @@ -157,22 +166,22 @@ impl Curve for ScaleCurve { /// however, spherical linear interpolation is intrinsic to `Vec3` itself, so the interpolation /// metadata itself will be lost if the curve is resampled. On the other hand, the variant curves each /// properly know their own modes of interpolation. -//#[derive(Clone, Debug)] +#[derive(Clone, Debug, Reflect)] pub enum RotationCurve { /// A curve which takes a constant value over its domain. Notably, this is how animations with /// only a single keyframe are interpreted. Constant(ConstantCurve), /// A curve which uses spherical linear interpolation between keyframes. - SphericalLinear(UnevenSampleCurve), + SphericalLinear(UnevenSampleAutoCurve), /// A curve which interpolates between keyframes in steps. - Step(UnevenSampleCurve>), + Step(SteppedKeyframeCurve), /// A curve which interpolates between keyframes by using auxiliary tangent data to join /// adjacent keyframes with a cubic Hermite spline. For quaternions, this means interpolating /// the underlying 4-vectors, sampling, and normalizing the result. - CubicSpline(UnevenSampleCurve>), + CubicSpline(CubicSplineKeyframeCurve), } impl Curve for RotationCurve { @@ -189,20 +198,19 @@ impl Curve for RotationCurve { match self { RotationCurve::Constant(c) => c.sample(t), RotationCurve::SphericalLinear(c) => c.sample(t), - RotationCurve::Step(c) => c.map(|x| x.0).sample(t), - RotationCurve::CubicSpline(c) => { - c.map(|x| Quat::from_vec4(x.point).normalize()).sample(t) - } + RotationCurve::Step(c) => c.sample(t), + RotationCurve::CubicSpline(c) => c.map(|x| Quat::from_vec4(x).normalize()).sample(t), } } } + /// 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>`; however, in order to avoid allocation, it is -/// recommended to use its implementation of the [`MultiCurve`] subtrait, which allows dumping -/// cross-channel sample data into an external buffer, avoiding allocation. -//#[derive(Reflect, Clone, Debug)] +/// recommended to use its implementation of the [`IterableCurve`] trait, which allows iterating +/// directly over information derived from the curve without allocating. +#[derive(Debug, Clone, Reflect)] 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. @@ -275,179 +283,9 @@ pub enum VariableCurve { Weights(WeightsCurve), } -//-----------------// -// NEW CURVE STUFF // -//-----------------// - -/// A curve data structure which holds data for a list of keyframes in a number of distinct -/// "channels" equal to its `width`. This is sampled through `sample_into`, which places the data -/// into an external buffer. If `T: Default`, this may also be used as a `Curve` directly, but a new -/// `Vec` will be allocated for each call, which may be undesirable. -//#[derive(Clone, Debug)] -pub struct DynamicArrayCurve -where - T: Interpolable, -{ - /// The times at which the keyframes are placed. There must be at least two of these, and they - /// must always be sorted in increasing order. - times: Vec, - - /// The output values. These are stored in multiples of the `width`, with each `width` of - /// indices corresponding to the outputs for a single keyframe. For instance, the indices - /// `0..self.width` in `values` correspond to the different channels for keyframe `0`. - /// - /// The length of this vector must always be `width` times that of `times`. - values: Vec, - - /// The number of channels that this data structure maintains, and therefore the factor between - /// the length of `times` and that of `values`. Must be at least `1`. - width: usize, -} - -/// An error that indicates that a [`DynamicArrayCurve`] could not be formed. -//#[derive(Debug, Clone, Copy)] -pub struct DynamicArrayError; - -impl DynamicArrayCurve -where - T: Interpolable, -{ - /// Create a new [`DynamicArrayCurve`]. Produces an error in any of the following circumstances: - /// * `width` is zero. - /// * `times` has a length less than `2`. - /// * `values` has the incorrect length relative to `times`. - pub fn new( - times: impl Into>, - values: impl Into>, - width: usize, - ) -> Result { - let times: Vec = times.into(); - let values: Vec = values.into(); - - if width == 0 { - return Err(DynamicArrayError); - } - if times.len() < 2 { - return Err(DynamicArrayError); - } - if values.len() != times.len() * width { - return Err(DynamicArrayError); - } - - Ok(Self { - times, - values, - width, - }) - } - - fn find_keyframe(&self, t: f32) -> Option { - match self - .times - .binary_search_by(|pt| pt.partial_cmp(&t).unwrap()) - { - Ok(index) => { - if index >= self.times.len() - 1 { - // This is the index of the last keyframe - None - } else { - // Exact match that is not the last keyframe - Some(index) - } - } - Err(index) => { - if index == 0 { - // This is before the first keyframe - None - } else if index >= self.times.len() { - // This is after the last keyframe - None - } else { - // This is actually in the middle somewhere; we subtract 1 to return the index - // of the lower of the two keyframes - Some(index - 1) - } - } - } - } - - /// The width for this curve; i.e., the number of distinct channels sampled for each keyframe. - pub fn width(&self) -> usize { - self.width - } - - /// The interval which spans between the first and last sample times. - fn domain(&self) -> Interval { - let start = self.times.first().unwrap(); - let end = self.times.last().unwrap(); - interval(*start, *end).unwrap() - } -} - -// Note that the `sample` function always allocates its output, whereas `sample_into` can dump -// the sample data into an external buffer, bypassing the need to allocate. -impl Curve> for DynamicArrayCurve -where - T: Interpolable + Default, -{ - fn domain(&self) -> Interval { - self.domain() - } - - fn sample(&self, t: f32) -> Vec { - self.sample_iter(t).collect() - } -} - -impl IterableCurve for DynamicArrayCurve -where - T: Interpolable, -{ - fn domain(&self) -> Interval { - self.domain() - } - - fn sample_iter<'a>(&self, t: f32) -> impl Iterator - where - Self: 'a, - { - let t = self.domain().clamp(t); - - let Some(lower_index) = self.find_keyframe(t) else { - // After clamping, `find_keyframe` will only return None if we landed on the - // last keyframe. - let index = self.times.len() - 1; - - // Jump to where the values for the last keyframe are: - let morph_index = index * self.width; - - // Return an iterator that just clones the last keyframe. - return IteratorDisjunction::Left( - self.values[morph_index..(morph_index + self.width)] - .iter() - .cloned(), - ); - }; - - // Get the adjacent timestamps and the lerp parameter of `t` between them: - let upper_index = lower_index + 1; - let lower_timestamp = self.times[lower_index]; - let upper_timestamp = self.times[upper_index]; - let lerp_param = f32::inverse_lerp(lower_timestamp, upper_timestamp, t); - - // The indices in `self.values` where the values actually start: - let lower_morph_index = lower_index * self.width; - let upper_morph_index = upper_index * self.width; - - // Return an iterator that lerps adjacent keyframes together. - IteratorDisjunction::Right( - self.values[lower_morph_index..(lower_morph_index + self.width)] - .iter() - .zip(self.values[upper_morph_index..(upper_morph_index + self.width)].iter()) - .map(move |(x, y)| x.interpolate(y, lerp_param)), - ) - } -} +//--------------// +// HELPER STUFF // +//--------------// enum IteratorDisjunction where From 7beecd88f00c4068e7d72381a1a5b62cd63c8f2e Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Tue, 11 Jun 2024 06:54:37 -0400 Subject: [PATCH 29/44] Add timed versions of betweenness sampling --- crates/bevy_math/src/curve/cores.rs | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs index 916c26c000e63..aa9d59171d4dd 100644 --- a/crates/bevy_math/src/curve/cores.rs +++ b/crates/bevy_math/src/curve/cores.rs @@ -187,6 +187,20 @@ impl EvenCore { pub fn sample_betweenness(&self, t: f32) -> Betweenness<&T> { even_betweenness(self.domain, self.samples.len(), t).map(|idx| &self.samples[idx]) } + + /// Like [`sample_betweenness`], but the returned values include the sample times. This can be + /// useful when sampling is not scale-invariant. + /// + /// [`sample_betweenness`]: EvenCore::sample_betweenness + pub fn sample_betweenness_timed(&self, t: f32) -> Betweenness<(f32, &T)> { + let segment_len = self.domain.length() / (self.samples.len() - 1) as f32; + even_betweenness(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 a [`Betweenness`] @@ -329,6 +343,14 @@ impl UnevenCore { uneven_betweenness(&self.times, t).map(|idx| &self.samples[idx]) } + /// Like [`sample_betweenness`], but the returned values include the sample times. This can be + /// useful when sampling is not scale-invariant. + /// + /// [`sample_betweenness`]: UnevenCore::sample_betweenness + pub fn sample_betweenness_timed(&self, t: f32) -> Betweenness<(f32, &T)> { + uneven_betweenness(&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. @@ -471,6 +493,15 @@ impl ChunkedUnevenCore { uneven_betweenness(&self.times, t).map(|idx| self.time_index_to_slice(idx)) } + /// Like [`sample_betweenness`], but the returned values include the sample times. This can be + /// useful when sampling is not scale-invariant. + /// + /// [`sample_betweenness`]: ChunkedUnevenCore::sample_betweenness + pub fn sample_betweenness_timed(&self, t: f32) -> Betweenness<(f32, &[T])> { + uneven_betweenness(&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. /// From 298f56fed5f91feaf07e50a8c554136480528901 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Tue, 11 Jun 2024 09:36:19 -0400 Subject: [PATCH 30/44] Complete core refactoring to the point that it actually compiles --- crates/bevy_animation/src/curves.rs | 238 +++++++++++++++++++--------- 1 file changed, 164 insertions(+), 74 deletions(-) diff --git a/crates/bevy_animation/src/curves.rs b/crates/bevy_animation/src/curves.rs index 9c549beafd675..b78b5ab355d4d 100644 --- a/crates/bevy_animation/src/curves.rs +++ b/crates/bevy_animation/src/curves.rs @@ -24,33 +24,14 @@ where } } -/// A keyframe-defined curve that uses linear interpolation for [`VectorSpace`] types. -#[derive(Debug, Clone, Reflect)] -pub struct LinearKeyframeCurve { - core: UnevenCore, -} - -impl Curve for LinearKeyframeCurve -where - V: VectorSpace, -{ - fn domain(&self) -> Interval { - self.core.domain() - } - - fn sample(&self, t: f32) -> V { - self.core.sample_with(t, |x, y, t| x.lerp(*y, t)) - } -} - /// A keyframe-defined curve that uses cubic spline interpolation, backed by a contiguous buffer. #[derive(Debug, Clone, Reflect)] -pub struct CubicSplineKeyframeCurve { +pub struct CubicKeyframeCurve { // Note: the sample width here should be 3. core: ChunkedUnevenCore, } -impl Curve for CubicSplineKeyframeCurve +impl Curve for CubicKeyframeCurve where V: VectorSpace, { @@ -59,25 +40,28 @@ where } fn sample(&self, t: f32) -> V { - match self.core.sample_betweenness(t) { + match self.core.sample_betweenness_timed(t) { // In all the cases where only one frame matters, defer to the position within it. - Betweenness::Exact(v) => v[1], - Betweenness::LeftTail(v) => v[1], - Betweenness::RightTail(v) => v[1], - Betweenness::Between(u, v, s) => { - cubic_spline_interpolation(u[1], u[2], v[0], v[1], s, todo!()) + Betweenness::Exact((_, v)) + | Betweenness::LeftTail((_, v)) + | Betweenness::RightTail((_, v)) => v[1], + + Betweenness::Between((t0, u), (t1, v), s) => { + cubic_spline_interpolation(u[1], u[2], v[0], v[1], s, t1 - t0) } } } } // Pie in the sky: `TranslationCurve` is basically the same thing as a `Box>` etc. +// The first couple variants can be taken "off the shelf" from the Curve library, while the others +// are built on top of the core abstractions. /// A curve specifying the translation component of a [`Transform`] in animation. The variants are /// broken down by interpolation mode (with the exception of `Constant`, which never interpolates). /// /// This type is, itself, a `Curve`, and it internally uses the provided sampling modes; each -/// variant "knows" its own sampling type +/// variant "knows" its own interpolation mode. #[derive(Clone, Debug, Reflect)] pub enum TranslationCurve { /// A curve which takes a constant value over its domain. Notably, this is how animations with @@ -92,7 +76,7 @@ pub enum TranslationCurve { /// 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(CubicSplineKeyframeCurve), + CubicSpline(CubicKeyframeCurve), } impl Curve for TranslationCurve { @@ -118,10 +102,8 @@ impl Curve for TranslationCurve { /// A curve specifying the scale component of a [`Transform`] in animation. The variants are /// broken down by interpolation mode (with the exception of `Constant`, which never interpolates). /// -/// This type is, itself, a `Curve`, and it internally uses the provided sampling modes; -/// however, linear interpolation is intrinsic to `Vec3` itself, so the interpolation metadata -/// itself will be lost if the curve is resampled. On the other hand, the variant curves each -/// properly know their own modes of interpolation. +/// This type is, itself, a `Curve`, and it internally uses the provided sampling modes; each +/// variant "knows" its own interpolation mode. #[derive(Clone, Debug, Reflect)] pub enum ScaleCurve { /// A curve which takes a constant value over its domain. Notably, this is how animations with @@ -129,14 +111,14 @@ pub enum ScaleCurve { Constant(ConstantCurve), /// A curve which interpolates linearly between keyframes. - Linear(LinearKeyframeCurve), + Linear(UnevenSampleAutoCurve), /// A curve which interpolates between keyframes in steps. Step(SteppedKeyframeCurve), /// 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(CubicSplineKeyframeCurve), + CubicSpline(CubicKeyframeCurve), } impl Curve for ScaleCurve { @@ -162,10 +144,8 @@ impl Curve for ScaleCurve { /// A curve specifying the scale component of a [`Transform`] in animation. The variants are /// broken down by interpolation mode (with the exception of `Constant`, which never interpolates). /// -/// This type is, itself, a `Curve`, and it internally uses the provided sampling modes; -/// however, spherical linear interpolation is intrinsic to `Vec3` itself, so the interpolation -/// metadata itself will be lost if the curve is resampled. On the other hand, the variant curves each -/// properly know their own modes of interpolation. +/// This type is, itself, a `Curve`, and it internally uses the provided sampling modes; each +/// variant "knows" its own interpolation mode. #[derive(Clone, Debug, Reflect)] pub enum RotationCurve { /// A curve which takes a constant value over its domain. Notably, this is how animations with @@ -181,7 +161,7 @@ pub enum RotationCurve { /// A curve which interpolates between keyframes by using auxiliary tangent data to join /// adjacent keyframes with a cubic Hermite spline. For quaternions, this means interpolating /// the underlying 4-vectors, sampling, and normalizing the result. - CubicSpline(CubicSplineKeyframeCurve), + CubicSpline(CubicKeyframeCurve), } impl Curve for RotationCurve { @@ -204,6 +184,130 @@ impl Curve for RotationCurve { } } +/// 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 { + // Here the sample width is the number of things to simultaneously interpolate. + core: ChunkedUnevenCore, +} + +impl IterableCurve for WideLinearKeyframeCurve +where + T: VectorSpace, +{ + fn domain(&self) -> Interval { + self.core.domain() + } + + fn sample_iter<'a>(&self, t: f32) -> impl Iterator + where + Self: 'a, + { + match self.core.sample_betweenness(t) { + Betweenness::Exact(v) | Betweenness::LeftTail(v) | Betweenness::RightTail(v) => { + TwoIterators::Left(v.iter().copied()) + } + + Betweenness::Between(u, v, s) => { + let interpolated = u.iter().zip(v.iter()).map(move |(x, y)| x.lerp(*y, s)); + TwoIterators::Right(interpolated) + } + } + } +} + +/// 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 { + // Here the sample width is the number of things to simultaneously interpolate. + core: ChunkedUnevenCore, +} + +impl IterableCurve for WideSteppedKeyframeCurve +where + T: Clone, +{ + fn domain(&self) -> Interval { + self.core.domain() + } + + fn sample_iter<'a>(&self, t: f32) -> impl Iterator + where + Self: 'a, + { + match self.core.sample_betweenness(t) { + Betweenness::Exact(v) | Betweenness::LeftTail(v) | Betweenness::RightTail(v) => { + TwoIterators::Left(v.iter().cloned()) + } + + Betweenness::Between(u, v, s) => { + let interpolated = + u.iter() + .zip(v.iter()) + .map(move |(x, y)| if s >= 1.0 { y.clone() } else { x.clone() }); + TwoIterators::Right(interpolated) + } + } + } +} + +/// 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 { + core: ChunkedUnevenCore, +} + +impl IterableCurve for WideCubicKeyframeCurve +where + T: VectorSpace, +{ + fn domain(&self) -> Interval { + self.core.domain() + } + + fn sample_iter<'a>(&self, t: f32) -> impl Iterator + where + Self: 'a, + { + match self.core.sample_betweenness_timed(t) { + Betweenness::Exact((_, v)) + | Betweenness::LeftTail((_, v)) + | Betweenness::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; + TwoIterators::Left(v[width..(width * 2)].iter().copied()) + } + + Betweenness::Between((t0, u), (t1, v), s) => TwoIterators::Right( + cubic_spline_interpolate_slices(self.core.width / 3, u, v, s, t1 - t0), + ), + } + } +} + +fn cubic_spline_interpolate_slices<'a, T: VectorSpace>( + width: usize, + first: &'a [T], + second: &'a [T], + s: f32, + step_between: f32, +) -> impl Iterator + 'a { + (0..width).map(move |idx| { + cubic_spline_interpolation( + first[idx + width], + first[idx + (width * 2)], + second[idx + width], + second[idx], + s, + step_between, + ) + }) +} + /// 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). /// @@ -216,15 +320,15 @@ pub enum WeightsCurve { /// only a single keyframe are interpreted. Constant(ConstantCurve>), - /// A curve which interpolates linearly between keyframes. - Linear(DynamicArrayCurve), + /// A curve which interpolates weights linearly between keyframes. + Linear(WideLinearKeyframeCurve), - /// A curve which interpolates between keyframes in steps. - Step(DynamicArrayCurve>), + /// A curve which interpolates weights between keyframes in steps. + Step(WideSteppedKeyframeCurve), /// 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(DynamicArrayCurve>), + CubicSpline(WideCubicKeyframeCurve), } impl IterableCurve for WeightsCurve { @@ -242,14 +346,10 @@ impl IterableCurve for WeightsCurve { Self: 'a, { match self { - WeightsCurve::Constant(c) => QuaternaryIteratorDisjunction::First(c.sample_iter(t)), - WeightsCurve::Linear(c) => QuaternaryIteratorDisjunction::Second(c.sample_iter(t)), - WeightsCurve::Step(c) => { - QuaternaryIteratorDisjunction::Third(c.sample_iter(t).map(|v| v.0)) - } - WeightsCurve::CubicSpline(c) => { - QuaternaryIteratorDisjunction::Fourth(c.sample_iter(t).map(|v| v.point)) - } + WeightsCurve::Constant(c) => FourIterators::First(c.sample_iter(t)), + WeightsCurve::Linear(c) => FourIterators::Second(c.sample_iter(t)), + WeightsCurve::Step(c) => FourIterators::Third(c.sample_iter(t)), + WeightsCurve::CubicSpline(c) => FourIterators::Fourth(c.sample_iter(t)), } } } @@ -268,7 +368,7 @@ impl Curve> for WeightsCurve { /// or the [`MorphWeights`] of morph targets for a mesh. /// /// Each variant yields a [`Curve`] over the data that it parametrizes. -//#[derive(Reflect, Clone, Debug)] +#[derive(Debug, Clone, Reflect)] pub enum VariableCurve { /// A [`TranslationCurve`] for animating the `translation` component of a [`Transform`]. Translation(TranslationCurve), @@ -287,16 +387,12 @@ pub enum VariableCurve { // HELPER STUFF // //--------------// -enum IteratorDisjunction -where - A: Iterator, - B: Iterator, -{ +enum TwoIterators { Left(A), Right(B), } -impl Iterator for IteratorDisjunction +impl Iterator for TwoIterators where A: Iterator, B: Iterator, @@ -305,26 +401,20 @@ where fn next(&mut self) -> Option { match self { - IteratorDisjunction::Left(a) => a.next(), - IteratorDisjunction::Right(b) => b.next(), + TwoIterators::Left(a) => a.next(), + TwoIterators::Right(b) => b.next(), } } } -enum QuaternaryIteratorDisjunction -where - A: Iterator, - B: Iterator, - C: Iterator, - D: Iterator, -{ +enum FourIterators { First(A), Second(B), Third(C), Fourth(D), } -impl Iterator for QuaternaryIteratorDisjunction +impl Iterator for FourIterators where A: Iterator, B: Iterator, @@ -335,10 +425,10 @@ where fn next(&mut self) -> Option { match self { - QuaternaryIteratorDisjunction::First(a) => a.next(), - QuaternaryIteratorDisjunction::Second(b) => b.next(), - QuaternaryIteratorDisjunction::Third(c) => c.next(), - QuaternaryIteratorDisjunction::Fourth(d) => d.next(), + FourIterators::First(a) => a.next(), + FourIterators::Second(b) => b.next(), + FourIterators::Third(c) => c.next(), + FourIterators::Fourth(d) => d.next(), } } } From eb3c124f7e2f09ba4499b9f928cf279906595077 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Tue, 11 Jun 2024 12:31:34 -0400 Subject: [PATCH 31/44] Finish refactoring animation lib --- crates/bevy_animation/src/curves.rs | 193 ++++++-- crates/bevy_animation/src/lib.rs | 718 +++++++--------------------- 2 files changed, 346 insertions(+), 565 deletions(-) diff --git a/crates/bevy_animation/src/curves.rs b/crates/bevy_animation/src/curves.rs index b78b5ab355d4d..91c9df1216b61 100644 --- a/crates/bevy_animation/src/curves.rs +++ b/crates/bevy_animation/src/curves.rs @@ -1,4 +1,5 @@ -use crate::cubic_spline_interpolation; +//! Curve structures used by the animation system. + use bevy_math::{curve::cores::*, curve::*, Quat, Vec3, Vec4, VectorSpace}; use bevy_reflect::Reflect; @@ -35,10 +36,12 @@ impl Curve for CubicKeyframeCurve where V: VectorSpace, { + #[inline] fn domain(&self) -> Interval { self.core.domain() } + #[inline] fn sample(&self, t: f32) -> V { match self.core.sample_betweenness_timed(t) { // In all the cases where only one frame matters, defer to the position within it. @@ -80,6 +83,7 @@ pub enum TranslationCurve { } impl Curve for TranslationCurve { + #[inline] fn domain(&self) -> Interval { match self { TranslationCurve::Constant(c) => c.domain(), @@ -89,6 +93,7 @@ impl Curve for TranslationCurve { } } + #[inline] fn sample(&self, t: f32) -> Vec3 { match self { TranslationCurve::Constant(c) => c.sample(t), @@ -99,6 +104,20 @@ impl Curve for TranslationCurve { } } +impl TranslationCurve { + /// The time of the last keyframe for this animation curve. If the curve is constant, None + /// is returned instead. + #[inline] + pub fn duration(&self) -> Option { + match self { + TranslationCurve::Constant(_) => None, + TranslationCurve::Linear(c) => Some(c.domain().end()), + TranslationCurve::Step(c) => Some(c.domain().end()), + TranslationCurve::CubicSpline(c) => Some(c.domain().end()), + } + } +} + /// A curve specifying the scale component of a [`Transform`] in animation. The variants are /// broken down by interpolation mode (with the exception of `Constant`, which never interpolates). /// @@ -122,6 +141,7 @@ pub enum ScaleCurve { } impl Curve for ScaleCurve { + #[inline] fn domain(&self) -> Interval { match self { ScaleCurve::Constant(c) => c.domain(), @@ -131,6 +151,7 @@ impl Curve for ScaleCurve { } } + #[inline] fn sample(&self, t: f32) -> Vec3 { match self { ScaleCurve::Constant(c) => c.sample(t), @@ -141,6 +162,20 @@ impl Curve for ScaleCurve { } } +impl ScaleCurve { + /// The time of the last keyframe for this animation curve. If the curve is constant, None + /// is returned instead. + #[inline] + pub fn duration(&self) -> Option { + match self { + ScaleCurve::Constant(_) => None, + ScaleCurve::Linear(c) => Some(c.domain().end()), + ScaleCurve::Step(c) => Some(c.domain().end()), + ScaleCurve::CubicSpline(c) => Some(c.domain().end()), + } + } +} + /// A curve specifying the scale component of a [`Transform`] in animation. The variants are /// broken down by interpolation mode (with the exception of `Constant`, which never interpolates). /// @@ -165,6 +200,7 @@ pub enum RotationCurve { } impl Curve for RotationCurve { + #[inline] fn domain(&self) -> Interval { match self { RotationCurve::Constant(c) => c.domain(), @@ -174,6 +210,7 @@ impl Curve for RotationCurve { } } + #[inline] fn sample(&self, t: f32) -> Quat { match self { RotationCurve::Constant(c) => c.sample(t), @@ -184,6 +221,20 @@ impl Curve for RotationCurve { } } +impl RotationCurve { + /// The time of the last keyframe for this animation curve. If the curve is constant, None + /// is returned instead. + #[inline] + pub fn duration(&self) -> Option { + match self { + RotationCurve::Constant(_) => None, + RotationCurve::SphericalLinear(c) => Some(c.domain().end()), + RotationCurve::Step(c) => Some(c.domain().end()), + RotationCurve::CubicSpline(c) => Some(c.domain().end()), + } + } +} + /// A keyframe-defined curve that uses linear interpolation over many samples at once, backed /// by a contiguous buffer. #[derive(Debug, Clone, Reflect)] @@ -196,10 +247,12 @@ impl IterableCurve for WideLinearKeyframeCurve where T: VectorSpace, { + #[inline] fn domain(&self) -> Interval { self.core.domain() } + #[inline] fn sample_iter<'a>(&self, t: f32) -> impl Iterator where Self: 'a, @@ -229,10 +282,12 @@ impl IterableCurve for WideSteppedKeyframeCurve where T: Clone, { + #[inline] fn domain(&self) -> Interval { self.core.domain() } + #[inline] fn sample_iter<'a>(&self, t: f32) -> impl Iterator where Self: 'a, @@ -264,10 +319,12 @@ impl IterableCurve for WideCubicKeyframeCurve where T: VectorSpace, { + #[inline] fn domain(&self) -> Interval { self.core.domain() } + #[inline] fn sample_iter<'a>(&self, t: f32) -> impl Iterator where Self: 'a, @@ -289,25 +346,6 @@ where } } -fn cubic_spline_interpolate_slices<'a, T: VectorSpace>( - width: usize, - first: &'a [T], - second: &'a [T], - s: f32, - step_between: f32, -) -> impl Iterator + 'a { - (0..width).map(move |idx| { - cubic_spline_interpolation( - first[idx + width], - first[idx + (width * 2)], - second[idx + width], - second[idx], - s, - step_between, - ) - }) -} - /// 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). /// @@ -332,6 +370,7 @@ pub enum WeightsCurve { } impl IterableCurve for WeightsCurve { + #[inline] fn domain(&self) -> Interval { match self { WeightsCurve::Constant(c) => IterableCurve::domain(c), @@ -341,6 +380,7 @@ impl IterableCurve for WeightsCurve { } } + #[inline] fn sample_iter<'a>(&self, t: f32) -> impl Iterator where Self: 'a, @@ -354,13 +394,17 @@ impl IterableCurve for WeightsCurve { } } -impl Curve> for WeightsCurve { - fn domain(&self) -> Interval { - IterableCurve::domain(self) - } - - fn sample(&self, t: f32) -> Vec { - self.sample_iter(t).collect() +impl WeightsCurve { + /// The time of the last keyframe for this animation curve. If the curve is constant, None + /// is returned instead. + #[inline] + pub fn duration(&self) -> Option { + match self { + WeightsCurve::Constant(_) => None, + WeightsCurve::Linear(c) => Some(c.domain().end()), + WeightsCurve::Step(c) => Some(c.domain().end()), + WeightsCurve::CubicSpline(c) => Some(c.domain().end()), + } } } @@ -368,6 +412,9 @@ impl Curve> for WeightsCurve { /// or the [`MorphWeights`] of morph targets for a mesh. /// /// Each variant yields a [`Curve`] over the data that it parametrizes. +/// +/// This follows the [glTF design]. +/// [glTF design]: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#animations #[derive(Debug, Clone, Reflect)] pub enum VariableCurve { /// A [`TranslationCurve`] for animating the `translation` component of a [`Transform`]. @@ -383,9 +430,58 @@ pub enum VariableCurve { Weights(WeightsCurve), } -//--------------// -// HELPER STUFF // -//--------------// +impl VariableCurve { + /// The domain of this curve as an interval. + #[inline] + pub fn domain(&self) -> Interval { + match self { + VariableCurve::Translation(c) => c.domain(), + VariableCurve::Rotation(c) => c.domain(), + VariableCurve::Scale(c) => c.domain(), + VariableCurve::Weights(c) => c.domain(), + } + } + + /// The time of the last keyframe for this animation curve. If the curve is constant, None + /// is returned instead. + #[inline] + pub fn duration(&self) -> Option { + match self { + VariableCurve::Translation(c) => c.duration(), + VariableCurve::Rotation(c) => c.duration(), + VariableCurve::Scale(c) => c.duration(), + VariableCurve::Weights(c) => c.duration(), + } + } +} + +impl From for VariableCurve { + fn from(curve: TranslationCurve) -> Self { + Self::Translation(curve) + } +} + +impl From for VariableCurve { + fn from(curve: RotationCurve) -> Self { + Self::Rotation(curve) + } +} + +impl From for VariableCurve { + fn from(curve: ScaleCurve) -> Self { + Self::Scale(curve) + } +} + +impl From for VariableCurve { + fn from(curve: WeightsCurve) -> Self { + Self::Weights(curve) + } +} + +//---------// +// HELPERS // +//---------// enum TwoIterators { Left(A), @@ -432,3 +528,40 @@ where } } } + +/// Helper function for cubic spline interpolation. +fn cubic_spline_interpolation( + value_start: T, + tangent_out_start: T, + tangent_in_end: T, + value_end: T, + lerp: f32, + step_duration: f32, +) -> T +where + T: VectorSpace, +{ + value_start * (2.0 * lerp.powi(3) - 3.0 * lerp.powi(2) + 1.0) + + tangent_out_start * (step_duration) * (lerp.powi(3) - 2.0 * lerp.powi(2) + lerp) + + value_end * (-2.0 * lerp.powi(3) + 3.0 * lerp.powi(2)) + + tangent_in_end * step_duration * (lerp.powi(3) - lerp.powi(2)) +} + +fn cubic_spline_interpolate_slices<'a, T: VectorSpace>( + width: usize, + first: &'a [T], + second: &'a [T], + s: f32, + step_between: f32, +) -> impl Iterator + 'a { + (0..width).map(move |idx| { + cubic_spline_interpolation( + first[idx + width], + first[idx + (width * 2)], + second[idx + width], + second[idx], + s, + step_between, + ) + }) +} diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 7cc7140270945..19a0952513ee0 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -13,11 +13,13 @@ mod graph; mod transition; mod util; +use bevy_math::curve::{Curve, IterableCurve}; +use curves::VariableCurve; + use std::cell::RefCell; use std::collections::BTreeMap; use std::hash::{Hash, Hasher}; use std::iter; -use std::ops::{Add, Mul}; use bevy_app::{App, Plugin, PostUpdate}; use bevy_asset::{Asset, AssetApp, Assets, Handle}; @@ -25,7 +27,7 @@ use bevy_core::Name; use bevy_ecs::entity::MapEntities; use bevy_ecs::prelude::*; use bevy_ecs::reflect::ReflectMapEntities; -use bevy_math::{FloatExt, Quat, Vec3}; +use bevy_math::FloatExt; use bevy_reflect::Reflect; use bevy_render::mesh::morph::MorphWeights; use bevy_time::Time; @@ -47,8 +49,8 @@ use uuid::Uuid; pub mod prelude { #[doc(hidden)] pub use crate::{ - animatable::*, graph::*, transition::*, AnimationClip, AnimationPlayer, AnimationPlugin, - Interpolation, Keyframes, VariableCurve, + animatable::*, curves::VariableCurve, graph::*, transition::*, AnimationClip, + AnimationPlayer, AnimationPlugin, }; } @@ -59,117 +61,6 @@ use crate::transition::{advance_transitions, expire_completed_transitions}; /// [UUID namespace]: https://en.wikipedia.org/wiki/Universally_unique_identifier#Versions_3_and_5_(namespace_name-based) pub static ANIMATION_TARGET_NAMESPACE: Uuid = Uuid::from_u128(0x3179f519d9274ff2b5966fd077023911); -/// List of keyframes for one of the attribute of a [`Transform`]. -#[derive(Reflect, Clone, Debug)] -pub enum Keyframes { - /// Keyframes for rotation. - Rotation(Vec), - /// Keyframes for translation. - Translation(Vec), - /// Keyframes for scale. - Scale(Vec), - /// Keyframes for morph target weights. - /// - /// Note that in `.0`, each contiguous `target_count` values is a single - /// keyframe representing the weight values at given keyframe. - /// - /// This follows the [glTF design]. - /// - /// [glTF design]: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#animations - Weights(Vec), -} - -impl Keyframes { - /// Returns the number of keyframes. - pub fn len(&self) -> usize { - match self { - Keyframes::Weights(vec) => vec.len(), - Keyframes::Translation(vec) | Keyframes::Scale(vec) => vec.len(), - Keyframes::Rotation(vec) => vec.len(), - } - } - - /// Returns true if the number of keyframes is zero. - pub fn is_empty(&self) -> bool { - self.len() == 0 - } -} - -/// Describes how an attribute of a [`Transform`] or [`MorphWeights`] should be animated. -/// -/// `keyframe_timestamps` and `keyframes` should have the same length. -#[derive(Reflect, Clone, Debug)] -pub struct VariableCurve { - /// Timestamp for each of the keyframes. - pub keyframe_timestamps: Vec, - /// List of the keyframes. - /// - /// The representation will depend on the interpolation type of this curve: - /// - /// - for `Interpolation::Step` and `Interpolation::Linear`, each keyframe is a single value - /// - for `Interpolation::CubicSpline`, each keyframe is made of three values for `tangent_in`, - /// `keyframe_value` and `tangent_out` - pub keyframes: Keyframes, - /// Interpolation method to use between keyframes. - pub interpolation: Interpolation, -} - -impl VariableCurve { - /// Find the index of the keyframe at or before the current time. - /// - /// Returns [`None`] if the curve is finished or not yet started. - /// To be more precise, this returns [`None`] if the frame is at or past the last keyframe: - /// we cannot get the *next* keyframe to interpolate to in that case. - pub fn find_current_keyframe(&self, seek_time: f32) -> Option { - // An Ok(keyframe_index) result means an exact result was found by binary search - // An Err result means the keyframe was not found, and the index is the keyframe - // PERF: finding the current keyframe can be optimised - let search_result = self - .keyframe_timestamps - .binary_search_by(|probe| probe.partial_cmp(&seek_time).unwrap()); - - // Subtract one for zero indexing! - let last_keyframe = self.keyframe_timestamps.len() - 1; - - // We want to find the index of the keyframe before the current time - // If the keyframe is past the second-to-last keyframe, the animation cannot be interpolated. - let step_start = match search_result { - // An exact match was found, and it is the last keyframe (or something has gone terribly wrong). - // This means that the curve is finished. - Ok(n) if n >= last_keyframe => return None, - // An exact match was found, and it is not the last keyframe. - Ok(i) => i, - // No exact match was found, and the seek_time is before the start of the animation. - // This occurs because the binary search returns the index of where we could insert a value - // without disrupting the order of the vector. - // If the value is less than the first element, the index will be 0. - Err(0) => return None, - // No exact match was found, and it was after the last keyframe. - // The curve is finished. - Err(n) if n > last_keyframe => return None, - // No exact match was found, so return the previous keyframe to interpolate from. - Err(i) => i - 1, - }; - - // Consumers need to be able to interpolate between the return keyframe and the next - assert!(step_start < self.keyframe_timestamps.len()); - - Some(step_start) - } -} - -/// Interpolation method to use between keyframes. -#[derive(Reflect, Clone, Debug)] -pub enum Interpolation { - /// Linear interpolation between the two closest keyframes. - Linear, - /// Step interpolation, the value of the start keyframe is used. - Step, - /// Cubic spline interpolation. The value of the two closest keyframes is used, with the out - /// tangent of the start keyframe and the in tangent of the end keyframe. - CubicSpline, -} - /// A list of [`VariableCurve`]s and the [`AnimationTargetId`]s to which they /// apply. /// @@ -304,9 +195,7 @@ impl AnimationClip { /// curve covers. pub fn add_curve_to_target(&mut self, target_id: AnimationTargetId, curve: VariableCurve) { // Update the duration of the animation by this curve duration if it's longer - self.duration = self - .duration - .max(*curve.keyframe_timestamps.last().unwrap_or(&0.0)); + self.duration = self.duration.max(curve.duration().unwrap_or(0.0)); self.curves.entry(target_id).or_default().push(curve); } } @@ -872,251 +761,43 @@ impl AnimationTargetContext<'_> { /// [`AnimationTargetContext`]. fn apply(&mut self, curves: &[VariableCurve], weight: f32, seek_time: f32) { for curve in curves { - // Some curves have only one keyframe used to set a transform - if curve.keyframe_timestamps.len() == 1 { - self.apply_single_keyframe(curve, weight); - return; - } - - // Find the current keyframe - let Some(step_start) = curve.find_current_keyframe(seek_time) else { - return; - }; - - let timestamp_start = curve.keyframe_timestamps[step_start]; - let timestamp_end = curve.keyframe_timestamps[step_start + 1]; - // Compute how far we are through the keyframe, normalized to [0, 1] - let lerp = f32::inverse_lerp(timestamp_start, timestamp_end, seek_time); - - self.apply_tweened_keyframe( - curve, - step_start, - lerp, - weight, - timestamp_end - timestamp_start, - ); - } - } - - fn apply_single_keyframe(&mut self, curve: &VariableCurve, weight: f32) { - match &curve.keyframes { - Keyframes::Rotation(keyframes) => { - if let Some(ref mut transform) = self.transform { - transform.rotation = transform.rotation.slerp(keyframes[0], weight); + match curve { + VariableCurve::Translation(translation_curve) => { + if let Some(ref mut transform) = self.transform { + transform.translation = transform + .translation + .lerp(translation_curve.sample(seek_time), weight); + } } - } - - Keyframes::Translation(keyframes) => { - if let Some(ref mut transform) = self.transform { - transform.translation = transform.translation.lerp(keyframes[0], weight); + VariableCurve::Rotation(rotation_curve) => { + if let Some(ref mut transform) = self.transform { + transform.rotation = transform + .rotation + .slerp(rotation_curve.sample(seek_time), weight); + } } - } - - Keyframes::Scale(keyframes) => { - if let Some(ref mut transform) = self.transform { - transform.scale = transform.scale.lerp(keyframes[0], weight); + VariableCurve::Scale(scale_curve) => { + if let Some(ref mut transform) = self.transform { + transform.scale = + transform.scale.lerp(scale_curve.sample(seek_time), weight); + } } - } - - Keyframes::Weights(keyframes) => { - let Some(ref mut morphs) = self.morph_weights else { - error!( - "Tried to animate morphs on {:?} ({:?}), but no `MorphWeights` was found", - self.entity, self.name, + VariableCurve::Weights(weights_curve) => { + let Some(ref mut morphs) = self.morph_weights else { + error!( + "Tried to animate morphs on {:?} ({:?}), but no `MorphWeights` was found", + self.entity, self.name, + ); + return; + }; + + lerp_morph_weights( + morphs.weights_mut(), + weights_curve.sample_iter(seek_time), + weight, ); - return; - }; - - let target_count = morphs.weights().len(); - lerp_morph_weights( - morphs.weights_mut(), - get_keyframe(target_count, keyframes, 0).iter().copied(), - weight, - ); - } - } - } - - fn apply_tweened_keyframe( - &mut self, - curve: &VariableCurve, - step_start: usize, - lerp: f32, - weight: f32, - duration: f32, - ) { - match (&curve.interpolation, &curve.keyframes) { - (Interpolation::Step, Keyframes::Rotation(keyframes)) => { - if let Some(ref mut transform) = self.transform { - transform.rotation = transform.rotation.slerp(keyframes[step_start], weight); - } - } - - (Interpolation::Linear, Keyframes::Rotation(keyframes)) => { - let Some(ref mut transform) = self.transform else { - return; - }; - - let rot_start = keyframes[step_start]; - let mut rot_end = keyframes[step_start + 1]; - // Choose the smallest angle for the rotation - if rot_end.dot(rot_start) < 0.0 { - rot_end = -rot_end; - } - // Rotations are using a spherical linear interpolation - let rot = rot_start.normalize().slerp(rot_end.normalize(), lerp); - transform.rotation = transform.rotation.slerp(rot, weight); - } - - (Interpolation::CubicSpline, Keyframes::Rotation(keyframes)) => { - let Some(ref mut transform) = self.transform else { - return; - }; - - let value_start = keyframes[step_start * 3 + 1]; - let tangent_out_start = keyframes[step_start * 3 + 2]; - let tangent_in_end = keyframes[(step_start + 1) * 3]; - let value_end = keyframes[(step_start + 1) * 3 + 1]; - let result = cubic_spline_interpolation( - value_start, - tangent_out_start, - tangent_in_end, - value_end, - lerp, - duration, - ); - transform.rotation = transform.rotation.slerp(result.normalize(), weight); - } - - (Interpolation::Step, Keyframes::Translation(keyframes)) => { - if let Some(ref mut transform) = self.transform { - transform.translation = - transform.translation.lerp(keyframes[step_start], weight); - } - } - - (Interpolation::Linear, Keyframes::Translation(keyframes)) => { - let Some(ref mut transform) = self.transform else { - return; - }; - - let translation_start = keyframes[step_start]; - let translation_end = keyframes[step_start + 1]; - let result = translation_start.lerp(translation_end, lerp); - transform.translation = transform.translation.lerp(result, weight); - } - - (Interpolation::CubicSpline, Keyframes::Translation(keyframes)) => { - let Some(ref mut transform) = self.transform else { - return; - }; - - let value_start = keyframes[step_start * 3 + 1]; - let tangent_out_start = keyframes[step_start * 3 + 2]; - let tangent_in_end = keyframes[(step_start + 1) * 3]; - let value_end = keyframes[(step_start + 1) * 3 + 1]; - let result = cubic_spline_interpolation( - value_start, - tangent_out_start, - tangent_in_end, - value_end, - lerp, - duration, - ); - transform.translation = transform.translation.lerp(result, weight); - } - - (Interpolation::Step, Keyframes::Scale(keyframes)) => { - if let Some(ref mut transform) = self.transform { - transform.scale = transform.scale.lerp(keyframes[step_start], weight); } } - - (Interpolation::Linear, Keyframes::Scale(keyframes)) => { - let Some(ref mut transform) = self.transform else { - return; - }; - - let scale_start = keyframes[step_start]; - let scale_end = keyframes[step_start + 1]; - let result = scale_start.lerp(scale_end, lerp); - transform.scale = transform.scale.lerp(result, weight); - } - - (Interpolation::CubicSpline, Keyframes::Scale(keyframes)) => { - let Some(ref mut transform) = self.transform else { - return; - }; - - let value_start = keyframes[step_start * 3 + 1]; - let tangent_out_start = keyframes[step_start * 3 + 2]; - let tangent_in_end = keyframes[(step_start + 1) * 3]; - let value_end = keyframes[(step_start + 1) * 3 + 1]; - let result = cubic_spline_interpolation( - value_start, - tangent_out_start, - tangent_in_end, - value_end, - lerp, - duration, - ); - transform.scale = transform.scale.lerp(result, weight); - } - - (Interpolation::Step, Keyframes::Weights(keyframes)) => { - let Some(ref mut morphs) = self.morph_weights else { - return; - }; - - let target_count = morphs.weights().len(); - let morph_start = get_keyframe(target_count, keyframes, step_start); - lerp_morph_weights(morphs.weights_mut(), morph_start.iter().copied(), weight); - } - - (Interpolation::Linear, Keyframes::Weights(keyframes)) => { - let Some(ref mut morphs) = self.morph_weights else { - return; - }; - - let target_count = morphs.weights().len(); - let morph_start = get_keyframe(target_count, keyframes, step_start); - let morph_end = get_keyframe(target_count, keyframes, step_start + 1); - let result = morph_start - .iter() - .zip(morph_end) - .map(|(a, b)| a.lerp(*b, lerp)); - lerp_morph_weights(morphs.weights_mut(), result, weight); - } - - (Interpolation::CubicSpline, Keyframes::Weights(keyframes)) => { - let Some(ref mut morphs) = self.morph_weights else { - return; - }; - - let target_count = morphs.weights().len(); - let morph_start = get_keyframe(target_count, keyframes, step_start * 3 + 1); - let tangents_out_start = get_keyframe(target_count, keyframes, step_start * 3 + 2); - let tangents_in_end = get_keyframe(target_count, keyframes, (step_start + 1) * 3); - let morph_end = get_keyframe(target_count, keyframes, (step_start + 1) * 3 + 1); - let result = morph_start - .iter() - .zip(tangents_out_start) - .zip(tangents_in_end) - .zip(morph_end) - .map( - |(((&value_start, &tangent_out_start), &tangent_in_end), &value_end)| { - cubic_spline_interpolation( - value_start, - tangent_out_start, - tangent_in_end, - value_end, - lerp, - duration, - ) - }, - ); - lerp_morph_weights(morphs.weights_mut(), result, weight); - } } } } @@ -1130,39 +811,6 @@ fn lerp_morph_weights(weights: &mut [f32], keyframe: impl Iterator, } } -/// Extract a keyframe from a list of keyframes by index. -/// -/// # Panics -/// -/// When `key_index * target_count` is larger than `keyframes` -/// -/// This happens when `keyframes` is not formatted as described in -/// [`Keyframes::Weights`]. A possible cause is [`AnimationClip`] not being -/// meant to be used for the [`MorphWeights`] of the entity it's being applied to. -fn get_keyframe(target_count: usize, keyframes: &[f32], key_index: usize) -> &[f32] { - let start = target_count * key_index; - let end = target_count * (key_index + 1); - &keyframes[start..end] -} - -/// Helper function for cubic spline interpolation. -fn cubic_spline_interpolation( - value_start: T, - tangent_out_start: T, - tangent_in_end: T, - value_end: T, - lerp: f32, - step_duration: f32, -) -> T -where - T: Mul + Add, -{ - value_start * (2.0 * lerp.powi(3) - 3.0 * lerp.powi(2) + 1.0) - + tangent_out_start * (step_duration) * (lerp.powi(3) - 2.0 * lerp.powi(2) + lerp) - + value_end * (-2.0 * lerp.powi(3) + 3.0 * lerp.powi(2)) - + tangent_in_end * step_duration * (lerp.powi(3) - lerp.powi(2)) -} - /// Adds animation support to an app #[derive(Default)] pub struct AnimationPlugin; @@ -1241,149 +889,149 @@ impl AnimationGraphEvaluator { #[cfg(test)] mod tests { - use crate::VariableCurve; - use bevy_math::Vec3; - - fn test_variable_curve() -> VariableCurve { - let keyframe_timestamps = vec![1.0, 2.0, 3.0, 4.0]; - let keyframes = vec![ - Vec3::ONE * 0.0, - Vec3::ONE * 3.0, - Vec3::ONE * 6.0, - Vec3::ONE * 9.0, - ]; - let interpolation = crate::Interpolation::Linear; - - let variable_curve = VariableCurve { - keyframe_timestamps, - keyframes: crate::Keyframes::Translation(keyframes), - interpolation, - }; - - assert!(variable_curve.keyframe_timestamps.len() == variable_curve.keyframes.len()); - - // f32 doesn't impl Ord so we can't easily sort it - let mut maybe_last_timestamp = None; - for current_timestamp in &variable_curve.keyframe_timestamps { - assert!(current_timestamp.is_finite()); - - if let Some(last_timestamp) = maybe_last_timestamp { - assert!(current_timestamp > last_timestamp); - } - maybe_last_timestamp = Some(current_timestamp); - } - - variable_curve - } - - #[test] - fn find_current_keyframe_is_in_bounds() { - let curve = test_variable_curve(); - let min_time = *curve.keyframe_timestamps.first().unwrap(); - // We will always get none at times at or past the second last keyframe - let second_last_keyframe = curve.keyframe_timestamps.len() - 2; - let max_time = curve.keyframe_timestamps[second_last_keyframe]; - let elapsed_time = max_time - min_time; - - let n_keyframes = curve.keyframe_timestamps.len(); - let n_test_points = 5; - - for i in 0..=n_test_points { - // Get a value between 0 and 1 - let normalized_time = i as f32 / n_test_points as f32; - let seek_time = min_time + normalized_time * elapsed_time; - assert!(seek_time >= min_time); - assert!(seek_time <= max_time); - - let maybe_current_keyframe = curve.find_current_keyframe(seek_time); - assert!( - maybe_current_keyframe.is_some(), - "Seek time: {seek_time}, Min time: {min_time}, Max time: {max_time}" - ); - - // We cannot return the last keyframe, - // because we want to interpolate between the current and next keyframe - assert!(maybe_current_keyframe.unwrap() < n_keyframes); - } - } - - #[test] - fn find_current_keyframe_returns_none_on_unstarted_animations() { - let curve = test_variable_curve(); - let min_time = *curve.keyframe_timestamps.first().unwrap(); - let seek_time = 0.0; - assert!(seek_time < min_time); - - let maybe_keyframe = curve.find_current_keyframe(seek_time); - assert!( - maybe_keyframe.is_none(), - "Seek time: {seek_time}, Minimum time: {min_time}" - ); - } - - #[test] - fn find_current_keyframe_returns_none_on_finished_animation() { - let curve = test_variable_curve(); - let max_time = *curve.keyframe_timestamps.last().unwrap(); - - assert!(max_time < f32::INFINITY); - let maybe_keyframe = curve.find_current_keyframe(f32::INFINITY); - assert!(maybe_keyframe.is_none()); - - let maybe_keyframe = curve.find_current_keyframe(max_time); - assert!(maybe_keyframe.is_none()); - } - - #[test] - fn second_last_keyframe_is_found_correctly() { - let curve = test_variable_curve(); - - // Exact time match - let second_last_keyframe = curve.keyframe_timestamps.len() - 2; - let second_last_time = curve.keyframe_timestamps[second_last_keyframe]; - let maybe_keyframe = curve.find_current_keyframe(second_last_time); - assert!(maybe_keyframe.unwrap() == second_last_keyframe); - - // Inexact match, between the last and second last frames - let seek_time = second_last_time + 0.001; - let last_time = curve.keyframe_timestamps[second_last_keyframe + 1]; - assert!(seek_time < last_time); - - let maybe_keyframe = curve.find_current_keyframe(seek_time); - assert!(maybe_keyframe.unwrap() == second_last_keyframe); - } - - #[test] - fn exact_keyframe_matches_are_found_correctly() { - let curve = test_variable_curve(); - let second_last_keyframe = curve.keyframes.len() - 2; - - for i in 0..=second_last_keyframe { - let seek_time = curve.keyframe_timestamps[i]; - - let keyframe = curve.find_current_keyframe(seek_time).unwrap(); - assert!(keyframe == i); - } - } - - #[test] - fn exact_and_inexact_keyframes_correspond() { - let curve = test_variable_curve(); - - let second_last_keyframe = curve.keyframes.len() - 2; - - for i in 0..=second_last_keyframe { - let seek_time = curve.keyframe_timestamps[i]; - - let exact_keyframe = curve.find_current_keyframe(seek_time).unwrap(); - - let inexact_seek_time = seek_time + 0.0001; - let final_time = *curve.keyframe_timestamps.last().unwrap(); - assert!(inexact_seek_time < final_time); - - let inexact_keyframe = curve.find_current_keyframe(inexact_seek_time).unwrap(); - - assert!(exact_keyframe == inexact_keyframe); - } - } + // use crate::VariableCurve; + // use bevy_math::Vec3; + + // fn test_variable_curve() -> VariableCurve { + // let keyframe_timestamps = vec![1.0, 2.0, 3.0, 4.0]; + // let keyframes = vec![ + // Vec3::ONE * 0.0, + // Vec3::ONE * 3.0, + // Vec3::ONE * 6.0, + // Vec3::ONE * 9.0, + // ]; + // let interpolation = crate::Interpolation::Linear; + + // let variable_curve = VariableCurve { + // keyframe_timestamps, + // keyframes: crate::Keyframes::Translation(keyframes), + // interpolation, + // }; + + // assert!(variable_curve.keyframe_timestamps.len() == variable_curve.keyframes.len()); + + // // f32 doesn't impl Ord so we can't easily sort it + // let mut maybe_last_timestamp = None; + // for current_timestamp in &variable_curve.keyframe_timestamps { + // assert!(current_timestamp.is_finite()); + + // if let Some(last_timestamp) = maybe_last_timestamp { + // assert!(current_timestamp > last_timestamp); + // } + // maybe_last_timestamp = Some(current_timestamp); + // } + + // variable_curve + // } + + // #[test] + // fn find_current_keyframe_is_in_bounds() { + // let curve = test_variable_curve(); + // let min_time = *curve.keyframe_timestamps.first().unwrap(); + // // We will always get none at times at or past the second last keyframe + // let second_last_keyframe = curve.keyframe_timestamps.len() - 2; + // let max_time = curve.keyframe_timestamps[second_last_keyframe]; + // let elapsed_time = max_time - min_time; + + // let n_keyframes = curve.keyframe_timestamps.len(); + // let n_test_points = 5; + + // for i in 0..=n_test_points { + // // Get a value between 0 and 1 + // let normalized_time = i as f32 / n_test_points as f32; + // let seek_time = min_time + normalized_time * elapsed_time; + // assert!(seek_time >= min_time); + // assert!(seek_time <= max_time); + + // let maybe_current_keyframe = curve.find_current_keyframe(seek_time); + // assert!( + // maybe_current_keyframe.is_some(), + // "Seek time: {seek_time}, Min time: {min_time}, Max time: {max_time}" + // ); + + // // We cannot return the last keyframe, + // // because we want to interpolate between the current and next keyframe + // assert!(maybe_current_keyframe.unwrap() < n_keyframes); + // } + // } + + // #[test] + // fn find_current_keyframe_returns_none_on_unstarted_animations() { + // let curve = test_variable_curve(); + // let min_time = *curve.keyframe_timestamps.first().unwrap(); + // let seek_time = 0.0; + // assert!(seek_time < min_time); + + // let maybe_keyframe = curve.find_current_keyframe(seek_time); + // assert!( + // maybe_keyframe.is_none(), + // "Seek time: {seek_time}, Minimum time: {min_time}" + // ); + // } + + // #[test] + // fn find_current_keyframe_returns_none_on_finished_animation() { + // let curve = test_variable_curve(); + // let max_time = *curve.keyframe_timestamps.last().unwrap(); + + // assert!(max_time < f32::INFINITY); + // let maybe_keyframe = curve.find_current_keyframe(f32::INFINITY); + // assert!(maybe_keyframe.is_none()); + + // let maybe_keyframe = curve.find_current_keyframe(max_time); + // assert!(maybe_keyframe.is_none()); + // } + + // #[test] + // fn second_last_keyframe_is_found_correctly() { + // let curve = test_variable_curve(); + + // // Exact time match + // let second_last_keyframe = curve.keyframe_timestamps.len() - 2; + // let second_last_time = curve.keyframe_timestamps[second_last_keyframe]; + // let maybe_keyframe = curve.find_current_keyframe(second_last_time); + // assert!(maybe_keyframe.unwrap() == second_last_keyframe); + + // // Inexact match, between the last and second last frames + // let seek_time = second_last_time + 0.001; + // let last_time = curve.keyframe_timestamps[second_last_keyframe + 1]; + // assert!(seek_time < last_time); + + // let maybe_keyframe = curve.find_current_keyframe(seek_time); + // assert!(maybe_keyframe.unwrap() == second_last_keyframe); + // } + + // #[test] + // fn exact_keyframe_matches_are_found_correctly() { + // let curve = test_variable_curve(); + // let second_last_keyframe = curve.keyframes.len() - 2; + + // for i in 0..=second_last_keyframe { + // let seek_time = curve.keyframe_timestamps[i]; + + // let keyframe = curve.find_current_keyframe(seek_time).unwrap(); + // assert!(keyframe == i); + // } + // } + + // #[test] + // fn exact_and_inexact_keyframes_correspond() { + // let curve = test_variable_curve(); + + // let second_last_keyframe = curve.keyframes.len() - 2; + + // for i in 0..=second_last_keyframe { + // let seek_time = curve.keyframe_timestamps[i]; + + // let exact_keyframe = curve.find_current_keyframe(seek_time).unwrap(); + + // let inexact_seek_time = seek_time + 0.0001; + // let final_time = *curve.keyframe_timestamps.last().unwrap(); + // assert!(inexact_seek_time < final_time); + + // let inexact_keyframe = curve.find_current_keyframe(inexact_seek_time).unwrap(); + + // assert!(exact_keyframe == inexact_keyframe); + // } + // } } From 39abc3ad3451401d40b82ecbf3d756fb48ec5973 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Tue, 11 Jun 2024 15:45:42 -0400 Subject: [PATCH 32/44] Refactor glTF loader --- crates/bevy_animation/src/curves.rs | 74 +++++++++++++++ crates/bevy_gltf/src/loader.rs | 142 ++++++++++++++++++++++++---- crates/bevy_math/src/curve/mod.rs | 26 ++++- 3 files changed, 219 insertions(+), 23 deletions(-) diff --git a/crates/bevy_animation/src/curves.rs b/crates/bevy_animation/src/curves.rs index 91c9df1216b61..08c6821d5f8b5 100644 --- a/crates/bevy_animation/src/curves.rs +++ b/crates/bevy_animation/src/curves.rs @@ -25,6 +25,20 @@ where } } +impl SteppedKeyframeCurve { + /// Create a new [`SteppedKeyframeCurve`], bypassing any formatting. If you use this, you must + /// uphold the invariants of [`UnevenCore`] yourself. + #[inline] + pub fn new_raw(times: impl Into>, samples: impl Into>) -> Self { + Self { + core: UnevenCore { + times: times.into(), + samples: samples.into(), + }, + } + } +} + /// A keyframe-defined curve that uses cubic spline interpolation, backed by a contiguous buffer. #[derive(Debug, Clone, Reflect)] pub struct CubicKeyframeCurve { @@ -56,6 +70,21 @@ where } } +impl CubicKeyframeCurve { + /// Create a new [`CubicKeyframeCurve`] from raw data, bypassing all checks. If you use this, you + /// must uphold the invariants of [`ChunkedUnevenCore`] yourself. + #[inline] + pub fn new_raw(times: impl Into>, values: impl Into>) -> Self { + Self { + core: ChunkedUnevenCore { + times: times.into(), + values: values.into(), + width: 3, + }, + } + } +} + // Pie in the sky: `TranslationCurve` is basically the same thing as a `Box>` etc. // The first couple variants can be taken "off the shelf" from the Curve library, while the others // are built on top of the core abstractions. @@ -270,6 +299,21 @@ where } } +impl WideLinearKeyframeCurve { + /// Create a new [`WideLinearKeyframeCurve`] from raw data, bypassing all checks. If you use this, you + /// must uphold the invariants of [`ChunkedUnevenCore`] yourself. + #[inline] + pub fn new_raw(times: impl Into>, values: impl Into>, width: usize) -> Self { + Self { + core: ChunkedUnevenCore { + times: times.into(), + values: values.into(), + width, + }, + } + } +} + /// A keyframe-defined curve that uses stepped "interpolation" over many samples at once, backed /// by a contiguous buffer. #[derive(Debug, Clone, Reflect)] @@ -308,6 +352,21 @@ where } } +impl WideSteppedKeyframeCurve { + /// Create a new [`WideSteppedKeyframeCurve`] from raw data, bypassing all checks. If you use this, you + /// must uphold the invariants of [`ChunkedUnevenCore`] yourself. + #[inline] + pub fn new_raw(times: impl Into>, values: impl Into>, width: usize) -> Self { + Self { + core: ChunkedUnevenCore { + times: times.into(), + values: values.into(), + width, + }, + } + } +} + /// A keyframe-defined curve that uses cubic interpolation over many samples at once, backed by a /// contiguous buffer. #[derive(Debug, Clone, Reflect)] @@ -346,6 +405,21 @@ where } } +impl WideCubicKeyframeCurve { + /// Create a new [`WideCubicKeyframeCurve`] from raw data, bypassing all checks. If you use this, you + /// must uphold the invariants of [`ChunkedUnevenCore`] yourself. + #[inline] + pub fn new_raw(times: impl Into>, values: impl Into>, width: usize) -> Self { + Self { + core: ChunkedUnevenCore { + times: times.into(), + values: values.into(), + width: width * 3, + }, + } + } +} + /// 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). /// diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index c4426026c92de..ab215b66faa38 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -260,7 +260,9 @@ async fn load_gltf<'a, 'b, 'c>( #[cfg(feature = "bevy_animation")] let (animations, named_animations, animation_roots) = { - use bevy_animation::{Interpolation, Keyframes}; + use bevy_animation::curves::*; + use bevy_math::curve::{constant_curve, everywhere, UnevenSampleAutoCurve}; + use bevy_math::{Quat, Vec4}; use gltf::animation::util::ReadOutputs; let mut animations = vec![]; let mut named_animations = HashMap::default(); @@ -268,12 +270,8 @@ async fn load_gltf<'a, 'b, 'c>( for animation in gltf.animations() { let mut animation_clip = bevy_animation::AnimationClip::default(); for channel in animation.channels() { - let interpolation = match channel.sampler().interpolation() { - gltf::animation::Interpolation::Linear => Interpolation::Linear, - gltf::animation::Interpolation::Step => Interpolation::Step, - gltf::animation::Interpolation::CubicSpline => Interpolation::CubicSpline, - }; let node = channel.target().node(); + let interpolation = channel.sampler().interpolation(); let reader = channel.reader(|buffer| Some(&buffer_data[buffer.index()])); let keyframe_timestamps: Vec = if let Some(inputs) = reader.read_inputs() { match inputs { @@ -288,19 +286,127 @@ async fn load_gltf<'a, 'b, 'c>( return Err(GltfError::MissingAnimationSampler(animation.index())); }; - let keyframes = if let Some(outputs) = reader.read_outputs() { + if keyframe_timestamps.len() == 0 { + warn!("Tried to load animation with no keyframe timestamps"); + continue; + } + + let curve = if let Some(outputs) = reader.read_outputs() { match outputs { ReadOutputs::Translations(tr) => { - Keyframes::Translation(tr.map(Vec3::from).collect()) + let translations: Vec = tr.map(Vec3::from).collect(); + VariableCurve::Translation(if keyframe_timestamps.len() == 1 { + TranslationCurve::Constant(constant_curve( + everywhere(), + translations[0], + )) + } else { + match interpolation { + gltf::animation::Interpolation::Linear => { + TranslationCurve::Linear(UnevenSampleAutoCurve::new_raw( + keyframe_timestamps, + translations, + )) + } + gltf::animation::Interpolation::Step => { + TranslationCurve::Step(SteppedKeyframeCurve::new_raw( + keyframe_timestamps, + translations, + )) + } + gltf::animation::Interpolation::CubicSpline => { + TranslationCurve::CubicSpline(CubicKeyframeCurve::new_raw( + keyframe_timestamps, + translations, + )) + } + } + }) + } + ReadOutputs::Rotations(rots) => { + let rotations: Vec = + rots.into_f32().map(bevy_math::Quat::from_array).collect(); + VariableCurve::Rotation(if keyframe_timestamps.len() == 1 { + RotationCurve::Constant(constant_curve(everywhere(), rotations[0])) + } else { + match interpolation { + gltf::animation::Interpolation::Linear => { + RotationCurve::SphericalLinear( + UnevenSampleAutoCurve::new_raw( + keyframe_timestamps, + rotations, + ), + ) + } + gltf::animation::Interpolation::Step => { + RotationCurve::Step(SteppedKeyframeCurve::new_raw( + keyframe_timestamps, + rotations, + )) + } + gltf::animation::Interpolation::CubicSpline => { + RotationCurve::CubicSpline(CubicKeyframeCurve::new_raw( + keyframe_timestamps, + rotations + .into_iter() + .map(Vec4::from) + .collect::>(), + )) + } + } + }) } - ReadOutputs::Rotations(rots) => Keyframes::Rotation( - rots.into_f32().map(bevy_math::Quat::from_array).collect(), - ), ReadOutputs::Scales(scale) => { - Keyframes::Scale(scale.map(Vec3::from).collect()) + let scales: Vec = scale.map(Vec3::from).collect(); + VariableCurve::Scale(if keyframe_timestamps.len() == 1 { + ScaleCurve::Constant(constant_curve(everywhere(), scales[0])) + } else { + match interpolation { + gltf::animation::Interpolation::Linear => ScaleCurve::Linear( + UnevenSampleAutoCurve::new_raw(keyframe_timestamps, scales), + ), + gltf::animation::Interpolation::Step => ScaleCurve::Step( + SteppedKeyframeCurve::new_raw(keyframe_timestamps, scales), + ), + gltf::animation::Interpolation::CubicSpline => { + ScaleCurve::CubicSpline(CubicKeyframeCurve::new_raw( + keyframe_timestamps, + scales, + )) + } + } + }) } ReadOutputs::MorphTargetWeights(weights) => { - Keyframes::Weights(weights.into_f32().collect()) + let weights: Vec = weights.into_f32().collect(); + let width = weights.len() / keyframe_timestamps.len(); + VariableCurve::Weights(if keyframe_timestamps.len() == 1 { + WeightsCurve::Constant(constant_curve(everywhere(), weights)) + } else { + match interpolation { + gltf::animation::Interpolation::Linear => { + WeightsCurve::Linear(WideLinearKeyframeCurve::new_raw( + keyframe_timestamps, + weights, + width, + )) + } + gltf::animation::Interpolation::Step => { + WeightsCurve::Step(WideSteppedKeyframeCurve::new_raw( + keyframe_timestamps, + weights, + width, + )) + } + gltf::animation::Interpolation::CubicSpline => { + WeightsCurve::CubicSpline(WideCubicKeyframeCurve::new_raw( + keyframe_timestamps, + weights, + width, + )) + } + } + }) } } } else { @@ -310,14 +416,8 @@ async fn load_gltf<'a, 'b, 'c>( if let Some((root_index, path)) = paths.get(&node.index()) { animation_roots.insert(*root_index); - animation_clip.add_curve_to_target( - AnimationTargetId::from_names(path.iter()), - bevy_animation::VariableCurve { - keyframe_timestamps, - keyframes, - interpolation, - }, - ); + animation_clip + .add_curve_to_target(AnimationTargetId::from_names(path.iter()), curve); } else { warn!( "Animation ignored for node {}: part of its hierarchy is missing a name", diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 42d699dbcd8bc..bb54a2cdef06b 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -512,6 +512,17 @@ impl SampleAutoCurve { core: EvenCore::new(domain, samples)?, }) } + + /// Create a new [`SampleAutoCurve`] from raw data, bypassing all checks. If you use this, you + /// must uphold the invariants of [`EvenCore`] yourself. + pub fn new_raw(domain: Interval, samples: impl Into>) -> Self { + Self { + core: EvenCore { + domain, + samples: samples.into(), + }, + } + } } /// A [`Curve`] that is defined by interpolation over unevenly spaced samples with explicit @@ -608,6 +619,17 @@ impl UnevenSampleAutoCurve { }) } + /// Create a new [`UnevenSampleAutoCurve`] from raw data, bypassing all checks. If you use this, you + /// must uphold the invariants of [`UnevenCore`] yourself. + pub fn new_raw(times: impl Into>, samples: impl Into>) -> Self { + Self { + core: UnevenCore { + times: times.into(), + samples: samples.into(), + }, + } + } + /// 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. @@ -865,13 +887,13 @@ where } /// Create a [`Curve`] that constantly takes the given `value` over the given `domain`. -pub fn constant_curve(domain: Interval, value: T) -> impl Curve { +pub fn constant_curve(domain: Interval, value: T) -> ConstantCurve { ConstantCurve { domain, value } } /// Convert the given function `f` into a [`Curve`] with the given `domain`, sampled by /// evaluating the function. -pub fn function_curve(domain: Interval, f: F) -> impl Curve +pub fn function_curve(domain: Interval, f: F) -> FunctionCurve where F: Fn(f32) -> T, { From 9078dcdd6a28f595d9e369457e274d1757a7f964 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Tue, 11 Jun 2024 15:56:00 -0400 Subject: [PATCH 33/44] Refactor animated_transform --- examples/animation/animated_transform.rs | 70 ++++++++++++------------ 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/examples/animation/animated_transform.rs b/examples/animation/animated_transform.rs index d85adf1028eb7..6eb3473cf58b6 100644 --- a/examples/animation/animated_transform.rs +++ b/examples/animation/animated_transform.rs @@ -2,7 +2,9 @@ use std::f32::consts::PI; +use bevy::animation::curves::{RotationCurve, ScaleCurve, TranslationCurve}; use bevy::animation::{AnimationTarget, AnimationTargetId}; +use bevy::math::curve::UnevenSampleAutoCurve; use bevy::prelude::*; fn main() { @@ -51,9 +53,9 @@ fn setup( let planet_animation_target_id = AnimationTargetId::from_name(&planet); animation.add_curve_to_target( planet_animation_target_id, - VariableCurve { - keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0, 4.0], - keyframes: Keyframes::Translation(vec![ + VariableCurve::Translation(TranslationCurve::Linear(UnevenSampleAutoCurve::new_raw( + vec![0.0, 1.0, 2.0, 3.0, 4.0], + vec![ Vec3::new(1.0, 0.0, 1.0), Vec3::new(-1.0, 0.0, 1.0), Vec3::new(-1.0, 0.0, -1.0), @@ -61,9 +63,8 @@ fn setup( // in case seamless looping is wanted, the last keyframe should // be the same as the first one Vec3::new(1.0, 0.0, 1.0), - ]), - interpolation: Interpolation::Linear, - }, + ], + ))), ); // Or it can modify the rotation of the transform. // To find the entity to modify, the hierarchy will be traversed looking for @@ -72,17 +73,18 @@ fn setup( AnimationTargetId::from_names([planet.clone(), orbit_controller.clone()].iter()); animation.add_curve_to_target( orbit_controller_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, - }, + VariableCurve::Rotation(RotationCurve::SphericalLinear( + UnevenSampleAutoCurve::new_raw( + vec![0.0, 1.0, 2.0, 3.0, 4.0], + 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, + ], + ), + )), ); // If a curve in an animation is shorter than the other, it will not repeat // until all other curves are finished. In that case, another animation should @@ -92,9 +94,9 @@ fn setup( ); animation.add_curve_to_target( satellite_animation_target_id, - VariableCurve { - keyframe_timestamps: vec![0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0], - keyframes: Keyframes::Scale(vec![ + VariableCurve::Scale(ScaleCurve::Linear(UnevenSampleAutoCurve::new_raw( + vec![0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0], + vec![ Vec3::splat(0.8), Vec3::splat(1.2), Vec3::splat(0.8), @@ -104,26 +106,26 @@ fn setup( Vec3::splat(0.8), Vec3::splat(1.2), Vec3::splat(0.8), - ]), - interpolation: Interpolation::Linear, - }, + ], + ))), ); // There can be more than one curve targeting the same entity path animation.add_curve_to_target( AnimationTargetId::from_names( [planet.clone(), orbit_controller.clone(), satellite.clone()].iter(), ), - 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, - }, + VariableCurve::Rotation(RotationCurve::SphericalLinear( + UnevenSampleAutoCurve::new_raw( + vec![0.0, 1.0, 2.0, 3.0, 4.0], + 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, + ], + ), + )), ); // Create the animation graph From 2803582765fee188d02dd2147c9db4616b93bd55 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Tue, 11 Jun 2024 17:08:23 -0400 Subject: [PATCH 34/44] Lint --- crates/bevy_animation/src/curves.rs | 2 +- crates/bevy_gltf/src/loader.rs | 2 +- crates/bevy_math/src/curve/mod.rs | 64 ++++++++++++++--------------- 3 files changed, 33 insertions(+), 35 deletions(-) diff --git a/crates/bevy_animation/src/curves.rs b/crates/bevy_animation/src/curves.rs index 08c6821d5f8b5..9b7a29b500b20 100644 --- a/crates/bevy_animation/src/curves.rs +++ b/crates/bevy_animation/src/curves.rs @@ -488,7 +488,7 @@ impl WeightsCurve { /// Each variant yields a [`Curve`] over the data that it parametrizes. /// /// This follows the [glTF design]. -/// [glTF design]: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#animations +/// [glTF design]: #[derive(Debug, Clone, Reflect)] pub enum VariableCurve { /// A [`TranslationCurve`] for animating the `translation` component of a [`Transform`]. diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index ab215b66faa38..8c33278bea2b6 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -286,7 +286,7 @@ async fn load_gltf<'a, 'b, 'c>( return Err(GltfError::MissingAnimationSampler(animation.index())); }; - if keyframe_timestamps.len() == 0 { + if keyframe_timestamps.is_empty() { warn!("Tried to load animation with no keyframe timestamps"); continue; } diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index bb54a2cdef06b..6f802692c4864 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -905,6 +905,37 @@ pub fn flip(curve: impl Curve<(S, T)>) -> impl Curve<(T, S)> { curve.map(|(s, t)| (t, s)) } +/// A curve which provides samples in the form of [`Iterator`]s. +/// +/// This is an abstraction that provides an interface for curves which look like `Curve>` +/// but side-stepping issues with allocation on sampling. This happens when the size of an output +/// array cannot be known statically. +pub trait IterableCurve { + /// The interval over which this curve is parametrized. + fn domain(&self) -> Interval; + + /// Sample this curve at a specified time `t`, producing an iterator over sampled values. + fn sample_iter<'a>(&self, t: f32) -> impl Iterator + where + Self: 'a; +} + +impl IterableCurve for ConstantCurve> +where + T: Clone, +{ + fn domain(&self) -> Interval { + self.domain + } + + fn sample_iter<'a>(&self, _t: f32) -> impl Iterator + where + Self: 'a, + { + self.value.iter().cloned() + } +} + #[cfg(test)] mod tests { use super::*; @@ -1055,36 +1086,3 @@ mod tests { assert_abs_diff_eq!(resampled_curve.domain().end(), 512.0); } } - -// Haha... you thought the file was over! - -/// A curve which provides samples in the form of [`Iterator`]s. -/// -/// This is an abstraction that provides an interface for curves which look like `Curve>` -/// but side-stepping issues with allocation on sampling. This happens when the size of an output -/// array cannot be known statically. -pub trait IterableCurve { - /// The interval over which this curve is parametrized. - fn domain(&self) -> Interval; - - /// Sample this curve at a specified time `t`, producing an iterator over sampled values. - fn sample_iter<'a>(&self, t: f32) -> impl Iterator - where - Self: 'a; -} - -impl IterableCurve for ConstantCurve> -where - T: Clone, -{ - fn domain(&self) -> Interval { - self.domain - } - - fn sample_iter<'a>(&self, _t: f32) -> impl Iterator - where - Self: 'a, - { - self.value.iter().cloned() - } -} From cb1a6963c3e1264835f9347cf1f97b6806e09b99 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Tue, 11 Jun 2024 21:55:50 -0400 Subject: [PATCH 35/44] Reduce size of ChunkedUnevenCore --- crates/bevy_math/src/curve/cores.rs | 34 +++++++++++++---------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs index aa9d59171d4dd..955a16fc346d6 100644 --- a/crates/bevy_math/src/curve/cores.rs +++ b/crates/bevy_math/src/curve/cores.rs @@ -373,7 +373,7 @@ impl UnevenCore { /// 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 array. +/// if the sample type can effectively be encoded as a fixed-length slice of values. /// /// [sampling width]: ChunkedUnevenCore::width #[derive(Debug, Clone)] @@ -383,22 +383,15 @@ pub struct ChunkedUnevenCore { /// 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. + /// This must always have a length of at least 2, be sorted, and have no duplicated or + /// non-finite times. pub times: Vec, - /// The values that are used in sampling. Each `width` of these correspond to a single sample. + /// The values that are used in sampling. Each width-worth of these correspond to a single sample. /// /// # Invariants - /// This must always have a length of `width` times that of `times`. + /// The length of this vector must always be some fixed integer multiple of that of `times`. pub values: Vec, - - /// The sampling width, determining how many consecutive elements of `values` are taken in a - /// single sample. - /// - /// # Invariants - /// This must never be zero. - pub width: usize, } /// An error that indicates that a [`ChunkedUnevenCore`] could not be formed. @@ -464,11 +457,7 @@ impl ChunkedUnevenCore { }); } - Ok(Self { - times, - values, - width, - }) + Ok(Self { times, values }) } /// The domain of the curve derived from this core. @@ -482,6 +471,12 @@ impl ChunkedUnevenCore { 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 [`Betweenness`] 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 @@ -509,8 +504,9 @@ impl ChunkedUnevenCore { /// [values]: ChunkedUnevenCore::values #[inline] fn time_index_to_slice(&self, idx: usize) -> &[T] { - let lower_idx = self.width * idx; - let upper_idx = lower_idx + self.width; + let width = self.width(); + let lower_idx = width * idx; + let upper_idx = lower_idx + width; &self.values[lower_idx..upper_idx] } } From 412ecb4b7c49d4f5b0aa44cae6b3cd8f2abfdc26 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Tue, 11 Jun 2024 22:07:02 -0400 Subject: [PATCH 36/44] Slight rework of multisample curves. VariableCurve -> 64 bytes --- crates/bevy_animation/src/curves.rs | 14 +++++--------- crates/bevy_gltf/src/loader.rs | 4 ---- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/crates/bevy_animation/src/curves.rs b/crates/bevy_animation/src/curves.rs index 9b7a29b500b20..0620ef7b76f3f 100644 --- a/crates/bevy_animation/src/curves.rs +++ b/crates/bevy_animation/src/curves.rs @@ -79,7 +79,6 @@ impl CubicKeyframeCurve { core: ChunkedUnevenCore { times: times.into(), values: values.into(), - width: 3, }, } } @@ -303,12 +302,11 @@ impl WideLinearKeyframeCurve { /// Create a new [`WideLinearKeyframeCurve`] from raw data, bypassing all checks. If you use this, you /// must uphold the invariants of [`ChunkedUnevenCore`] yourself. #[inline] - pub fn new_raw(times: impl Into>, values: impl Into>, width: usize) -> Self { + pub fn new_raw(times: impl Into>, values: impl Into>) -> Self { Self { core: ChunkedUnevenCore { times: times.into(), values: values.into(), - width, }, } } @@ -356,12 +354,11 @@ impl WideSteppedKeyframeCurve { /// Create a new [`WideSteppedKeyframeCurve`] from raw data, bypassing all checks. If you use this, you /// must uphold the invariants of [`ChunkedUnevenCore`] yourself. #[inline] - pub fn new_raw(times: impl Into>, values: impl Into>, width: usize) -> Self { + pub fn new_raw(times: impl Into>, values: impl Into>) -> Self { Self { core: ChunkedUnevenCore { times: times.into(), values: values.into(), - width, }, } } @@ -394,12 +391,12 @@ where | Betweenness::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; + let width = self.core.width(); TwoIterators::Left(v[width..(width * 2)].iter().copied()) } Betweenness::Between((t0, u), (t1, v), s) => TwoIterators::Right( - cubic_spline_interpolate_slices(self.core.width / 3, u, v, s, t1 - t0), + cubic_spline_interpolate_slices(self.core.width() / 3, u, v, s, t1 - t0), ), } } @@ -409,12 +406,11 @@ impl WideCubicKeyframeCurve { /// Create a new [`WideCubicKeyframeCurve`] from raw data, bypassing all checks. If you use this, you /// must uphold the invariants of [`ChunkedUnevenCore`] yourself. #[inline] - pub fn new_raw(times: impl Into>, values: impl Into>, width: usize) -> Self { + pub fn new_raw(times: impl Into>, values: impl Into>) -> Self { Self { core: ChunkedUnevenCore { times: times.into(), values: values.into(), - width: width * 3, }, } } diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index 8c33278bea2b6..761fb4430680e 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -379,7 +379,6 @@ async fn load_gltf<'a, 'b, 'c>( } ReadOutputs::MorphTargetWeights(weights) => { let weights: Vec = weights.into_f32().collect(); - let width = weights.len() / keyframe_timestamps.len(); VariableCurve::Weights(if keyframe_timestamps.len() == 1 { WeightsCurve::Constant(constant_curve(everywhere(), weights)) } else { @@ -388,21 +387,18 @@ async fn load_gltf<'a, 'b, 'c>( WeightsCurve::Linear(WideLinearKeyframeCurve::new_raw( keyframe_timestamps, weights, - width, )) } gltf::animation::Interpolation::Step => { WeightsCurve::Step(WideSteppedKeyframeCurve::new_raw( keyframe_timestamps, weights, - width, )) } gltf::animation::Interpolation::CubicSpline => { WeightsCurve::CubicSpline(WideCubicKeyframeCurve::new_raw( keyframe_timestamps, weights, - width, )) } } From cc558ed370911fd47d2b0f32b9d7af68b16abd44 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Fri, 16 Aug 2024 13:47:35 -0400 Subject: [PATCH 37/44] Move IterableCurve into its own module --- crates/bevy_animation/src/curves.rs | 6 +++-- crates/bevy_animation/src/lib.rs | 2 +- crates/bevy_math/src/curve/iterable.rs | 35 ++++++++++++++++++++++++++ crates/bevy_math/src/curve/mod.rs | 32 +---------------------- 4 files changed, 41 insertions(+), 34 deletions(-) create mode 100644 crates/bevy_math/src/curve/iterable.rs diff --git a/crates/bevy_animation/src/curves.rs b/crates/bevy_animation/src/curves.rs index 0620ef7b76f3f..00dbb1faa6ddb 100644 --- a/crates/bevy_animation/src/curves.rs +++ b/crates/bevy_animation/src/curves.rs @@ -1,6 +1,9 @@ //! Curve structures used by the animation system. -use bevy_math::{curve::cores::*, curve::*, Quat, Vec3, Vec4, VectorSpace}; +use bevy_math::{ + curve::{cores::*, iterable::IterableCurve, *}, + Quat, Vec3, Vec4, VectorSpace, +}; use bevy_reflect::Reflect; /// A keyframe-defined curve that "interpolates" by stepping at `t = 1.0` to the next keyframe. @@ -84,7 +87,6 @@ impl CubicKeyframeCurve { } } -// Pie in the sky: `TranslationCurve` is basically the same thing as a `Box>` etc. // The first couple variants can be taken "off the shelf" from the Curve library, while the others // are built on top of the core abstractions. diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 19a0952513ee0..b989d78bbe989 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -13,7 +13,7 @@ mod graph; mod transition; mod util; -use bevy_math::curve::{Curve, IterableCurve}; +use bevy_math::curve::{iterable::IterableCurve, Curve}; use curves::VariableCurve; use std::cell::RefCell; diff --git a/crates/bevy_math/src/curve/iterable.rs b/crates/bevy_math/src/curve/iterable.rs new file mode 100644 index 0000000000000..6b530e459a1ec --- /dev/null +++ b/crates/bevy_math/src/curve/iterable.rs @@ -0,0 +1,35 @@ +//! Iterable curves, which sample in the form of an iterator in order to support `Vec`-like +//! output whose length cannot be known statically. + +use super::{ConstantCurve, Interval}; + +/// A curve which provides samples in the form of [`Iterator`]s. +/// +/// This is an abstraction that provides an interface for curves which look like `Curve>` +/// but side-stepping issues with allocation on sampling. This happens when the size of an output +/// array cannot be known statically. +pub trait IterableCurve { + /// The interval over which this curve is parametrized. + fn domain(&self) -> Interval; + + /// Sample this curve at a specified time `t`, producing an iterator over sampled values. + fn sample_iter<'a>(&self, t: f32) -> impl Iterator + where + Self: 'a; +} + +impl IterableCurve for ConstantCurve> +where + T: Clone, +{ + fn domain(&self) -> Interval { + self.domain + } + + fn sample_iter<'a>(&self, _t: f32) -> impl Iterator + where + Self: 'a, + { + self.value.iter().cloned() + } +} diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 6f802692c4864..bae07a6c3e033 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -4,6 +4,7 @@ pub mod cores; pub mod interval; +pub mod iterable; pub use interval::{everywhere, interval, Interval}; @@ -905,37 +906,6 @@ pub fn flip(curve: impl Curve<(S, T)>) -> impl Curve<(T, S)> { curve.map(|(s, t)| (t, s)) } -/// A curve which provides samples in the form of [`Iterator`]s. -/// -/// This is an abstraction that provides an interface for curves which look like `Curve>` -/// but side-stepping issues with allocation on sampling. This happens when the size of an output -/// array cannot be known statically. -pub trait IterableCurve { - /// The interval over which this curve is parametrized. - fn domain(&self) -> Interval; - - /// Sample this curve at a specified time `t`, producing an iterator over sampled values. - fn sample_iter<'a>(&self, t: f32) -> impl Iterator - where - Self: 'a; -} - -impl IterableCurve for ConstantCurve> -where - T: Clone, -{ - fn domain(&self) -> Interval { - self.domain - } - - fn sample_iter<'a>(&self, _t: f32) -> impl Iterator - where - Self: 'a, - { - self.value.iter().cloned() - } -} - #[cfg(test)] mod tests { use super::*; From 987d2b447829e7f342d6e0e5695ac72f67e48410 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Fri, 16 Aug 2024 15:14:33 -0400 Subject: [PATCH 38/44] Delete outdated Reflect stubs --- crates/bevy_reflect/src/impls/math/curve.rs | 1 - 1 file changed, 1 deletion(-) delete mode 100644 crates/bevy_reflect/src/impls/math/curve.rs diff --git a/crates/bevy_reflect/src/impls/math/curve.rs b/crates/bevy_reflect/src/impls/math/curve.rs deleted file mode 100644 index b3a20d555e3f1..0000000000000 --- a/crates/bevy_reflect/src/impls/math/curve.rs +++ /dev/null @@ -1 +0,0 @@ -impl_reflect!(); From 22491e1034a12c8e0d16bc9e387cbaba59015410 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sun, 18 Aug 2024 13:03:47 -0400 Subject: [PATCH 39/44] Obliterate unchecked curve constructions --- crates/bevy_animation/src/curves.rs | 132 ++++++++++++++------- crates/bevy_gltf/src/loader.rs | 172 ++++++++++++++++------------ crates/bevy_math/src/curve/cores.rs | 65 ++++++++++- 3 files changed, 247 insertions(+), 122 deletions(-) diff --git a/crates/bevy_animation/src/curves.rs b/crates/bevy_animation/src/curves.rs index 34320a1ecc6ba..fbccb9563f2cd 100644 --- a/crates/bevy_animation/src/curves.rs +++ b/crates/bevy_animation/src/curves.rs @@ -5,6 +5,7 @@ use bevy_math::{ Quat, Vec3, Vec4, VectorSpace, }; use bevy_reflect::Reflect; +use thiserror::Error; /// A keyframe-defined curve that "interpolates" by stepping at `t = 1.0` to the next keyframe. #[derive(Debug, Clone, Reflect)] @@ -29,16 +30,13 @@ where } impl SteppedKeyframeCurve { - /// Create a new [`SteppedKeyframeCurve`], bypassing any formatting. If you use this, you must - /// uphold the invariants of [`UnevenCore`] yourself. + /// Create a new [`SteppedKeyframeCurve`]. If the curve could not be constructed from the + /// given data, an error is returned. #[inline] - pub fn new_raw(times: impl Into>, samples: impl Into>) -> Self { - Self { - core: UnevenCore { - times: times.into(), - samples: samples.into(), - }, - } + pub fn new(timed_samples: impl IntoIterator) -> Result { + Ok(Self { + core: UnevenCore::new(timed_samples)?, + }) } } @@ -74,16 +72,25 @@ where } impl CubicKeyframeCurve { - /// Create a new [`CubicKeyframeCurve`] from raw data, bypassing all checks. If you use this, you - /// must uphold the invariants of [`ChunkedUnevenCore`] yourself. + /// 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_raw(times: impl Into>, values: impl Into>) -> Self { - Self { - core: ChunkedUnevenCore { - times: times.into(), - values: values.into(), - }, - } + pub fn new( + times: impl IntoIterator, + values: impl IntoIterator, + ) -> Result { + Ok(Self { + core: ChunkedUnevenCore::new(times, values, 3)?, + }) } } @@ -303,16 +310,19 @@ where } impl WideLinearKeyframeCurve { - /// Create a new [`WideLinearKeyframeCurve`] from raw data, bypassing all checks. If you use this, you - /// must uphold the invariants of [`ChunkedUnevenCore`] yourself. + /// 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_raw(times: impl Into>, values: impl Into>) -> Self { - Self { - core: ChunkedUnevenCore { - times: times.into(), - values: values.into(), - }, - } + pub fn new( + times: impl IntoIterator, + values: impl IntoIterator, + ) -> Result { + Ok(Self { + core: ChunkedUnevenCore::new_width_inferred(times, values)?, + }) } } @@ -355,16 +365,19 @@ where } impl WideSteppedKeyframeCurve { - /// Create a new [`WideSteppedKeyframeCurve`] from raw data, bypassing all checks. If you use this, you - /// must uphold the invariants of [`ChunkedUnevenCore`] yourself. + /// 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_raw(times: impl Into>, values: impl Into>) -> Self { - Self { - core: ChunkedUnevenCore { - times: times.into(), - values: values.into(), - }, - } + pub fn new( + times: impl IntoIterator, + values: impl IntoIterator, + ) -> Result { + Ok(Self { + core: ChunkedUnevenCore::new_width_inferred(times, values)?, + }) } } @@ -406,17 +419,48 @@ where } } +/// 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("The number of values ({values_given}) was expected to be 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. + CoreError(#[from] ChunkedUnevenCoreError), +} + impl WideCubicKeyframeCurve { - /// Create a new [`WideCubicKeyframeCurve`] from raw data, bypassing all checks. If you use this, you - /// must uphold the invariants of [`ChunkedUnevenCore`] yourself. + /// 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_raw(times: impl Into>, values: impl Into>) -> Self { - Self { - core: ChunkedUnevenCore { - times: times.into(), - values: values.into(), - }, + pub fn new( + times: impl IntoIterator, + values: impl IntoIterator, + ) -> Result { + let times: Vec = times.into_iter().collect(); + let values: Vec = 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)?, + }) } } diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index 8fb4b701f2e79..48dcdb9386cd9 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -294,132 +294,150 @@ async fn load_gltf<'a, 'b, 'c>( continue; } - let curve = if let Some(outputs) = reader.read_outputs() { + let maybe_curve: Option = if let Some(outputs) = + reader.read_outputs() + { match outputs { ReadOutputs::Translations(tr) => { let translations: Vec = tr.map(Vec3::from).collect(); - VariableCurve::Translation(if keyframe_timestamps.len() == 1 { - TranslationCurve::Constant(constant_curve( - Interval::EVERYWHERE, - translations[0], - )) + if keyframe_timestamps.len() == 1 { + Some(VariableCurve::Translation(TranslationCurve::Constant( + constant_curve(Interval::EVERYWHERE, translations[0]), + ))) } else { match interpolation { gltf::animation::Interpolation::Linear => { - TranslationCurve::Linear( - UnevenSampleAutoCurve::new( - keyframe_timestamps.into_iter().zip(translations), - ) - .unwrap(), + UnevenSampleAutoCurve::new( + keyframe_timestamps.into_iter().zip(translations), ) + .ok() + .map(|c| { + VariableCurve::Translation(TranslationCurve::Linear(c)) + }) } gltf::animation::Interpolation::Step => { - TranslationCurve::Step(SteppedKeyframeCurve::new_raw( - keyframe_timestamps, - translations, - )) + SteppedKeyframeCurve::new( + keyframe_timestamps.into_iter().zip(translations), + ) + .ok() + .map(|c| { + VariableCurve::Translation(TranslationCurve::Step(c)) + }) } gltf::animation::Interpolation::CubicSpline => { - TranslationCurve::CubicSpline(CubicKeyframeCurve::new_raw( - keyframe_timestamps, - translations, - )) + CubicKeyframeCurve::new(keyframe_timestamps, translations) + .ok() + .map(|c| { + VariableCurve::Translation( + TranslationCurve::CubicSpline(c), + ) + }) } } - }) + } } ReadOutputs::Rotations(rots) => { let rotations: Vec = rots.into_f32().map(bevy_math::Quat::from_array).collect(); - VariableCurve::Rotation(if keyframe_timestamps.len() == 1 { - RotationCurve::Constant(constant_curve( - Interval::EVERYWHERE, - rotations[0], - )) + if keyframe_timestamps.len() == 1 { + Some(VariableCurve::Rotation(RotationCurve::Constant( + constant_curve(Interval::EVERYWHERE, rotations[0]), + ))) } else { match interpolation { gltf::animation::Interpolation::Linear => { - RotationCurve::SphericalLinear( - UnevenSampleAutoCurve::new( - keyframe_timestamps.into_iter().zip(rotations), - ) - .unwrap(), + UnevenSampleAutoCurve::new( + keyframe_timestamps.into_iter().zip(rotations), ) + .ok() + .map(|c| { + VariableCurve::Rotation(RotationCurve::SphericalLinear( + c, + )) + }) } gltf::animation::Interpolation::Step => { - RotationCurve::Step(SteppedKeyframeCurve::new_raw( - keyframe_timestamps, - rotations, - )) + SteppedKeyframeCurve::new( + keyframe_timestamps.into_iter().zip(rotations), + ) + .ok() + .map(|c| VariableCurve::Rotation(RotationCurve::Step(c))) } gltf::animation::Interpolation::CubicSpline => { - RotationCurve::CubicSpline(CubicKeyframeCurve::new_raw( + CubicKeyframeCurve::new( keyframe_timestamps, - rotations - .into_iter() - .map(Vec4::from) - .collect::>(), - )) + rotations.into_iter().map(Vec4::from), + ) + .ok() + .map(|c| { + VariableCurve::Rotation(RotationCurve::CubicSpline(c)) + }) } } - }) + } } ReadOutputs::Scales(scale) => { let scales: Vec = scale.map(Vec3::from).collect(); - VariableCurve::Scale(if keyframe_timestamps.len() == 1 { - ScaleCurve::Constant(constant_curve( + if keyframe_timestamps.len() == 1 { + Some(VariableCurve::Scale(ScaleCurve::Constant(constant_curve( Interval::EVERYWHERE, scales[0], - )) + )))) } else { match interpolation { - gltf::animation::Interpolation::Linear => ScaleCurve::Linear( + gltf::animation::Interpolation::Linear => { UnevenSampleAutoCurve::new( keyframe_timestamps.into_iter().zip(scales), ) - .unwrap(), - ), - gltf::animation::Interpolation::Step => ScaleCurve::Step( - SteppedKeyframeCurve::new_raw(keyframe_timestamps, scales), - ), + .ok() + .map(|c| VariableCurve::Scale(ScaleCurve::Linear(c))) + } + gltf::animation::Interpolation::Step => { + SteppedKeyframeCurve::new( + keyframe_timestamps.into_iter().zip(scales), + ) + .ok() + .map(|c| VariableCurve::Scale(ScaleCurve::Step(c))) + } gltf::animation::Interpolation::CubicSpline => { - ScaleCurve::CubicSpline(CubicKeyframeCurve::new_raw( - keyframe_timestamps, - scales, - )) + CubicKeyframeCurve::new(keyframe_timestamps, scales) + .ok() + .map(|c| { + VariableCurve::Scale(ScaleCurve::CubicSpline(c)) + }) } } - }) + } } ReadOutputs::MorphTargetWeights(weights) => { let weights: Vec = weights.into_f32().collect(); - VariableCurve::Weights(if keyframe_timestamps.len() == 1 { - WeightsCurve::Constant(constant_curve( - Interval::EVERYWHERE, - weights, - )) + if keyframe_timestamps.len() == 1 { + Some(VariableCurve::Weights(WeightsCurve::Constant( + constant_curve(Interval::EVERYWHERE, weights), + ))) } else { match interpolation { gltf::animation::Interpolation::Linear => { - WeightsCurve::Linear(WideLinearKeyframeCurve::new_raw( - keyframe_timestamps, - weights, - )) + WideLinearKeyframeCurve::new(keyframe_timestamps, weights) + .ok() + .map(|c| { + VariableCurve::Weights(WeightsCurve::Linear(c)) + }) } gltf::animation::Interpolation::Step => { - WeightsCurve::Step(WideSteppedKeyframeCurve::new_raw( - keyframe_timestamps, - weights, - )) + WideSteppedKeyframeCurve::new(keyframe_timestamps, weights) + .ok() + .map(|c| VariableCurve::Weights(WeightsCurve::Step(c))) } gltf::animation::Interpolation::CubicSpline => { - WeightsCurve::CubicSpline(WideCubicKeyframeCurve::new_raw( - keyframe_timestamps, - weights, - )) + WideCubicKeyframeCurve::new(keyframe_timestamps, weights) + .ok() + .map(|c| { + VariableCurve::Weights(WeightsCurve::CubicSpline(c)) + }) } } - }) + } } } } else { @@ -427,6 +445,14 @@ async fn load_gltf<'a, 'b, 'c>( return Err(GltfError::MissingAnimationSampler(animation.index())); }; + let Some(curve) = maybe_curve else { + warn!( + "Invalid keyframe data for node {}; curve could not be constructed", + node.index() + ); + continue; + }; + if let Some((root_index, path)) = paths.get(&node.index()) { animation_roots.insert(*root_index); animation_clip diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs index abe9225c7aa44..3d3c3ffc6be9e 100644 --- a/crates/bevy_math/src/curve/cores.rs +++ b/crates/bevy_math/src/curve/cores.rs @@ -496,6 +496,15 @@ pub enum ChunkedUnevenCoreError { /// The actual length of the value buffer. actual: usize, }, + + /// Tried to infer the width, but the ratio of lengths wasn't an integer, so no such length exists. + #[error("The length of the list of values ({values_len}) was not divisible by that of the list of times ({times_len})")] + NonDivisibleLengths { + /// The length of the value buffer. + values_len: usize, + /// The length of the time buffer. + times_len: usize, + }, } impl ChunkedUnevenCore { @@ -504,17 +513,17 @@ impl ChunkedUnevenCore { /// /// Produces an error in any of the following circumstances: /// - `width` is zero. - /// - `times` has less than `2` valid unique entries. + /// - `times` has less than `2` unique valid entries. /// - `values` has the incorrect length relative to `times`. /// /// [type-level documentation]: ChunkedUnevenCore pub fn new( - times: impl Into>, - values: impl Into>, + times: impl IntoIterator, + values: impl IntoIterator, width: usize, ) -> Result { - let times: Vec = times.into(); - let values: Vec = values.into(); + let times = times.into_iter().collect_vec(); + let values = values.into_iter().collect_vec(); if width == 0 { return Err(ChunkedUnevenCoreError::ZeroWidth); @@ -538,6 +547,52 @@ impl ChunkedUnevenCore { Ok(Self { times, values }) } + /// Create a new [`ChunkedUnevenCore`], inferring the width from the sizes of the inputs. + /// The given `times` are sorted, filtered to finite times, and deduplicated. See the + /// [type-level documentation] for more information about this type. Prefer using [`new`] + /// if possible, since that constructor has richer error checking. + /// + /// Produces an error in any of the following circumstances: + /// - `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). + /// + /// The [width] is implicitly taken to be the length of `values` divided by that of `times` + /// (once sorted, filtered, and deduplicated). + /// + /// [type-level documentation]: ChunkedUnevenCore + /// [`new`]: ChunkedUnevenCore::new + /// [width]: ChunkedUnevenCore::width + pub fn new_width_inferred( + times: impl IntoIterator, + values: impl IntoIterator, + ) -> Result { + let times = times.into_iter().collect_vec(); + let values = values.into_iter().collect_vec(); + + let times = filter_sort_dedup_times(times); + + if times.len() < 2 { + return Err(ChunkedUnevenCoreError::NotEnoughSamples { + samples: times.len(), + }); + } + + if values.len() % times.len() != 0 { + return Err(ChunkedUnevenCoreError::NonDivisibleLengths { + values_len: values.len(), + times_len: times.len(), + }); + } + + if values.is_empty() { + return Err(ChunkedUnevenCoreError::ZeroWidth); + } + + Ok(Self { times, values }) + } + /// The domain of the curve derived from this core. /// /// # Panics From 1fe2f4d2722b667f404569990b2f8abf0f03e88a Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sun, 18 Aug 2024 13:18:30 -0400 Subject: [PATCH 40/44] Fix doc links --- crates/bevy_animation/src/curves.rs | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/crates/bevy_animation/src/curves.rs b/crates/bevy_animation/src/curves.rs index fbccb9563f2cd..0319b6dd4203c 100644 --- a/crates/bevy_animation/src/curves.rs +++ b/crates/bevy_animation/src/curves.rs @@ -94,14 +94,14 @@ impl CubicKeyframeCurve { } } -// The first couple variants can be taken "off the shelf" from the Curve library, while the others -// are built on top of the core abstractions. - /// A curve specifying the translation component of a [`Transform`] in animation. The variants are /// broken down by interpolation mode (with the exception of `Constant`, which never interpolates). /// /// This type is, itself, a `Curve`, and it internally uses the provided sampling modes; each /// variant "knows" its own interpolation mode. +/// +/// [`Transform`]: bevy_transform::components::Transform + #[derive(Clone, Debug, Reflect)] pub enum TranslationCurve { /// A curve which takes a constant value over its domain. Notably, this is how animations with @@ -160,6 +160,8 @@ impl TranslationCurve { /// /// This type is, itself, a `Curve`, and it internally uses the provided sampling modes; each /// variant "knows" its own interpolation mode. +/// +/// [`Transform`]: bevy_transform::components::Transform #[derive(Clone, Debug, Reflect)] pub enum ScaleCurve { /// A curve which takes a constant value over its domain. Notably, this is how animations with @@ -218,6 +220,8 @@ impl ScaleCurve { /// /// This type is, itself, a `Curve`, and it internally uses the provided sampling modes; each /// variant "knows" its own interpolation mode. +/// +/// [`Transform`]: bevy_transform::components::Transform #[derive(Clone, Debug, Reflect)] pub enum RotationCurve { /// A curve which takes a constant value over its domain. Notably, this is how animations with @@ -470,6 +474,8 @@ impl WideCubicKeyframeCurve { /// This type is, itself, a `Curve>`; 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_render::prelude::MorphWeights #[derive(Debug, Clone, Reflect)] pub enum WeightsCurve { /// A curve which takes a constant value over its domain. Notably, this is how animations with @@ -533,18 +539,29 @@ impl WeightsCurve { /// /// This follows the [glTF design]. /// [glTF design]: +/// +/// [`Transform`]: bevy_transform::components::Transform +/// [`MorphWeights`]: bevy_render::prelude::MorphWeights #[derive(Debug, Clone, Reflect)] pub enum VariableCurve { /// A [`TranslationCurve`] for animating the `translation` component of a [`Transform`]. + /// + /// [`Transform`]: bevy_transform::components::Transform Translation(TranslationCurve), /// A [`RotationCurve`] for animating the `rotation` component of a [`Transform`]. + /// + /// [`Transform`]: bevy_transform::components::Transform Rotation(RotationCurve), /// A [`ScaleCurve`] for animating the `scale` component of a [`Transform`]. + /// + /// [`Transform`]: bevy_transform::components::Transform Scale(ScaleCurve), /// A [`WeightsCurve`] for animating [`MorphWeights`] of a mesh. + /// + /// [`MorphWeights`]: bevy_render::prelude::MorphWeights Weights(WeightsCurve), } From 52d65c83ed9bd4c54064aec5c1af802185ce6412 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 19 Aug 2024 06:13:15 -0400 Subject: [PATCH 41/44] Replace keyframe tests in bevy_animation with tests in bevy_math::curve::cores --- crates/bevy_animation/src/curves.rs | 16 +-- crates/bevy_animation/src/lib.rs | 151 +------------------------ crates/bevy_math/src/curve/cores.rs | 131 +++++++++++++++++++++ crates/bevy_math/src/curve/iterable.rs | 4 +- 4 files changed, 142 insertions(+), 160 deletions(-) diff --git a/crates/bevy_animation/src/curves.rs b/crates/bevy_animation/src/curves.rs index 0319b6dd4203c..db64cfdab4482 100644 --- a/crates/bevy_animation/src/curves.rs +++ b/crates/bevy_animation/src/curves.rs @@ -296,7 +296,7 @@ where } #[inline] - fn sample_iter<'a>(&self, t: f32) -> impl Iterator + fn sample_iter_unchecked<'a>(&self, t: f32) -> impl Iterator where Self: 'a, { @@ -348,7 +348,7 @@ where } #[inline] - fn sample_iter<'a>(&self, t: f32) -> impl Iterator + fn sample_iter_unchecked<'a>(&self, t: f32) -> impl Iterator where Self: 'a, { @@ -402,7 +402,7 @@ where } #[inline] - fn sample_iter<'a>(&self, t: f32) -> impl Iterator + fn sample_iter_unchecked<'a>(&self, t: f32) -> impl Iterator where Self: 'a, { @@ -505,15 +505,15 @@ impl IterableCurve for WeightsCurve { } #[inline] - fn sample_iter<'a>(&self, t: f32) -> impl Iterator + fn sample_iter_unchecked<'a>(&self, t: f32) -> impl Iterator where Self: 'a, { match self { - WeightsCurve::Constant(c) => FourIterators::First(c.sample_iter(t)), - WeightsCurve::Linear(c) => FourIterators::Second(c.sample_iter(t)), - WeightsCurve::Step(c) => FourIterators::Third(c.sample_iter(t)), - WeightsCurve::CubicSpline(c) => FourIterators::Fourth(c.sample_iter(t)), + WeightsCurve::Constant(c) => FourIterators::First(c.sample_iter_unchecked(t)), + WeightsCurve::Linear(c) => FourIterators::Second(c.sample_iter_unchecked(t)), + WeightsCurve::Step(c) => FourIterators::Third(c.sample_iter_unchecked(t)), + WeightsCurve::CubicSpline(c) => FourIterators::Fourth(c.sample_iter_unchecked(t)), } } } diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index a856efd47d45f..9bcde33472777 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -795,7 +795,7 @@ impl AnimationTargetContext<'_> { lerp_morph_weights( morphs.weights_mut(), - weights_curve.sample_iter(seek_time), + weights_curve.sample_iter_unchecked(seek_time), weight, ); } @@ -888,152 +888,3 @@ impl AnimationGraphEvaluator { self.weights.extend(iter::repeat(0.0).take(node_count)); } } - -#[cfg(test)] -mod tests { - // use crate::VariableCurve; - // use bevy_math::Vec3; - - // fn test_variable_curve() -> VariableCurve { - // let keyframe_timestamps = vec![1.0, 2.0, 3.0, 4.0]; - // let keyframes = vec![ - // Vec3::ONE * 0.0, - // Vec3::ONE * 3.0, - // Vec3::ONE * 6.0, - // Vec3::ONE * 9.0, - // ]; - // let interpolation = crate::Interpolation::Linear; - - // let variable_curve = VariableCurve { - // keyframe_timestamps, - // keyframes: crate::Keyframes::Translation(keyframes), - // interpolation, - // }; - - // assert!(variable_curve.keyframe_timestamps.len() == variable_curve.keyframes.len()); - - // // f32 doesn't impl Ord so we can't easily sort it - // let mut maybe_last_timestamp = None; - // for current_timestamp in &variable_curve.keyframe_timestamps { - // assert!(current_timestamp.is_finite()); - - // if let Some(last_timestamp) = maybe_last_timestamp { - // assert!(current_timestamp > last_timestamp); - // } - // maybe_last_timestamp = Some(current_timestamp); - // } - - // variable_curve - // } - - // #[test] - // fn find_current_keyframe_is_in_bounds() { - // let curve = test_variable_curve(); - // let min_time = *curve.keyframe_timestamps.first().unwrap(); - // // We will always get none at times at or past the second last keyframe - // let second_last_keyframe = curve.keyframe_timestamps.len() - 2; - // let max_time = curve.keyframe_timestamps[second_last_keyframe]; - // let elapsed_time = max_time - min_time; - - // let n_keyframes = curve.keyframe_timestamps.len(); - // let n_test_points = 5; - - // for i in 0..=n_test_points { - // // Get a value between 0 and 1 - // let normalized_time = i as f32 / n_test_points as f32; - // let seek_time = min_time + normalized_time * elapsed_time; - // assert!(seek_time >= min_time); - // assert!(seek_time <= max_time); - - // let maybe_current_keyframe = curve.find_current_keyframe(seek_time); - // assert!( - // maybe_current_keyframe.is_some(), - // "Seek time: {seek_time}, Min time: {min_time}, Max time: {max_time}" - // ); - - // // We cannot return the last keyframe, - // // because we want to interpolate between the current and next keyframe - // assert!(maybe_current_keyframe.unwrap() < n_keyframes); - // } - // } - - // #[test] - // fn find_current_keyframe_returns_none_on_unstarted_animations() { - // let curve = test_variable_curve(); - // let min_time = *curve.keyframe_timestamps.first().unwrap(); - // let seek_time = 0.0; - // assert!(seek_time < min_time); - - // let maybe_keyframe = curve.find_current_keyframe(seek_time); - // assert!( - // maybe_keyframe.is_none(), - // "Seek time: {seek_time}, Minimum time: {min_time}" - // ); - // } - - // #[test] - // fn find_current_keyframe_returns_none_on_finished_animation() { - // let curve = test_variable_curve(); - // let max_time = *curve.keyframe_timestamps.last().unwrap(); - - // assert!(max_time < f32::INFINITY); - // let maybe_keyframe = curve.find_current_keyframe(f32::INFINITY); - // assert!(maybe_keyframe.is_none()); - - // let maybe_keyframe = curve.find_current_keyframe(max_time); - // assert!(maybe_keyframe.is_none()); - // } - - // #[test] - // fn second_last_keyframe_is_found_correctly() { - // let curve = test_variable_curve(); - - // // Exact time match - // let second_last_keyframe = curve.keyframe_timestamps.len() - 2; - // let second_last_time = curve.keyframe_timestamps[second_last_keyframe]; - // let maybe_keyframe = curve.find_current_keyframe(second_last_time); - // assert!(maybe_keyframe.unwrap() == second_last_keyframe); - - // // Inexact match, between the last and second last frames - // let seek_time = second_last_time + 0.001; - // let last_time = curve.keyframe_timestamps[second_last_keyframe + 1]; - // assert!(seek_time < last_time); - - // let maybe_keyframe = curve.find_current_keyframe(seek_time); - // assert!(maybe_keyframe.unwrap() == second_last_keyframe); - // } - - // #[test] - // fn exact_keyframe_matches_are_found_correctly() { - // let curve = test_variable_curve(); - // let second_last_keyframe = curve.keyframes.len() - 2; - - // for i in 0..=second_last_keyframe { - // let seek_time = curve.keyframe_timestamps[i]; - - // let keyframe = curve.find_current_keyframe(seek_time).unwrap(); - // assert!(keyframe == i); - // } - // } - - // #[test] - // fn exact_and_inexact_keyframes_correspond() { - // let curve = test_variable_curve(); - - // let second_last_keyframe = curve.keyframes.len() - 2; - - // for i in 0..=second_last_keyframe { - // let seek_time = curve.keyframe_timestamps[i]; - - // let exact_keyframe = curve.find_current_keyframe(seek_time).unwrap(); - - // let inexact_seek_time = seek_time + 0.0001; - // let final_time = *curve.keyframe_timestamps.last().unwrap(); - // assert!(inexact_seek_time < final_time); - - // let inexact_keyframe = curve.find_current_keyframe(inexact_seek_time).unwrap(); - - // assert!(exact_keyframe == inexact_keyframe); - // } - // } -} diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs index 3d3c3ffc6be9e..1c1839cef5f2e 100644 --- a/crates/bevy_math/src/curve/cores.rs +++ b/crates/bevy_math/src/curve/cores.rs @@ -681,3 +681,134 @@ pub fn uneven_interp(times: &[f32], t: f32) -> InterpolationDatum { } } } + +#[cfg(test)] +mod tests { + use super::{ChunkedUnevenCore, EvenCore, UnevenCore}; + use crate::curve::{cores::InterpolationDatum, interval}; + use approx::{assert_abs_diff_eq, AbsDiffEq}; + + fn approx_between(datum: InterpolationDatum, start: T, end: T, p: f32) -> bool + where + T: PartialEq, + { + if let InterpolationDatum::Between(m_start, m_end, m_p) = datum { + m_start == start && m_end == end && m_p.abs_diff_eq(&p, 1e-6) + } else { + false + } + } + + fn is_left_tail(datum: InterpolationDatum) -> bool { + matches!(datum, InterpolationDatum::LeftTail(_)) + } + + fn is_right_tail(datum: InterpolationDatum) -> bool { + matches!(datum, InterpolationDatum::RightTail(_)) + } + + fn is_exact(datum: InterpolationDatum, target: T) -> bool + where + T: PartialEq, + { + if let InterpolationDatum::Exact(v) = datum { + v == target + } else { + false + } + } + + #[test] + fn even_sample_interp() { + let even_core = EvenCore::::new( + interval(0.0, 1.0).unwrap(), + // 11 entries -> 10 segments + vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0], + ) + .expect("Failed to construct test core"); + + let datum = even_core.sample_interp(-1.0); + assert!(is_left_tail(datum)); + let datum = even_core.sample_interp(0.0); + assert!(is_left_tail(datum)); + let datum = even_core.sample_interp(1.0); + assert!(is_right_tail(datum)); + let datum = even_core.sample_interp(2.0); + assert!(is_right_tail(datum)); + + let datum = even_core.sample_interp(0.05); + let InterpolationDatum::Between(0.0, 1.0, p) = datum else { + panic!("Sample did not lie in the correct subinterval") + }; + assert_abs_diff_eq!(p, 0.5); + + let datum = even_core.sample_interp(0.05); + assert!(approx_between(datum, &0.0, &1.0, 0.5)); + let datum = even_core.sample_interp(0.33); + assert!(approx_between(datum, &3.0, &4.0, 0.3)); + let datum = even_core.sample_interp(0.78); + assert!(approx_between(datum, &7.0, &8.0, 0.8)); + + let datum = even_core.sample_interp(0.5); + assert!(approx_between(datum, &4.0, &5.0, 1.0) || approx_between(datum, &5.0, &6.0, 0.0)); + let datum = even_core.sample_interp(0.7); + assert!(approx_between(datum, &6.0, &7.0, 1.0) || approx_between(datum, &7.0, &8.0, 0.0)); + } + + #[test] + fn uneven_sample_interp() { + let uneven_core = UnevenCore::::new(vec![ + (0.0, 0.0), + (1.0, 3.0), + (2.0, 9.0), + (4.0, 10.0), + (8.0, -5.0), + ]) + .expect("Failed to construct test core"); + + let datum = uneven_core.sample_interp(-1.0); + assert!(is_left_tail(datum)); + let datum = uneven_core.sample_interp(0.0); + assert!(is_exact(datum, &0.0)); + let datum = uneven_core.sample_interp(8.0); + assert!(is_exact(datum, &(-5.0))); + let datum = uneven_core.sample_interp(9.0); + assert!(is_right_tail(datum)); + + let datum = uneven_core.sample_interp(0.5); + assert!(approx_between(datum, &0.0, &3.0, 0.5)); + let datum = uneven_core.sample_interp(2.5); + assert!(approx_between(datum, &9.0, &10.0, 0.25)); + let datum = uneven_core.sample_interp(7.0); + assert!(approx_between(datum, &10.0, &(-5.0), 0.75)); + + let datum = uneven_core.sample_interp(2.0); + assert!(is_exact(datum, &9.0)); + let datum = uneven_core.sample_interp(4.0); + assert!(is_exact(datum, &10.0)); + } + + #[test] + fn chunked_uneven_sample_interp() { + let core = + ChunkedUnevenCore::new(vec![0.0, 2.0, 8.0], vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0], 2) + .expect("Failed to construct test core"); + + let datum = core.sample_interp(-1.0); + assert!(is_left_tail(datum)); + let datum = core.sample_interp(0.0); + assert!(is_exact(datum, &[0.0, 1.0])); + let datum = core.sample_interp(8.0); + assert!(is_exact(datum, &[4.0, 5.0])); + let datum = core.sample_interp(10.0); + assert!(is_right_tail(datum)); + + let datum = core.sample_interp(1.0); + assert!(approx_between(datum, &[0.0, 1.0], &[2.0, 3.0], 0.5)); + let datum = core.sample_interp(3.0); + assert!(approx_between(datum, &[2.0, 3.0], &[4.0, 5.0], 1.0 / 6.0)); + + let datum = core.sample_interp(2.0); + assert!(is_exact(datum, &[2.0, 3.0])); + } +} diff --git a/crates/bevy_math/src/curve/iterable.rs b/crates/bevy_math/src/curve/iterable.rs index 6b530e459a1ec..1dfe42240450d 100644 --- a/crates/bevy_math/src/curve/iterable.rs +++ b/crates/bevy_math/src/curve/iterable.rs @@ -13,7 +13,7 @@ pub trait IterableCurve { fn domain(&self) -> Interval; /// Sample this curve at a specified time `t`, producing an iterator over sampled values. - fn sample_iter<'a>(&self, t: f32) -> impl Iterator + fn sample_iter_unchecked<'a>(&self, t: f32) -> impl Iterator where Self: 'a; } @@ -26,7 +26,7 @@ where self.domain } - fn sample_iter<'a>(&self, _t: f32) -> impl Iterator + fn sample_iter_unchecked<'a>(&self, _t: f32) -> impl Iterator where Self: 'a, { From b23b8e88bb58815606ed05e72393def24dd3e861 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 19 Aug 2024 06:40:45 -0400 Subject: [PATCH 42/44] Add sampling variants for IterableCurve, use clamped sampling in animation --- crates/bevy_animation/src/lib.rs | 11 +++++------ crates/bevy_math/src/curve/iterable.rs | 25 ++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 9bcde33472777..f6ba362179cb7 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -758,8 +758,7 @@ pub fn animate_targets( } impl AnimationTargetContext<'_> { - /// Applies a clip to a single animation target according to the - /// [`AnimationTargetContext`]. + /// Applies a clip to a single animation target according to the [`AnimationTargetContext`]. fn apply(&mut self, curves: &[VariableCurve], weight: f32, seek_time: f32) { for curve in curves { match curve { @@ -767,21 +766,21 @@ impl AnimationTargetContext<'_> { if let Some(ref mut transform) = self.transform { transform.translation = transform .translation - .lerp(translation_curve.sample_unchecked(seek_time), weight); + .lerp(translation_curve.sample_clamped(seek_time), weight); } } VariableCurve::Rotation(rotation_curve) => { if let Some(ref mut transform) = self.transform { transform.rotation = transform .rotation - .slerp(rotation_curve.sample_unchecked(seek_time), weight); + .slerp(rotation_curve.sample_clamped(seek_time), weight); } } VariableCurve::Scale(scale_curve) => { if let Some(ref mut transform) = self.transform { transform.scale = transform .scale - .lerp(scale_curve.sample_unchecked(seek_time), weight); + .lerp(scale_curve.sample_clamped(seek_time), weight); } } VariableCurve::Weights(weights_curve) => { @@ -795,7 +794,7 @@ impl AnimationTargetContext<'_> { lerp_morph_weights( morphs.weights_mut(), - weights_curve.sample_iter_unchecked(seek_time), + weights_curve.sample_iter_clamped(seek_time), weight, ); } diff --git a/crates/bevy_math/src/curve/iterable.rs b/crates/bevy_math/src/curve/iterable.rs index 1dfe42240450d..b8bfc06719cc2 100644 --- a/crates/bevy_math/src/curve/iterable.rs +++ b/crates/bevy_math/src/curve/iterable.rs @@ -12,10 +12,33 @@ pub trait IterableCurve { /// The interval over which this curve is parametrized. fn domain(&self) -> Interval; - /// Sample this curve at a specified time `t`, producing an iterator over sampled values. + /// Sample a point on this curve at the parameter value `t`, producing an iterator over values. + /// This is the unchecked version of sampling, which should only be used if the sample time `t` + /// is already known to lie within the curve's domain. + /// + /// Values sampled from outside of a curve's domain are generally considered invalid; data which + /// is nonsensical or otherwise useless may be returned in such a circumstance, and extrapolation + /// beyond a curve's domain should not be relied upon. fn sample_iter_unchecked<'a>(&self, t: f32) -> impl Iterator where Self: 'a; + + /// Sample this curve at a specified time `t`, producing an iterator over sampled values. + /// The parameter `t` is clamped to the domain of the curve. + fn sample_iter_clamped<'a>(self: &'a Self, t: f32) -> impl Iterator { + let t_clamped = self.domain().clamp(t); + self.sample_iter_unchecked(t_clamped) + } + + /// Sample this curve at a specified time `t`, producing an iterator over sampled values. + /// If the parameter `t` does not lie in the curve's domain, `None` is returned. + fn sample_iter<'a>(self: &'a Self, t: f32) -> Option> { + if self.domain().contains(t) { + Some(self.sample_iter_unchecked(t)) + } else { + None + } + } } impl IterableCurve for ConstantCurve> From 1ed2925839a35205b707b9a0b8e7bfca8cff1f66 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 19 Aug 2024 06:59:33 -0400 Subject: [PATCH 43/44] Lint --- crates/bevy_math/src/curve/iterable.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_math/src/curve/iterable.rs b/crates/bevy_math/src/curve/iterable.rs index b8bfc06719cc2..6780aa38b8552 100644 --- a/crates/bevy_math/src/curve/iterable.rs +++ b/crates/bevy_math/src/curve/iterable.rs @@ -25,14 +25,14 @@ pub trait IterableCurve { /// Sample this curve at a specified time `t`, producing an iterator over sampled values. /// The parameter `t` is clamped to the domain of the curve. - fn sample_iter_clamped<'a>(self: &'a Self, t: f32) -> impl Iterator { + fn sample_iter_clamped(&self, t: f32) -> impl Iterator { let t_clamped = self.domain().clamp(t); self.sample_iter_unchecked(t_clamped) } /// Sample this curve at a specified time `t`, producing an iterator over sampled values. /// If the parameter `t` does not lie in the curve's domain, `None` is returned. - fn sample_iter<'a>(self: &'a Self, t: f32) -> Option> { + fn sample_iter(&self, t: f32) -> Option> { if self.domain().contains(t) { Some(self.sample_iter_unchecked(t)) } else { From 4e68dd0bdcda77deee6d2264f12982cf15b0726e Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 19 Aug 2024 11:10:06 -0400 Subject: [PATCH 44/44] Return to unchecked sampling for perf --- crates/bevy_animation/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index f6ba362179cb7..d68cd1e007e06 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -766,21 +766,21 @@ impl AnimationTargetContext<'_> { if let Some(ref mut transform) = self.transform { transform.translation = transform .translation - .lerp(translation_curve.sample_clamped(seek_time), weight); + .lerp(translation_curve.sample_unchecked(seek_time), weight); } } VariableCurve::Rotation(rotation_curve) => { if let Some(ref mut transform) = self.transform { transform.rotation = transform .rotation - .slerp(rotation_curve.sample_clamped(seek_time), weight); + .slerp(rotation_curve.sample_unchecked(seek_time), weight); } } VariableCurve::Scale(scale_curve) => { if let Some(ref mut transform) = self.transform { transform.scale = transform .scale - .lerp(scale_curve.sample_clamped(seek_time), weight); + .lerp(scale_curve.sample_unchecked(seek_time), weight); } } VariableCurve::Weights(weights_curve) => { @@ -794,7 +794,7 @@ impl AnimationTargetContext<'_> { lerp_morph_weights( morphs.weights_mut(), - weights_curve.sample_iter_clamped(seek_time), + weights_curve.sample_iter_unchecked(seek_time), weight, ); }