-
-
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
"smooth_damp" functions for primitives and math structs #13408
Comments
I definitely want this: very useful! This feels like something that could be added as an auto-implemented method on the 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? |
|
|
If |
Yes, I'm not familiar enough with Rust to know if auto-implement is the right thing to do. |
Pretty much all of those should have an |
# 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.
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:
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
The text was updated successfully, but these errors were encountered: