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

Smooth Damp #2001

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8b8ad09
smooth damp for f32, f64, vec2, vec3
msklywenn Apr 24, 2021
a79e3cc
format
msklywenn Apr 24, 2021
c387dba
Merge branch 'main' of https://github.com/bevyengine/bevy
msklywenn Apr 24, 2021
3b31005
smooth_time comment
msklywenn Apr 26, 2021
565f556
smooth_time comment
msklywenn Apr 26, 2021
16ba211
smooth damp with max speed
msklywenn Apr 26, 2021
2f3b0cb
format
msklywenn Apr 26, 2021
830fef9
use clamp_length_max
msklywenn Apr 26, 2021
1485281
ensure max speed validity to avoid any divide by zero
msklywenn Apr 26, 2021
b74ae4f
removed needless casts
msklywenn Apr 26, 2021
f685e66
removed unnecessary variable
msklywenn Apr 26, 2021
a060a0d
macros to avoid code duplication
msklywenn Apr 26, 2021
e54efb3
export smooth damp max in prelude
msklywenn Apr 26, 2021
5755ffd
format
msklywenn Apr 26, 2021
ece1667
more macros for even less code duplication
msklywenn Apr 26, 2021
19ed7e4
fixed smooth_damp_max and simplified its macro
msklywenn Apr 27, 2021
2b152b2
Merge branch 'main' of https://github.com/bevyengine/bevy
msklywenn Apr 27, 2021
2a7211f
Merge branch 'main' of https://github.com/msklywenn/bevy
msklywenn Apr 27, 2021
745b368
use faster exponential approximation
msklywenn Apr 27, 2021
687e47a
added doc example
msklywenn Apr 27, 2021
24b0b2f
format...
msklywenn Apr 27, 2021
3397f26
properly hide example boilerplate
msklywenn Apr 27, 2021
4331def
make example parameters public
msklywenn Apr 27, 2021
e4d42ad
replaced maxs with asserts on inputs
msklywenn Apr 27, 2021
71ab208
document panics + SmoothDampMax example
msklywenn Apr 27, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 4 additions & 2 deletions crates/bevy_math/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
mod face_toward;
mod geometry;
mod smooth;

pub use face_toward::*;
pub use geometry::*;
pub use glam::*;
pub use smooth::*;

pub mod prelude {
pub use crate::{
BVec2, BVec3, BVec4, FaceToward, IVec2, IVec3, IVec4, Mat3, Mat4, Quat, Rect, Size, UVec2,
UVec3, UVec4, Vec2, Vec3, Vec4,
BVec2, BVec3, BVec4, FaceToward, IVec2, IVec3, IVec4, Mat3, Mat4, Quat, Rect, Size,
SmoothDamp, SmoothDampMax, UVec2, UVec3, UVec4, Vec2, Vec3, Vec4,
};
}
207 changes: 207 additions & 0 deletions crates/bevy_math/src/smooth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
use crate::{Vec2, Vec3};

/// Smooths value to a goal using a damped spring.
pub trait SmoothDamp {
/// Smooths value to a goal using a damped spring.
///
/// `smooth_time` is the expected time to reach the target when at maximum velocity.
///
/// Returns smoothed value and new velocity.
///
/// # Panics
/// Panics if `smooth_time <= 0.0`.
///
/// # Example
/// ```
/// # use bevy_math::prelude::{Vec3, Quat};
/// # use bevy_math::SmoothDamp;
/// # struct Transform {
/// # translation: Vec3,
/// # rotation: Quat,
/// # scale: Vec3
/// # }
/// struct SmoothTransform {
/// pub smoothness: f32,
/// pub target: Vec3,
/// velocity: Vec3
/// }
///
/// fn smooth_transform_update(dt: f32, transform: &mut Transform, smoother: &mut SmoothTransform) {
/// let (p, v) = Vec3::smooth_damp(
/// transform.translation,
/// smoother.target,
/// smoother.velocity,
/// smoother.smoothness,
/// dt,
/// );
/// transform.translation = p;
/// smoother.velocity = v;
/// // When destructured assignement will be supported by Rust:
/// // (transform.translation, smoother.velocity) =
/// // Vec3::smooth_damp(
/// // transform.translation,
/// // smoother.target,
/// // smoother.velocity,
/// // smoother.smoothness,
/// // dt,
/// // );
/// }
/// ```
fn smooth_damp(
from: Self,
to: Self,
velocity: Self,
smooth_time: f32,
delta_time: f32,
) -> (Self, Self)
Copy link
Contributor

Choose a reason for hiding this comment

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

IMO returning structs > return tuples.
Having a struct makes it much clearer what each value is and prevent bugs where someone accidentally switches .0 and .1.

So having this return something like

pub struct SmoothDampResult<T> {
    smoothed: T,
    velocity: T,
}

would be much clearer and prevent some user-end bugs.

Copy link
Contributor Author

@msklywenn msklywenn Apr 27, 2021

Choose a reason for hiding this comment

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

At the moment, rust doesn't support tuple destructuring assignment (rust-lang/rust#71126).

But in the future, I expect users to call the function this way: (obj.pos, obj.vel) = smooth_damp(obj.pos, target, obj.vel, 0.1, dt); That's the only reason I wrote it this way.

If I were to use a SmoothDamp structure, I would use it as input too, but that's less friendly. It would prevent smoothing the translation of a Transform while keeping the velocity in another component, for example.

The last option would be to make the velocity input &mut, like C/C++/C# version do, but I fear needless lifetime issues and I like the function being pure...

Copy link
Contributor

Choose a reason for hiding this comment

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

Input parameters via &mut are not a good option IMO.

I would still argue for the struct return value since you can do something like this:

