Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Math for Curves and Interpolations (Lerp and Callmul-Rom) #1837

Closed
wants to merge 52 commits into from
Closed
Show file tree
Hide file tree
Changes from 51 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
3c3eaca
curves and lerp
lassade Apr 6, 2021
5414266
Merge branch 'main' into curves-and-lerp
lassade Apr 6, 2021
acbab8a
docs
lassade Apr 6, 2021
e18b222
lerp unclamped, lerp handle, fix clippy errors
lassade Apr 7, 2021
d3757a4
redesign of `CurveVariable`, fix wording
lassade Apr 13, 2021
2a02a12
update auto tangents
lassade Apr 14, 2021
81a2c08
curve variable example
lassade Apr 15, 2021
9e8b537
fix smooth interpolation
lassade Apr 15, 2021
2d1864c
curve editing functions
lassade Apr 16, 2021
bf417aa
curve variable getters
lassade Apr 17, 2021
5593700
error handling
lassade Apr 17, 2021
6857f54
fix clippy error
lassade Apr 17, 2021
e140a21
Merge branch 'main' into curves-and-lerp
lassade Apr 17, 2021
b36cd7d
fix tests
lassade Apr 17, 2021
281a912
force ci to rerun
lassade Apr 17, 2021
00a6f57
curve variable bench
lassade Apr 17, 2021
d7306a1
soa is better for random sampling
lassade Apr 17, 2021
f7a0bb4
remove unused variables
lassade Apr 17, 2021
d564c53
fix set_time and auto tangent
lassade Apr 18, 2021
69587dc
fmt + clippy
lassade Apr 18, 2021
8953ef7
renamed curve algo
lassade May 2, 2021
ab7f8ac
fix step function and cmp interpolation enum
lassade May 8, 2021
d5ad840
fix set free tangents and comments
lassade May 9, 2021
88347b5
Merge branch 'main' of github.com:bevyengine/bevy into curves-and-lerp
lassade May 9, 2021
63aca45
add thiserror crate
lassade May 9, 2021
fbec101
fix samples
lassade May 9, 2021
dc4d43b
fix error typo
lassade May 9, 2021
960aff4
idiomatic Option<T> lerp impl
lassade May 9, 2021
07c11d8
fix build errors
lassade May 9, 2021
df986e3
must use keyframe insert builder
lassade May 9, 2021
6693aaf
fix keyframe insertion
lassade May 9, 2021
958feb5
Update crates/bevy_math/src/curves/variable.rs
lassade Jul 8, 2021
4a3ed85
Update crates/bevy_math/src/curves/variable.rs
lassade Jul 8, 2021
6bfd5b6
Update crates/bevy_math/src/interpolation/utils.rs
lassade Jul 8, 2021
bfc7a16
comments
lassade Jul 8, 2021
e6e3faa
use step function
lassade Jul 8, 2021
1573711
rename `inv_sqrt` to `fast_inv_sqrt`
lassade Jul 8, 2021
f57c146
derive Clone
lassade Jul 8, 2021
6ade8f8
comments
lassade Jul 8, 2021
d35b739
fix offset confuse terminology
lassade Jul 8, 2021
9d77d76
forget to finish the sentence
lassade Jul 8, 2021
48b0740
Merge branch 'main' of github.com:bevyengine/bevy into curves-and-lerp
lassade Jul 8, 2021
0b5cd21
fix Quat conversion errors
lassade Jul 8, 2021
56b0eff
renamed CurveCursor to KeyframeIndex
lassade Jul 8, 2021
2691259
curve cursor comments
lassade Jul 8, 2021
fed5db3
docs and more intuitive api
lassade Jul 10, 2021
67cb019
added useful api functions, docs, comments and resampling function
lassade Jul 10, 2021
5820d76
better naming conventions for interpolation code
lassade Jul 11, 2021
9663acd
explicity inlining
lassade Jul 11, 2021
0331ce0
docs and api
lassade Jul 11, 2021
a45428a
fix panic docs
lassade Jul 11, 2021
9a4f7a4
approx rsqrt
lassade Jul 15, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,11 @@ crate-type = ["cdylib"]
name = "android"
path = "examples/android/android.rs"

