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" functions for primitives and math structs #13408

Open
vveisard opened this issue May 17, 2024 · 6 comments
Open

"smooth_damp" functions for primitives and math structs #13408

vveisard opened this issue May 17, 2024 · 6 comments
Labels
A-Animation Make things move and change over time A-Math Fundamental domain-agnostic mathematical operations C-Enhancement A new feature S-Needs-Design This issue requires design work to think about how it would best be accomplished

Comments

@vveisard
Copy link

What problem does this solve or what need does it fill?

I need to smoothly move a value towards a target value over time using velocity and other parameters (see below).

What solution would you like?

I would like a set of "smooth_damp" functions for primitives and which have:

  • current
  • target
  • velocity
  • smooth time
  • max value delta
  • time delta

Importantly, I would also like a spherical smooth_damp for Vec2 and Vec3.

What alternative(s) have you considered?

A similar effect can be achieved using interpolation functions while updating the start value. However, this lacks all the parameters of a smooth_damp function.

Additional context

https://docs.unity3d.com/ScriptReference/Vector3.SmoothDamp.html

@vveisard vveisard added C-Enhancement A new feature S-Needs-Triage This issue needs to be labelled labels May 17, 2024
@alice-i-cecile alice-i-cecile added A-Math Fundamental domain-agnostic mathematical operations A-Animation Make things move and change over time S-Needs-Design This issue requires design work to think about how it would best be accomplished and removed S-Needs-Triage This issue needs to be labelled labels May 18, 2024
@alice-i-cecile
Copy link
Member

I definitely want this: very useful!

This feels like something that could be added as an auto-implemented method on the Animatable trait. Does that make sense to you?

You would probably need to special case the spherical interpolation in some form. Perhaps a newtyped Vec2/Vec3 that captures the notion of operating in spherical coordinates?

@IQuick143
Copy link
Contributor

Perhaps a newtyped Vec2/Vec3 that captures the notion of operating in spherical coordinates?
How about a Dir2/3, it's already a newtyped Vec2/3, bound to the unit sphere.

@alice-i-cecile
Copy link
Member

Dir2 and Dir3 are a great primitive for this, and interpolating them should definitely be on the surface of the n-sphere. I think that's probably enough actually, as long as we have an example that shows how to use those interpolation methods to create a spherical path around a center between two points.

@SIGSTACKFAULT
Copy link
Contributor

If Dir3 gets it then Quat should too

@vveisard
Copy link
Author

This feels like something that could be added as an auto-implemented method on the Animatable trait. Does that make sense to you?

Yes, Animatable seems like the right place for a smooth_damp method. However, I would expect a smooth_damp method for f32, Rotation2d, Vec2, Vec3, Dir2, Dir3, and Quat, and some of those lack an Animatable implementation.

I'm not familiar enough with Rust to know if auto-implement is the right thing to do.

@alice-i-cecile
Copy link
Member

Pretty much all of those should have an Animatable implementation :) But yes, this can just be a method on the trait with a default implementation of the method.

github-merge-queue bot pushed a commit that referenced this issue Jun 10, 2024
# Objective

Partially address #13408 

Rework of #13613

Unify the very nice forms of interpolation specifically present in
`bevy_math` under a shared trait upon which further behavior can be
based.

The ideas in this PR were prompted by [Lerp smoothing is broken by Freya
Holmer](https://www.youtube.com/watch?v=LSNQuFEDOyQ).

## Solution

There is a new trait `StableInterpolate` in `bevy_math::common_traits`
which enshrines a quite-specific notion of interpolation with a lot of
guarantees:
```rust
/// A type with a natural interpolation that provides strong subdivision guarantees.
///
/// Although the only required method is `interpolate_stable`, many things are expected of it:
///
/// 1. The notion of interpolation should follow naturally from the semantics of the type, so
///    that inferring the interpolation mode from the type alone is sensible.
///
/// 2. The interpolation recovers something equivalent to the starting value at `t = 0.0`
///    and likewise with the ending value at `t = 1.0`.
///
/// 3. Importantly, the interpolation must be *subdivision-stable*: for any interpolation curve
///    between two (unnamed) values and any parameter-value pairs `(t0, p)` and `(t1, q)`, the
///    interpolation curve between `p` and `q` must be the *linear* reparametrization of the original
///    interpolation curve restricted to the interval `[t0, t1]`.
///
/// The last of these conditions is very strong and indicates something like constant speed. It
/// is called "subdivision stability" because it guarantees that breaking up the interpolation
/// into segments and joining them back together has no effect.
///
/// Here is a diagram depicting it:
/// ```text
/// top curve = u.interpolate_stable(v, t)
///
///              t0 => p   t1 => q    
///   |-------------|---------|-------------|
/// 0 => u         /           \          1 => v
///              /               \
///            /                   \
///          /        linear         \
///        /     reparametrization     \
///      /   t = t0 * (1 - s) + t1 * s   \
///    /                                   \
///   |-------------------------------------|
/// 0 => p                                1 => q
///
/// bottom curve = p.interpolate_stable(q, s)
/// ```
///
/// Note that some common forms of interpolation do not satisfy this criterion. For example,
/// [`Quat::lerp`] and [`Rot2::nlerp`] are not subdivision-stable.
///
/// Furthermore, this is not to be used as a general trait for abstract interpolation.
/// Consumers rely on the strong guarantees in order for behavior based on this trait to be
/// well-behaved.
///
/// [`Quat::lerp`]: crate::Quat::lerp
/// [`Rot2::nlerp`]: crate::Rot2::nlerp
pub trait StableInterpolate: 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`.
    /// When `t = 0.0`, `self` is recovered, while `other` is recovered at `t = 1.0`,
    /// with intermediate values lying between the two.
    fn interpolate_stable(&self, other: &Self, t: f32) -> Self;
}
```

This trait has a blanket implementation over `NormedVectorSpace`, where
`lerp` is used, along with implementations for `Rot2`, `Quat`, and the
direction types using variants of `slerp`. Other areas may choose to
implement this trait in order to hook into its functionality, but the
stringent requirements must actually be met.

This trait bears no direct relationship with `bevy_animation`'s
`Animatable` trait, although they may choose to use `interpolate_stable`
in their trait implementations if they wish, as both traits involve
type-inferred interpolations of the same kind. `StableInterpolate` is
not a supertrait of `Animatable` for a couple reasons:
1. Notions of interpolation in animation are generally going to be much
more general than those allowed under these constraints.
2. Laying out these generalized interpolation notions is the domain of
`bevy_animation` rather than of `bevy_math`. (Consider also that
inferring interpolation from types is not universally desirable.)

Similarly, this is not implemented on `bevy_color`'s color types,
although their current mixing behavior does meet the conditions of the
trait.

As an aside, the subdivision-stability condition is of interest
specifically for the [Curve
RFC](bevyengine/rfcs#80), where it also ensures
a kind of stability for subsampling.

Importantly, this trait ensures that the "smooth following" behavior
defined in this PR behaves predictably:
```rust
    /// Smoothly nudge this value towards the `target` at a given decay rate. The `decay_rate`
    /// parameter controls how fast the distance between `self` and `target` decays relative to
    /// the units of `delta`; the intended usage is for `decay_rate` to generally remain fixed,
    /// while `delta` is something like `delta_time` from an updating system. This produces a
    /// smooth following of the target that is independent of framerate.
    ///
    /// More specifically, when this is called repeatedly, the result is that the distance between
    /// `self` and a fixed `target` attenuates exponentially, with the rate of this exponential
    /// decay given by `decay_rate`.
    ///
    /// For example, at `decay_rate = 0.0`, this has no effect.
    /// At `decay_rate = f32::INFINITY`, `self` immediately snaps to `target`.
    /// In general, higher rates mean that `self` moves more quickly towards `target`.
    ///
    /// # Example
    /// ```
    /// # use bevy_math::{Vec3, StableInterpolate};
    /// # let delta_time: f32 = 1.0 / 60.0;
    /// let mut object_position: Vec3 = Vec3::ZERO;
    /// let target_position: Vec3 = Vec3::new(2.0, 3.0, 5.0);
    /// // Decay rate of ln(10) => after 1 second, remaining distance is 1/10th
    /// let decay_rate = f32::ln(10.0);
    /// // Calling this repeatedly will move `object_position` towards `target_position`:
    /// object_position.smooth_nudge(&target_position, decay_rate, delta_time);
    /// ```
    fn smooth_nudge(&mut self, target: &Self, decay_rate: f32, delta: f32) {
        self.interpolate_stable_assign(target, 1.0 - f32::exp(-decay_rate * delta));
    }
```

As the documentation indicates, the intention is for this to be called
in game update systems, and `delta` would be something like
`Time::delta_seconds` in Bevy, allowing positions, orientations, and so
on to smoothly follow a target. A new example, `smooth_follow`,
demonstrates a basic implementation of this, with a sphere smoothly
following a sharply moving target:


https://github.com/bevyengine/bevy/assets/2975848/7124b28b-6361-47e3-acf7-d1578ebd0347


## Testing

Tested by running the example with various parameters.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Animation Make things move and change over time A-Math Fundamental domain-agnostic mathematical operations C-Enhancement A new feature S-Needs-Design This issue requires design work to think about how it would best be accomplished
Projects
None yet
Development

No branches or pull requests

4 participants