-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
Changes from 51 commits
3c3eaca
5414266
acbab8a
e18b222
d3757a4
2a02a12
81a2c08
9e8b537
2d1864c
bf417aa
5593700
6857f54
e140a21
b36cd7d
281a912
00a6f57
d7306a1
f7a0bb4
d564c53
69587dc
8953ef7
ab7f8ac
d5ad840
88347b5
63aca45
fbec101
dc4d43b
960aff4
07c11d8
df986e3
6693aaf
958feb5
4a3ed85
6bfd5b6
bfc7a16
e6e3faa
1573711
f57c146
6ade8f8
d35b739
9d77d76
48b0740
0b5cd21
56b0eff
2691259
fed5db3
67cb019
5820d76
9663acd
0331ce0
a45428a
9a4f7a4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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}; | ||
|
@@ -230,6 +231,34 @@ impl<T: Asset> Clone for Handle<T> { | |
} | ||
} | ||
|
||
impl<T: Asset + 'static> Lerp for Handle<T> { | ||
#[inline(always)] | ||
fn lerp_unclamped(a: &Self, b: &Self, t: f32) -> Self { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. from alternatively I could agree with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm fine with |
||
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 | ||
|
@@ -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), | ||
|
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" |
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)) | ||
} | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 byimpl<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)