# Animation
[[example]]
name = "smooth_curves"
path = "examples/animations/smooth_curves.rs"

[package.metadata.android]
apk_label = "Bevy Example"
assets = "assets"
Expand Down
6 changes: 6 additions & 0 deletions benches/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ edition = "2018"

[dev-dependencies]
criterion = "0.3"
rand = "0.8.3"
bevy = { path = "../" }

[[bench]]
Expand All @@ -25,3 +26,8 @@ harness = false
name = "iter"
path = "benches/bevy_tasks/iter.rs"
harness = false

[[bench]]
name = "curves"
path = "benches/bevy_math/curves.rs"
harness = false
58 changes: 58 additions & 0 deletions benches/benches/bevy_math/curves.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use bevy::math::{
curves::{Curve, KeyframeIndex, CurveVariable},
Vec4,
};
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use rand::prelude::*;

criterion_group!(benches, curve_variable);
criterion_main!(benches);

const SAMPLES_COUNT: usize = 100;

fn curve_sampling<T>(samples: &[f32], curve: &impl Curve<Output = T>) {
let mut c: KeyframeIndex = 0;
for t in samples {
let (nc, v) = curve.sample_with_cursor(c, *t);
black_box(v);
c = nc;
}
}

fn curve_variable(criterion: &mut Criterion) {
let mut group = criterion.benchmark_group("curve_variable");
group.warm_up_time(std::time::Duration::from_millis(500));
group.measurement_time(std::time::Duration::from_secs(3));

let curve = CurveVariable::with_auto_tangents(
vec![0.0, 1.0, 1.3, 1.6, 1.7, 1.8, 1.9, 2.0],
vec![3.0, 0.0, 1.0, 0.0, 0.5, 0.0, 0.25, 0.0]
.iter()
.map(|x| Vec4::splat(*x))
.collect::<Vec<_>>(),
)
.unwrap();

let duration = curve.duration();
let mut rand = rand::thread_rng();

let rand_samples = (0..SAMPLES_COUNT)
.into_iter()
.map(|_| duration * rand.gen::<f32>())
.collect::<Vec<_>>();

let samples = (0..SAMPLES_COUNT)
.into_iter()
.map(|i| duration * (i as f32) / (SAMPLES_COUNT - 1) as f32)
.collect::<Vec<_>>();

group.bench_function("random_sampling", |bencher| {
bencher.iter(|| black_box(curve_sampling(&rand_samples[..], &curve)));
});

group.bench_function("sampling", |bencher| {
bencher.iter(|| black_box(curve_sampling(&samples[..], &curve)));
});

group.finish()
}
1 change: 1 addition & 0 deletions crates/bevy_asset/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ bevy_log = { path = "../bevy_log", version = "0.5.0" }
bevy_reflect = { path = "../bevy_reflect", version = "0.5.0", features = ["bevy"] }
bevy_tasks = { path = "../bevy_tasks", version = "0.5.0" }
bevy_utils = { path = "../bevy_utils", version = "0.5.0" }
bevy_math = { path = "../bevy_math", version = "0.5.0" }

# other
serde = { version = "1", features = ["derive"] }
Expand Down
57 changes: 57 additions & 0 deletions crates/bevy_asset/src/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::{
Asset, Assets,
};
use bevy_ecs::reflect::ReflectComponent;
use bevy_math::interpolation::{utils::step_unclamped, Interpolate, Lerp, TangentIgnore};
use bevy_reflect::{Reflect, ReflectDeserialize};
use bevy_utils::Uuid;
use crossbeam_channel::{Receiver, Sender};
Expand Down Expand Up @@ -230,6 +231,34 @@ impl<T: Asset> Clone for Handle<T> {
}
}

impl<T: Asset + 'static> Lerp for Handle<T> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit nervous about chucking Lerp and related methods onto all handles to assets. What if we end up using the asset system for audio? Or dialog?

Perhaps an extra trait + trait bound makes sense here, to limit this to assets with a transform.

Copy link
Contributor Author

@lassade lassade Jul 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle fields can only be swapped, because lerp can't create a new asset handle to return, that's by desing.

What you are describing is done via impl<T> Lerp for T not by impl<T> Lerp for Handle<T>, even still I think lerping should only be done in copy types;

A new PR could add a trait that describes fn lerp_into(&a, &b, t, &mut target)

#[inline(always)]
fn lerp_unclamped(a: &Self, b: &Self, t: f32) -> Self {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to see more descriptive variable names here. I can piece together a = "starting_position", b = "end_position", t = "time_step" from context but that will be harder for beginners and tough to read.

Copy link
Contributor Author

@lassade lassade Jul 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from a to b is kind "standard" in the lerp functions that I saw in the wild, maybe docs should solve the issue?

alternatively I could agree with v0 to v1 meaning value at 0 and 1, what do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with a and b if they're standard, so long as they're documented in a way that's attached to the function :)

step_unclamped(a, b, t)
}
}

impl<T: Asset + 'static> Interpolate for Handle<T> {
type Tangent = TangentIgnore;
const FLAT_TANGENT: Self::Tangent = TangentIgnore;

fn interpolate_unclamped(
k0: &Self,
_: &Self::Tangent,
k1: &Self,
_: &Self::Tangent,
_: bevy_math::interpolation::Interpolation,
t: f32,
_: f32,
) -> Self {
step_unclamped(k0, k1, t)
}

fn auto_tangent(_: f32, _: f32, _: f32, _: &Self, _: &Self, _: &Self) -> Self::Tangent {
TangentIgnore
}
}

/// A non-generic version of [Handle]
///
/// This allows handles to be mingled in a cross asset context. For example, storing `Handle<A>` and
Expand Down Expand Up @@ -337,6 +366,34 @@ impl Clone for HandleUntyped {
}
}

impl Lerp for HandleUntyped {
#[inline(always)]
fn lerp_unclamped(a: &Self, b: &Self, t: f32) -> Self {
step_unclamped(a, b, t)
}
}

impl Interpolate for HandleUntyped {
type Tangent = TangentIgnore;
const FLAT_TANGENT: Self::Tangent = TangentIgnore;

fn interpolate_unclamped(
k0: &Self,
_: &Self::Tangent,
k1: &Self,
_: &Self::Tangent,
_: bevy_math::interpolation::Interpolation,
t: f32,
_: f32,
) -> Self {
step_unclamped(k0, k1, t)
}

fn auto_tangent(_: f32, _: f32, _: f32, _: &Self, _: &Self, _: &Self) -> Self::Tangent {
TangentIgnore
}
}

