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

Stable interpolation and smooth following #13741

Merged
merged 13 commits into from
Jun 10, 2024

Conversation

mweatherley
Copy link
Contributor

@mweatherley mweatherley commented Jun 7, 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.

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:

/// 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, 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:

    /// 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:

smooth_follow.mov

Testing

Tested by running the example with various parameters.

@mweatherley mweatherley added C-Feature A new feature, making something new possible A-Math Fundamental domain-agnostic mathematical operations X-Contentious There are nontrivial implications that should be thought through S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jun 7, 2024
@mweatherley
Copy link
Contributor Author

mweatherley commented Jun 7, 2024

I didn't label this for Animation or Color since it technically doesn't touch those things, but feedback from those directions (persuant to my comments in the description or otherwise) is obviously still welcome :)

let abs_delta = (target_pos.0 - target.translation).norm();

// Avoid overshooting in case of high values of `delta_time``:
let magnitude = min_by(abs_delta, delta_time * target_speed.0, |f0, f1| {
Copy link
Contributor

@torsteingrindvik torsteingrindvik Jun 8, 2024

Choose a reason for hiding this comment

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

nitpick:

could

let magnitude = abs_delta.min(delta_time * target_speed.0);

work here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I forgot about f32::min and was just thinking of the Ord version. Good catch!

/// # 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
Copy link
Contributor

@torsteingrindvik torsteingrindvik Jun 8, 2024

Choose a reason for hiding this comment

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

praise: I had the exact question of how can I as a user approximate how long it would take a follower to reach a target- love that you added this example

// The two are really close, so let's generate a new target position:
Err(_) => {
let legal_region = Cuboid::from_size(Vec3::splat(4.0));
*target_pos = TargetPosition(legal_region.sample_interior(&mut rng.0));
Copy link
Contributor

@torsteingrindvik torsteingrindvik Jun 8, 2024

Choose a reason for hiding this comment

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

praise: Elegant 👍

// Conservatively, we presently only apply this for normed vector spaces, where the notion
// of being constant-speed is literally true. The technical axioms are satisfied for any
// VectorSpace type, but the "natural from the semantics" part is less clear in general.
impl<V> StableInterpolate for V
Copy link
Contributor

@torsteingrindvik torsteingrindvik Jun 8, 2024

Choose a reason for hiding this comment

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

question: I saw that neither this blanket impl nor the concrete impls below add an impl for any color type.

I see color types can lerp via this trait: http://dev-docs.bevyengine.org/bevy/math/trait.VectorSpace.html
I'm curious how smooth nudging would look like for colors.

I suppose there is a reason why colors are left out- they probably don't meet the requirements for StableInterpolate?

EDIT: I read the OP description now and I see it mentions colors being left out intentionally but that it could possibly have an impl later, that sounds promising.

Copy link
Contributor

@djeedai djeedai Jun 8, 2024

Choose a reason for hiding this comment

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

We discussed on another PR, colors are very different from other types and there's a perceptual component to blending them. I general it makes few sense to treat them like other math types, except for trivial cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, precisely as djeedai says. The way I see it is that despite the fact that existing color mixing satisfies the interpolation laws, it's still unclear that it's actually "canonical" enough to warrant being included here. For example, HSV color space does some kind of cylindrical interpolation, but "uniformity" in that space only has to do with the way colors are represented, so a constant-speed path in that space is not necessarily semantically meaningful. Perhaps a stronger case could be made in perceptually uniform color spaces, but that's quite a nuanced matter that I'll leave to the color people. :)

Copy link
Contributor

Choose a reason for hiding this comment

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

It'd like to see colors and animation revisited once the curves api is out of the oven. The policy that has worked the best for us so far is to design useful things in bevy_math and then later go back and integrate them.

@torsteingrindvik
Copy link
Contributor

torsteingrindvik commented Jun 8, 2024

smooth-follow.mp4

This is your example but with 1k spheres, works great.

I tried different amount of targets/followers, changing decay rates and speeds etc. and everything works as expected.

Copy link
Contributor

@torsteingrindvik torsteingrindvik left a comment

Choose a reason for hiding this comment

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

Approving on the basis of:

  • It's something I've wanted to see in Bevy
  • As a user it feels elegant to use
  • I tried the PR and it works well

But the math itself I'll leave to others- not my strength

/// 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`
Copy link
Contributor

Choose a reason for hiding this comment

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

The doc on interpolate_stable() seems to say you recover exactly self and other. Why the vague "something equivalent to" mention here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I chose this loosey-goosey wording around "equivalence" because what I really mean is that they don't necessarily have to be data-identical, but they do have to be semantically identical. An example of this is that a Quat as used in glam/bevy_math represents a rotation, and it's the case that slerp doesn't always return a data-identical quaternion at its end; however, it does always return one that represents the same rotation as the one that was input. I'll amend the docs to make this more clear and make the two more consistent.

Copy link
Contributor

Choose a reason for hiding this comment

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

Good point on Quat rotations. Maybe it's worth writing down that example to explain the wording.

Copy link
Contributor

Choose a reason for hiding this comment

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

I like the new clarification you added here.

Copy link
Contributor

@NthTensor NthTensor left a comment

Choose a reason for hiding this comment

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

Wonderful. The properties of interpolate_stable seem extremely useful and well defined, and the documentation is truly top-notch.

I've very pleased to see your investigation of the curves api yielding related work like this.

@NthTensor NthTensor added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jun 9, 2024
@alice-i-cecile alice-i-cecile added this to the 0.15 milestone Jun 9, 2024
@alice-i-cecile alice-i-cecile added the M-Needs-Release-Note Work that should be called out in the blog due to impact label Jun 10, 2024
@alice-i-cecile alice-i-cecile added this pull request to the merge queue Jun 10, 2024
Merged via the queue into bevyengine:main with commit a569b35 Jun 10, 2024
33 checks passed
@mweatherley mweatherley deleted the interpolate-follow branch June 10, 2024 13:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Math Fundamental domain-agnostic mathematical operations C-Feature A new feature, making something new possible M-Needs-Release-Note Work that should be called out in the blog due to impact S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Contentious There are nontrivial implications that should be thought through
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants