Skip to content

Commit

Permalink
segmented trajectory
Browse files Browse the repository at this point in the history
  • Loading branch information
Nertsal committed Oct 13, 2024
1 parent 05d1102 commit 2d50cc1
Show file tree
Hide file tree
Showing 8 changed files with 114 additions and 94 deletions.
63 changes: 18 additions & 45 deletions crates/ctl-core/src/interpolation/bezier.rs
Original file line number Diff line number Diff line change
@@ -1,65 +1,38 @@
use super::*;

/// A Bezier curve of arbitrary degree.
pub struct Bezier<const N: usize, T> {
segments: Vec<BezierSegment<N, T>>,
}

impl<const N: usize, T: 'static + Interpolatable> Bezier<N, T> {
pub fn new(points: &[T]) -> Self {
Self {
segments: points
.windows(N)
.enumerate()
.filter(|(i, _)| *i % (N - 1) == 0)
.map(|(_, window)| {
assert_eq!(window.len(), N);
let points = std::array::from_fn(|i| window[i].clone());
BezierSegment::new(points)
})
.collect(),
}
}

pub fn get(&self, interval: usize, t: Time) -> Option<T> {
let subinterval = interval % (N - 1);
let interval = interval / (N - 1);
let interval = self.segments.get(interval)?;
let t = (t.as_f32() + subinterval as f32) / (N - 1) as f32;
Some(interval.get(t))
}
}