        let SmoothDampResult { smoothed, velocity } = Vec3::smooth_damp(
            player_move.smoothed_input,
            raw_input,
            player_move.smoothed_input_velocity,
            0.15,
            time.delta_seconds(),
        );
        player_move.smoothed_input = smoothed;
        player_move.smoothed_input_velocity = velocity ;

But I would leave this decision up to @cart 😄

where
Self: Sized;
}

macro_rules! impl_smooth_damp {
($t:ty, $f:ty) => {
impl SmoothDamp for $t {
fn smooth_damp(
from: $t,
to: $t,
velocity: $t,
smooth_time: f32,
delta_time: f32,
) -> ($t, $t) {
assert!(smooth_time > 0.0);
let smooth_time = smooth_time as $f;

let delta_time = delta_time as $f;
msklywenn marked this conversation as resolved.
Show resolved Hide resolved

// from game programming gems 4, chapter 1.10
let omega = 2.0 / smooth_time;
let x = omega * delta_time;

// fast and good enough approximation of exp(x)
let exp = 1.0 / (1.0 + x * (1.0 + x * (0.48 + 0.235 * x)));

let change = from - to;
let temp = (velocity + omega * change) * delta_time;

(
to + (change + temp) * exp, // position
(velocity - omega * temp) * exp, // velocity
)
}
}
};
}

impl_smooth_damp! {f32, f32}
impl_smooth_damp! {f64, f64}
impl_smooth_damp! {Vec2, f32}
impl_smooth_damp! {Vec3, f32}

/// Smooths value to a goal using a damped spring limited by a maximum speed.
pub trait SmoothDampMax {
/// Smooths value to a goal using a damped spring limited by a maximum speed.
///
/// `smooth_time` is the expected time to reach the target when at maximum velocity.
///
/// Returns smoothed value and new velocity.
///
/// # Panics
/// Panics if `smooth_time <= 0.0` or `max_speed <= 0.0`.
///
/// # Example
/// ```
/// # use bevy_math::prelude::{Vec3, Quat};
/// # use bevy_math::SmoothDampMax;
/// # struct Transform {
/// # translation: Vec3,
/// # rotation: Quat,
/// # scale: Vec3
/// # }
/// struct SmoothTransform {
/// pub smoothness: f32,
/// pub max_speed: f32,
/// pub target: Vec3,
/// velocity: Vec3
/// }
///
/// fn smooth_transform_update(dt: f32, transform: &mut Transform, smoother: &mut SmoothTransform) {
/// let (p, v) = Vec3::smooth_damp_max(
/// transform.translation,
/// smoother.target,
/// smoother.velocity,
/// smoother.max_speed,
/// smoother.smoothness,
/// dt,
/// );
/// transform.translation = p;
/// smoother.velocity = v;
/// // When destructured assignement will be supported by Rust:
/// // (transform.translation, smoother.velocity) =
/// // Vec3::smooth_damp_max(
/// // transform.translation,
/// // smoother.target,
/// // smoother.velocity,
/// // smoother.max_speed,
/// // smoother.smoothness,
/// // dt,
/// // );
/// }
/// ```
fn smooth_damp_max(
from: Self,
to: Self,
velocity: Self,
max_speed: f32,
smooth_time: f32,
delta_time: f32,
) -> (Self, Self)
where
Self: Sized;
}

macro_rules! impl_smooth_damp_max {
($t:ty, $f:ty, $clamp:expr) => {
impl SmoothDampMax for $t {
fn smooth_damp_max(
from: $t,
to: $t,
velocity: $t,
max_speed: f32,
smooth_time: f32,
delta_time: f32,
) -> ($t, $t) {
assert!(max_speed > 0.0);
let max_speed = max_speed as $f;

assert!(smooth_time > 0.0);
let smooth_time = smooth_time as $f;

let delta_time = delta_time as $f;
msklywenn marked this conversation as resolved.
Show resolved Hide resolved

// from game programming gems 4, chapter 1.10
let omega = 2.0 / smooth_time;
let x = omega * delta_time;

// fast and good enough approximation of exp(x)
let exp = 1.0 / (1.0 + x * (1.0 + x * (0.48 + 0.235 * x)));

let max = max_speed * smooth_time;
let change = from - to;
let change = $clamp(change, max);
let to = from - change;

let temp = (velocity + omega * change) * delta_time;

(
to + (change + temp) * exp, // position
(velocity - omega * temp) * exp, // velocity
)
}
}
};
}

impl_smooth_damp_max! {f32, f32, |change, max:f32| { f32::clamp(change, -max, max) }}
impl_smooth_damp_max! {f64, f64, |change, max:f64| { f64::clamp(change, -max, max) }}
impl_smooth_damp_max! {Vec2, f32, |change:Vec2, max| { change.clamp_length_max(max) }}
impl_smooth_damp_max! {Vec3, f32, |change:Vec3, max| { change.clamp_length_max(max) }}
Copy link
Contributor

Choose a reason for hiding this comment

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

You should probably write some test cases in a test module #[cfg(test)].
While I believe your implementation is correct, we should really start having tests for new features we add to bevy.

Copy link
Contributor Author

@msklywenn msklywenn Apr 27, 2021

Choose a reason for hiding this comment

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

Yup. Usually math functions are great candidates for tests. However, I've been scratching my head since the beginning and I have no idea of a good test for this. The function is usually used iteratively... Any suggestions?