pub(crate) enum RefChange {
Increment(HandleId),
Decrement(HandleId),
Expand Down
35 changes: 18 additions & 17 deletions crates/bevy_math/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
[package]
name = "bevy_math"
version = "0.5.0"
edition = "2018"
authors = [
"Bevy Contributors <[email protected]>",
"Carter Anderson <[email protected]>",
]
description = "Provides math functionality for Bevy Engine"
homepage = "https://bevyengine.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT"
keywords = ["bevy"]

[dependencies]
glam = { version = "0.15.1", features = ["serde", "bytemuck"] }
bevy_reflect = { path = "../bevy_reflect", version = "0.5.0", features = ["bevy"] }
[package]
name = "bevy_math"
version = "0.5.0"
edition = "2018"
authors = [
"Bevy Contributors <[email protected]>",
"Carter Anderson <[email protected]>",
]
description = "Provides math functionality for Bevy Engine"
homepage = "https://bevyengine.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT"
keywords = ["bevy"]

[dependencies]
glam = { version = "0.15.1", features = ["serde", "bytemuck"] }
bevy_reflect = { path = "../bevy_reflect", version = "0.5.0", features = ["bevy"] }
thiserror = "1.0"
126 changes: 126 additions & 0 deletions crates/bevy_math/src/curves/fixed.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
use crate::{
curves::{Curve, KeyframeIndex},
interpolation::Lerp,
};

// TODO: impl Serialize, Deserialize
/// Curve with evenly spaced keyframes, in another words a curve with a fixed frame rate.
///
/// This curve maintains the faster sampling rate over a wide range of frame rates, because
/// it doesn't rely on keyframe cursor. As a downside, it will have a bigger memory foot print.
#[derive(Default, Debug, Clone)]
pub struct CurveFixed<T: Lerp + Clone> {
/// Frames per second
frame_rate: f32,
lassade marked this conversation as resolved.
Show resolved Hide resolved
/// Negative number of frames before the curve starts, it's stored
/// in a `f32` to avoid castings in the when sampling the curve and also
/// negated to use [`std::f32::mul_add`]
negative_frame_offset: f32,
pub keyframes: Vec<T>,
}

impl<T: Lerp + Clone> CurveFixed<T> {
pub fn from_keyframes(frame_rate: f32, frame_offset: i32, keyframes: Vec<T>) -> Self {
Self {
frame_rate,
negative_frame_offset: -(frame_offset as f32),
keyframes,
}
}

pub fn from_constant(v: T) -> Self {
Self {
frame_rate: 30.0,
negative_frame_offset: 0.0,
keyframes: vec![v],
}
}

#[inline]
pub fn frame_rate(&self) -> f32 {
self.frame_rate
}

#[inline]
pub fn set_frame_rate(&mut self, frame_rate: f32) {
self.frame_rate = frame_rate;
}

/// Sets the start keyframe index.
///
/// Adds a starting delay in multiples of the frame duration `(1 / frame_rate)`
#[inline]
pub fn set_frame_offset(&mut self, offset: i32) {
self.negative_frame_offset = -offset as f32;
}

/// Number of the start keyframe
#[inline]
pub fn frame_offset(&self) -> i32 {
-self.negative_frame_offset as i32
}

/// `true` when this `CurveFixed` doesn't have any keyframe
#[inline]
pub fn is_empty(&self) -> bool {
self.len() == 0
}

#[inline]
pub fn iter(&self) -> impl Iterator<Item = &T> {
self.keyframes.iter()
}

#[inline]
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
self.keyframes.iter_mut()
}
}

impl<T: Lerp + Clone> Curve for CurveFixed<T> {
type Output = T;

fn duration(&self) -> f32 {
((self.len() as f32 - 1.0 - self.negative_frame_offset) / self.frame_rate).max(0.0)
}

#[inline]
fn time_offset(&self) -> f32 {
-self.negative_frame_offset / self.frame_rate
}

#[inline]
fn len(&self) -> usize {
self.keyframes.len()
}

fn sample(&self, time: f32) -> Self::Output {
lassade marked this conversation as resolved.
Show resolved Hide resolved
// Make sure to have at least one sample
assert!(!self.keyframes.is_empty(), "track is empty");

let t = time.mul_add(self.frame_rate, self.negative_frame_offset);
if t.is_sign_negative() {
// Underflow clamp
return self.keyframes[0].clone();
}

let f = t.trunc();
let t = t - f;

let f = f as usize;
let f_n = self.len() - 1;
if f >= f_n {
// Overflow clamp
return self.keyframes[f_n].clone();
}

// Lerp the value
T::lerp_unclamped(&self.keyframes[f], &self.keyframes[f + 1], t)
}

/// Same as the [`sample`] function
#[inline]
fn sample_with_cursor(&self, _: KeyframeIndex, time: f32) -> (KeyframeIndex, Self::Output) {
(0, self.sample(time))
}
}
Loading