/// A single Bezier segment of arbitrary degree.
/// BezierSegment<3> is a Bezier defined by 3 points, so a curve of the 2nd degree, or a quadratic Bezier.
pub struct BezierSegment<const N: usize, T> {
points: [T; N],
/// A single Bezier segment of arbitrary degree, defined by the given number of points.
pub struct Bezier<T> {
points: Vec<T>, // TODO: smallvec
uniform_transform: Box<dyn Fn(f32) -> f32>,
}

impl<const N: usize, T: 'static + Interpolatable> BezierSegment<N, T> {
pub fn new(points: [T; N]) -> Self {
let sample = {
let points = points.clone();
move |t: f32| sample(&points, t)
};
impl<T: 'static + Interpolatable> Bezier<T> {
pub fn new(points: &[T]) -> Self {
let sample = |t: f32| sample(points, t);
let uniform_transform = Box::new(calculate_uniform_transformation(&sample));
Self {
points,
points: points.to_vec(),
uniform_transform,
}
}

pub fn num_intervals(&self) -> usize {
self.points.len().saturating_sub(1)
}

/// Returns a smoothed point.
/// `t` is expected to be in range `0..=1`.
pub fn get(&self, t: f32) -> T {
pub fn get(&self, interval: usize, t: Time) -> Option<T> {
let degree = self.points.len().saturating_sub(1);
let t = (t.as_f32() + interval as f32) / degree as f32;

let t = (self.uniform_transform)(t);
sample(&self.points, t)
Some(sample(&self.points, t))
}
}

fn sample<const N: usize, T: Interpolatable>(points: &[T; N], t: f32) -> T {
let n = N - 1;
fn sample<T: Interpolatable>(points: &[T], t: f32) -> T {
let n = points.len();
(0..=n)
.map(|i| {
let p = points[i].clone();
Expand Down
50 changes: 36 additions & 14 deletions crates/ctl-core/src/interpolation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,51 @@ pub use self::{bezier::*, spline::*};

use super::*;

pub enum Interpolation<T> {
Linear(Vec<T>),
Spline(Spline<T>),
BezierQuadratic(Bezier<3, T>),
BezierCubic(Bezier<4, T>),
pub struct Interpolation<T> {
pub segments: Vec<InterpolationSegment<T>>,
}

impl<T: 'static + Interpolatable> Interpolation<T> {
pub fn linear(points: Vec<T>) -> Self {
Self::Linear(points)
pub fn get(&self, mut interval: usize, t: Time) -> Option<T> {
self.segments
.iter()
.find(|segment| {
let f = segment.num_intervals() > interval;
if !f {
interval -= segment.num_intervals();
}
f
})
.and_then(|segment| segment.get(interval, t))
}
}

pub enum InterpolationSegment<T> {
Linear(Vec<T>), // TODO: smallvec
Spline(Spline<T>),
Bezier(Bezier<T>),
}

pub fn spline(points: Vec<T>, tension: f32) -> Self {
impl<T: 'static + Interpolatable> InterpolationSegment<T> {
pub fn linear(points: &[T]) -> Self {
Self::Linear(points.to_vec())
}

pub fn spline(points: &[T], tension: f32) -> Self {
Self::Spline(Spline::new(points, tension))
}

pub fn bezier_quadratic(points: Vec<T>) -> Self {
Self::BezierQuadratic(Bezier::new(&points))
pub fn bezier(points: &[T]) -> Self {
Self::Bezier(Bezier::new(points))
}

pub fn bezier_cubic(points: Vec<T>) -> Self {
Self::BezierCubic(Bezier::new(&points))
/// The number of intervals within the curve segment.
pub fn num_intervals(&self) -> usize {
match self {
Self::Linear(points) => points.len().saturating_sub(1),
Self::Spline(spline) => spline.num_intervals(),
Self::Bezier(bezier) => bezier.num_intervals(),
}
}

/// Get an interpolated value on the given interval.
Expand All @@ -40,8 +63,7 @@ impl<T: 'static + Interpolatable> Interpolation<T> {
Some(a.clone().add(b.sub(a).scale(t.as_f32()))) // a + (b - a) * t
}
Self::Spline(i) => i.get(interval, t),
Self::BezierQuadratic(i) => i.get(interval, t),
Self::BezierCubic(i) => i.get(interval, t),
Self::Bezier(i) => i.get(interval, t),
}
}
}
Expand Down
15 changes: 8 additions & 7 deletions crates/ctl-core/src/interpolation/spline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use super::*;

/// Represents a [cardinal spline](https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Cardinal_spline).
pub struct Spline<T> {
intervals: Vec<Interval<T>>,
intervals: Vec<Interval<T>>, // TODO: smallvec
}

/// Represents a single interval of the spline.
Expand Down Expand Up @@ -33,10 +33,14 @@ impl<T: Interpolatable> Interval<T> {
}

impl<T: 'static + Interpolatable> Spline<T> {
pub fn num_intervals(&self) -> usize {
self.intervals.len()
}

/// Create a cardinal spline passing through points.
/// Tension should be in range `0..=1`.
/// For example, if tension is `0.5`, then the curve is a Catmull-Rom spline.
pub fn new(points: Vec<T>, tension: f32) -> Self {
pub fn new(points: &[T], tension: f32) -> Self {
let n = points.len();
let intervals = intervals(points, tension);
assert_eq!(n, intervals.len() + 1);
Expand All @@ -51,7 +55,7 @@ impl<T: 'static + Interpolatable> Spline<T> {
}
}

fn intervals<T: 'static + Interpolatable>(points: Vec<T>, tension: f32) -> Vec<Interval<T>> {
fn intervals<T: 'static + Interpolatable>(points: &[T], tension: f32) -> Vec<Interval<T>> {
// Calculate tangents
let len = points.len();
let mut tangents = Vec::with_capacity(len);
Expand Down Expand Up @@ -105,10 +109,7 @@ fn intervals<T: 'static + Interpolatable>(points: Vec<T>, tension: f32) -> Vec<I
let m0 = prev;
let m1 = next.clone();

let sample = {
let (p0, p1, m0, m1) = (p0.clone(), p1.clone(), m0.clone(), m1.clone());
move |t| sample(t, p0.clone(), p1.clone(), m0.clone(), m1.clone())
};
let sample = |t| sample(t, p0.clone(), p1.clone(), m0.clone(), m1.clone());
let uniform_transformation = Box::new(calculate_uniform_transformation(&sample));

intervals.push(Interval {
Expand Down
2 changes: 1 addition & 1 deletion crates/ctl-core/src/interpolation/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use super::*;
/// `sample` function to achieve uniform speed when interpolating.
pub fn calculate_uniform_transformation<T: Interpolatable>(
sample: &impl Fn(f32) -> T,
) -> impl Fn(f32) -> f32 {
) -> impl Fn(f32) -> f32 + 'static {
const RESOLUTION: usize = 100;
let step = (RESOLUTION as f32).recip();

Expand Down
2 changes: 1 addition & 1 deletion crates/ctl-core/src/legacy/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,6 @@ impl From<Level> for crate::Level {
fade_in: light.light.movement.fade_in,
fade_out: light.light.movement.fade_out,
initial: light.light.movement.initial.into(),
interpolation: crate::TrajectoryInterpolation::default(),
key_frames: light
.light
.movement
Expand Down Expand Up @@ -243,6 +242,7 @@ impl From<MoveFrame> for crate::MoveFrame {
Self {
lerp_time: value.lerp_time,
interpolation: crate::MoveInterpolation::default(),
change_curve: None, // Linear
transform: value.transform.into(),
}
}
Expand Down
69 changes: 46 additions & 23 deletions crates/ctl-core/src/model/movement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ pub struct Movement {
/// Time (in beats) to spend fading out of the last keyframe.
pub fade_out: Time,
pub initial: Transform,
pub interpolation: TrajectoryInterpolation,
pub key_frames: VecDeque<MoveFrame>,
}

Expand All @@ -17,6 +16,10 @@ pub struct MoveFrame {
pub lerp_time: Time,
/// Interpolation to use when moving towards this frame.
pub interpolation: MoveInterpolation,
/// Whether to start a new curve starting from this frame.
/// Is set to `None`, the curve will either continue the previous type,
/// or continue as linear in the case of bezier.
pub change_curve: Option<TrajectoryInterpolation>,
pub transform: Transform,
}

Expand Down Expand Up @@ -52,22 +55,8 @@ pub enum TrajectoryInterpolation {
Linear,
/// Connects keyframes via a smooth cubic Cardinal spline.
Spline { tension: R32 },
/// Connects keyframes via a quadratic Bezier curve, using every other keyframe as an intermediate control point.
BezierQuadratic,
/// Connects keyframes via a quadratic Bezier curve, using every third keyframe as an endpoint.
BezierCubic,
}

impl TrajectoryInterpolation {
/// Bakes the interpolation path based on the keypoints.
pub fn bake<T: 'static + Interpolatable>(&self, points: Vec<T>) -> Interpolation<T> {
match self {
Self::Linear => Interpolation::linear(points),
Self::Spline { tension } => Interpolation::spline(points, tension.as_f32()),
Self::BezierQuadratic => Interpolation::bezier_quadratic(points),
Self::BezierCubic => Interpolation::bezier_cubic(points),
}
}
/// Connects keyframes via a Bezier curve.
Bezier,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
Expand Down Expand Up @@ -137,6 +126,7 @@ impl MoveFrame {
Self {
lerp_time: lerp_time.as_r32(),
interpolation: MoveInterpolation::default(),
change_curve: None,
transform: Transform::scale(scale),
}
}
Expand Down Expand Up @@ -179,7 +169,6 @@ impl Default for Movement {
fade_in: r32(1.0),
fade_out: r32(1.0),
initial: Transform::default(),
interpolation: TrajectoryInterpolation::default(),
key_frames: VecDeque::new(),
}
}
Expand Down Expand Up @@ -266,11 +255,7 @@ impl Movement {
time -= self.fade_in;

// TODO: bake only once before starting the level, then cache
let interpolation = self.interpolation.bake(
self.timed_positions()
.map(|(_, transform, _)| transform)
.collect(),
);
let interpolation = self.bake();

// Find the target frame
for (i, frame) in self.frames_iter().enumerate() {
Expand Down Expand Up @@ -321,6 +306,44 @@ impl Movement {
pub fn change_fade_in(&mut self, target: Time) {
self.fade_in = target.clamp(r32(0.25), r32(25.0));
}

/// Bakes the interpolation path based on the keypoints.
pub fn bake(&self) -> Interpolation<Transform> {
let points = std::iter::once((self.initial, None)).chain(
self.frames_iter()
.map(|frame| (frame.transform, frame.change_curve)),
);

let mk_segment = |curve, segment: &[_]| match curve {
TrajectoryInterpolation::Linear => InterpolationSegment::linear(segment),
TrajectoryInterpolation::Spline { tension } => {
InterpolationSegment::spline(segment, tension.as_f32())
}
TrajectoryInterpolation::Bezier => InterpolationSegment::bezier(segment),
};

let mut segments = vec![];
let mut current_curve = TrajectoryInterpolation::Linear;
let mut current_segment = vec![]; // TODO: smallvec
for (point, curve) in points {
match curve {
None => current_segment.push(point),
Some(new_curve) => {
if !current_segment.is_empty() {
segments.push(mk_segment(current_curve, &current_segment));
}
current_segment = vec![point];
current_curve = new_curve;
}
}
}

if !current_segment.is_empty() {
segments.push(mk_segment(current_curve, &current_segment));
}

Interpolation { segments }
}
}

fn smoothstep<T: Float>(t: T) -> T {
Expand Down
6 changes: 4 additions & 2 deletions src/editor/action/level.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,8 @@ impl LevelEditor {
self.current_beat - light.light.movement.fade_in - light.telegraph.precede_time;
light.light.movement.key_frames.push_front(MoveFrame {
lerp_time: event.beat - time,
interpolation: MoveInterpolation::Linear, // TODO: interpolation customize
interpolation: MoveInterpolation::default(), // TODO: use the same as other waypoints
change_curve: None,
transform,
});
event.beat = time;
Expand All @@ -385,7 +386,8 @@ impl LevelEditor {
i,
MoveFrame {
lerp_time,
interpolation: MoveInterpolation::Linear, // TODO: interpolation customize
interpolation: MoveInterpolation::default(), // TODO: use the same as other waypoints
change_curve: None,
transform,
},
);
Expand Down
1 change: 0 additions & 1 deletion src/model/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ impl HoverButton {
animation: Movement {
fade_in: r32(0.0),
initial: Transform::scale(2.25),
interpolation: TrajectoryInterpolation::Linear,
key_frames: vec![MoveFrame::scale(0.5, 5.0), MoveFrame::scale(0.25, 75.0)].into(),
fade_out: r32(0.2),
},
Expand Down

0 comments on commit 2d50cc1

Please sign in to comment.