From 42c7e8e5f1d66ab82b5d14fef7241c75cf385d40 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Fri, 31 May 2024 10:59:55 -0400 Subject: [PATCH 01/12] Add shell of Interpolate to common_traits --- crates/bevy_math/src/common_traits.rs | 51 ++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/crates/bevy_math/src/common_traits.rs b/crates/bevy_math/src/common_traits.rs index 6074f2526607d..4f7ea11d81b4e 100644 --- a/crates/bevy_math/src/common_traits.rs +++ b/crates/bevy_math/src/common_traits.rs @@ -1,4 +1,4 @@ -use glam::{Vec2, Vec3, Vec3A, Vec4}; +use crate::{Dir2, Dir3, Dir3A, Quat, Vec2, Vec3, Vec3A, Vec4}; use std::fmt::Debug; use std::ops::{Add, Div, Mul, Neg, Sub}; @@ -161,3 +161,52 @@ impl NormedVectorSpace for f32 { self * self } } + +pub trait Interpolate: Clone { + fn interpolate(&self, other: &Self, t: f32) -> Self; + + fn interpolate_assign(&mut self, other: &Self, t: f32) { + *self = self.interpolate(other, t); + } + + fn smooth_nudge(&self, other: &Self, rate: f32, delta: f32) -> Self { + self.interpolate(other, 1.0 - f32::exp(-rate * delta)) + } + + fn smooth_nudge_assign(&mut self, other: &Self, rate: f32, delta: f32) { + *self = self.smooth_nudge(other, rate, delta); + } +} + +impl Interpolate for V +where + V: VectorSpace, +{ + fn interpolate(&self, other: &Self, t: f32) -> Self { + *self * (1.0 - t) + *other * t + } +} + +impl Interpolate for Quat { + fn interpolate(&self, other: &Self, t: f32) -> Self { + self.slerp(*other, t) + } +} + +impl Interpolate for Dir2 { + fn interpolate(&self, other: &Self, t: f32) -> Self { + self.slerp(*other, t) + } +} + +impl Interpolate for Dir3 { + fn interpolate(&self, other: &Self, t: f32) -> Self { + self.slerp(*other, t) + } +} + +impl Interpolate for Dir3A { + fn interpolate(&self, other: &Self, t: f32) -> Self { + self.slerp(*other, t) + } +} From e1bae300326d1e7696901b0f5290f57fc6bb2e70 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Fri, 31 May 2024 12:02:13 -0400 Subject: [PATCH 02/12] Add documentation --- crates/bevy_math/src/common_traits.rs | 50 +++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/crates/bevy_math/src/common_traits.rs b/crates/bevy_math/src/common_traits.rs index 4f7ea11d81b4e..a485271373aca 100644 --- a/crates/bevy_math/src/common_traits.rs +++ b/crates/bevy_math/src/common_traits.rs @@ -162,19 +162,57 @@ impl NormedVectorSpace for f32 { } } +/// A type that can be intermediately interpolated between two given values +/// with an auxiliary parameter. +/// +/// The expectations for the implementing type are as follows: +/// - `interpolate(&first, &second, t)` produces `first.clone()` when `t = 0.0` +/// and `second.clone()` when `t = 1.0`. +/// - `interpolate` is self-similar in the sense that, for any values `t0`, `t1`, +/// `interpolate(interpolate(&first, &second, t0), interpolate(&first, &second, t1), t)` +/// is equivalent to `interpolate(&first, &second, interpolate(&t0, &t1, t))`. pub trait Interpolate: 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`. + /// However, when `t = 0.0`, `self` is recovered, while `other` is recovered at `t = 1.0`, + /// with intermediate values lying "between" the two in some appropriate sense. fn interpolate(&self, other: &Self, t: f32) -> Self; + /// A version of [`interpolate`] that assigns the result to `self` for convenience. + /// + /// [`interpolate`]: Interpolate::interpolate fn interpolate_assign(&mut self, other: &Self, t: f32) { *self = self.interpolate(other, t); } - fn smooth_nudge(&self, other: &Self, rate: f32, delta: f32) -> Self { - self.interpolate(other, 1.0 - f32::exp(-rate * delta)) - } - - fn smooth_nudge_assign(&mut self, other: &Self, rate: f32, delta: f32) { - *self = self.smooth_nudge(other, rate, delta); + /// Returns the result of nudging `self` 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 a fixed-time + /// 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, Interpolate}; + /// # 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 = self.interpolate(target, 1.0 - f32::exp(-decay_rate * delta)); } } From 8937db91eeefe1684739ee62f6d7bfdc9de9fc8b Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Fri, 31 May 2024 12:07:49 -0400 Subject: [PATCH 03/12] Minor tweaks --- crates/bevy_math/src/common_traits.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_math/src/common_traits.rs b/crates/bevy_math/src/common_traits.rs index a485271373aca..59308394c6118 100644 --- a/crates/bevy_math/src/common_traits.rs +++ b/crates/bevy_math/src/common_traits.rs @@ -163,7 +163,7 @@ impl NormedVectorSpace for f32 { } /// A type that can be intermediately interpolated between two given values -/// with an auxiliary parameter. +/// using an auxiliary linear parameter. /// /// The expectations for the implementing type are as follows: /// - `interpolate(&first, &second, t)` produces `first.clone()` when `t = 0.0` @@ -208,11 +208,11 @@ pub trait Interpolate: Clone { /// 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` + /// // 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 = self.interpolate(target, 1.0 - f32::exp(-decay_rate * delta)); + self.interpolate_assign(target, 1.0 - f32::exp(-decay_rate * delta)); } } From fd30d0404d54677a3f55d87020fb7c7a20bcd345 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Fri, 31 May 2024 15:33:55 -0400 Subject: [PATCH 04/12] Refactor out Interpolate from Animatable, make example --- Cargo.toml | 11 ++ crates/bevy_animation/src/animatable.rs | 46 +----- crates/bevy_animation/src/lib.rs | 1 - crates/bevy_animation/src/util.rs | 10 -- crates/bevy_math/src/common_traits.rs | 48 +++++- crates/bevy_math/src/lib.rs | 6 +- .../src/components/transform.rs | 12 +- examples/README.md | 1 + examples/math/smooth_follow.rs | 145 ++++++++++++++++++ 9 files changed, 218 insertions(+), 62 deletions(-) delete mode 100644 crates/bevy_animation/src/util.rs create mode 100644 examples/math/smooth_follow.rs diff --git a/Cargo.toml b/Cargo.toml index fae29567b7bb4..00989b1fd2304 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2993,6 +2993,17 @@ description = "Demonstrates how to sample random points from mathematical primit category = "Math" wasm = true +[[example]] +name = "smooth_follow" +path = "examples/math/smooth_follow.rs" +doc-scrape-examples = true + +[package.metadata.example.smooth_follow] +name = "Smooth Follow" +description = "Demonstrates how to make an entity smoothly follow another using interpolation" +category = "Math" +wasm = true + # Gizmos [[example]] name = "2d_gizmos" diff --git a/crates/bevy_animation/src/animatable.rs b/crates/bevy_animation/src/animatable.rs index 4e59ccc8b2875..1e09aa70380c4 100644 --- a/crates/bevy_animation/src/animatable.rs +++ b/crates/bevy_animation/src/animatable.rs @@ -1,4 +1,3 @@ -use crate::util; use bevy_color::{Laba, LinearRgba, Oklaba, Srgba, Xyza}; use bevy_ecs::world::World; use bevy_math::*; @@ -16,12 +15,7 @@ pub struct BlendInput { } /// An animatable value type. -pub trait Animatable: Reflect + Sized + Send + Sync + 'static { - /// Interpolates between `a` and `b` with a interpolation factor of `time`. - /// - /// The `time` parameter here may not be clamped to the range `[0.0, 1.0]`. - fn interpolate(a: &Self, b: &Self, time: f32) -> Self; - +pub trait Animatable: Reflect + Interpolate + Sized + Send + Sync + 'static { /// Blends one or more values together. /// /// Implementors should return a default value when no inputs are provided here. @@ -35,12 +29,6 @@ pub trait Animatable: Reflect + Sized + Send + Sync + 'static { macro_rules! impl_float_animatable { ($ty: ty, $base: ty) => { impl Animatable for $ty { - #[inline] - fn interpolate(a: &Self, b: &Self, t: f32) -> Self { - let t = <$base>::from(t); - (*a) * (1.0 - t) + (*b) * t - } - #[inline] fn blend(inputs: impl Iterator>) -> Self { let mut value = Default::default(); @@ -60,12 +48,6 @@ macro_rules! impl_float_animatable { macro_rules! impl_color_animatable { ($ty: ident) => { impl Animatable for $ty { - #[inline] - fn interpolate(a: &Self, b: &Self, t: f32) -> Self { - let value = *a * (1. - t) + *b * t; - value - } - #[inline] fn blend(inputs: impl Iterator>) -> Self { let mut value = Default::default(); @@ -100,11 +82,6 @@ impl_color_animatable!(Xyza); // Vec3 is special cased to use Vec3A internally for blending impl Animatable for Vec3 { - #[inline] - fn interpolate(a: &Self, b: &Self, t: f32) -> Self { - (*a) * (1.0 - t) + (*b) * t - } - #[inline] fn blend(inputs: impl Iterator>) -> Self { let mut value = Vec3A::ZERO; @@ -120,11 +97,6 @@ impl Animatable for Vec3 { } impl Animatable for bool { - #[inline] - fn interpolate(a: &Self, b: &Self, t: f32) -> Self { - util::step_unclamped(*a, *b, t) - } - #[inline] fn blend(inputs: impl Iterator>) -> Self { inputs @@ -135,14 +107,6 @@ impl Animatable for bool { } impl Animatable for Transform { - fn interpolate(a: &Self, b: &Self, t: f32) -> Self { - Self { - translation: Vec3::interpolate(&a.translation, &b.translation, t), - rotation: Quat::interpolate(&a.rotation, &b.rotation, t), - scale: Vec3::interpolate(&a.scale, &b.scale, t), - } - } - fn blend(inputs: impl Iterator>) -> Self { let mut translation = Vec3A::ZERO; let mut scale = Vec3A::ZERO; @@ -173,14 +137,6 @@ impl Animatable for Transform { } impl Animatable for Quat { - /// Performs a slerp to smoothly interpolate between quaternions. - #[inline] - fn interpolate(a: &Self, b: &Self, t: f32) -> Self { - // We want to smoothly interpolate between the two quaternions by default, - // rather than using a quicker but less correct linear interpolation. - a.slerp(*b, t) - } - #[inline] fn blend(inputs: impl Iterator>) -> Self { let mut value = Self::IDENTITY; diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index f634d13259159..7bf62a32de2a1 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -10,7 +10,6 @@ mod animatable; mod graph; mod transition; -mod util; use std::cell::RefCell; use std::collections::BTreeMap; diff --git a/crates/bevy_animation/src/util.rs b/crates/bevy_animation/src/util.rs deleted file mode 100644 index 67aaf8116e365..0000000000000 --- a/crates/bevy_animation/src/util.rs +++ /dev/null @@ -1,10 +0,0 @@ -/// Steps between two different discrete values of any type. -/// Returns `a` if `t < 1.0`, otherwise returns `b`. -#[inline] -pub(crate) fn step_unclamped(a: T, b: T, t: f32) -> T { - if t < 1.0 { - a - } else { - b - } -} diff --git a/crates/bevy_math/src/common_traits.rs b/crates/bevy_math/src/common_traits.rs index 59308394c6118..a7d06ae6cee23 100644 --- a/crates/bevy_math/src/common_traits.rs +++ b/crates/bevy_math/src/common_traits.rs @@ -1,4 +1,4 @@ -use crate::{Dir2, Dir3, Dir3A, Quat, Vec2, Vec3, Vec3A, Vec4}; +use crate::{DVec2, DVec3, DVec4, Dir2, Dir3, Dir3A, Quat, Vec2, Vec3, Vec3A, Vec4}; use std::fmt::Debug; use std::ops::{Add, Div, Mul, Neg, Sub}; @@ -216,35 +216,79 @@ pub trait Interpolate: Clone { } } +/// Steps between two different discrete values of any type. +/// Returns `a` if `t < 1.0`, otherwise returns `b`. +/// +/// This is a common form of interpolation for discrete types. +#[inline] +fn step_unclamped(a: T, b: T, t: f32) -> T { + if t < 1.0 { + a + } else { + b + } +} + impl Interpolate for V where V: VectorSpace, { + #[inline] fn interpolate(&self, other: &Self, t: f32) -> Self { - *self * (1.0 - t) + *other * t + self.lerp(*other, t) } } impl Interpolate for Quat { + #[inline] fn interpolate(&self, other: &Self, t: f32) -> Self { self.slerp(*other, t) } } impl Interpolate for Dir2 { + #[inline] fn interpolate(&self, other: &Self, t: f32) -> Self { self.slerp(*other, t) } } impl Interpolate for Dir3 { + #[inline] fn interpolate(&self, other: &Self, t: f32) -> Self { self.slerp(*other, t) } } impl Interpolate for Dir3A { + #[inline] fn interpolate(&self, other: &Self, t: f32) -> Self { self.slerp(*other, t) } } + +/// This macro is for implementing `Interpolate` on non-f32-based vector-space-like entities. +macro_rules! impl_float_interpolate { + ($ty: ty, $base: ty) => { + impl Interpolate for $ty { + #[inline] + fn interpolate(&self, other: &Self, t: f32) -> Self { + let t = <$base>::from(t); + (*self) * (1.0 - t) + (*other) * t + } + } + }; +} + +impl_float_interpolate!(f64, f64); +impl_float_interpolate!(DVec2, f64); +impl_float_interpolate!(DVec3, f64); +impl_float_interpolate!(DVec4, f64); + +// This is slightly cursed but necessary for unifying with an `Animatable` implementation for `bool` +impl Interpolate for bool { + #[inline] + fn interpolate(&self, other: &Self, t: f32) -> Self { + step_unclamped(*self, *other, t) + } +} diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index c98c328d1befa..cdb4e01fa0b9e 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -50,9 +50,9 @@ pub mod prelude { }, direction::{Dir2, Dir3, Dir3A}, primitives::*, - BVec2, BVec3, BVec4, EulerRot, FloatExt, IRect, IVec2, IVec3, IVec4, Mat2, Mat3, Mat4, - Quat, Ray2d, Ray3d, Rect, Rotation2d, URect, UVec2, UVec3, UVec4, Vec2, Vec2Swizzles, Vec3, - Vec3Swizzles, Vec4, Vec4Swizzles, + BVec2, BVec3, BVec4, EulerRot, FloatExt, IRect, IVec2, IVec3, IVec4, Interpolate, Mat2, + Mat3, Mat4, Quat, Ray2d, Ray3d, Rect, Rotation2d, URect, UVec2, UVec3, UVec4, Vec2, + Vec2Swizzles, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles, }; } diff --git a/crates/bevy_transform/src/components/transform.rs b/crates/bevy_transform/src/components/transform.rs index a7c3d4db0c397..2af438f4fb2b5 100644 --- a/crates/bevy_transform/src/components/transform.rs +++ b/crates/bevy_transform/src/components/transform.rs @@ -1,6 +1,6 @@ use super::GlobalTransform; use bevy_ecs::{component::Component, reflect::ReflectComponent}; -use bevy_math::{Affine3A, Dir3, Mat3, Mat4, Quat, Vec3}; +use bevy_math::{Affine3A, Dir3, Interpolate, Mat3, Mat4, Quat, Vec3}; use bevy_reflect::prelude::*; use bevy_reflect::Reflect; use std::ops::Mul; @@ -559,3 +559,13 @@ impl Mul for Transform { self.transform_point(value) } } + +impl Interpolate for Transform { + fn interpolate(&self, other: &Self, t: f32) -> Self { + Transform { + translation: self.translation.interpolate(&other.translation, t), + rotation: self.rotation.interpolate(&other.rotation, t), + scale: self.scale.interpolate(&other.scale, t), + } + } +} diff --git a/examples/README.md b/examples/README.md index 661c9c6a78d0c..aaf3b0e6dc32f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -323,6 +323,7 @@ Example | Description [Random Sampling](../examples/math/random_sampling.rs) | Demonstrates how to sample random points from mathematical primitives [Rendering Primitives](../examples/math/render_primitives.rs) | Shows off rendering for all math primitives as both Meshes and Gizmos [Sampling Primitives](../examples/math/sampling_primitives.rs) | Demonstrates all the primitives which can be sampled. +[Smooth Follow](../examples/math/smooth_follow.rs) | Demonstrates how to make an entity smoothly follow another using interpolation ## Reflection diff --git a/examples/math/smooth_follow.rs b/examples/math/smooth_follow.rs new file mode 100644 index 0000000000000..f15fd65fa0d17 --- /dev/null +++ b/examples/math/smooth_follow.rs @@ -0,0 +1,145 @@ +//! This example demonstrates how to use interpolation to make one entity smoothly follow another. + +use bevy::math::{prelude::*, vec3, NormedVectorSpace}; +use bevy::prelude::*; +use rand::SeedableRng; +use rand_chacha::ChaCha8Rng; +use std::cmp::min_by; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, (move_target, move_follower).chain()) + .run(); +} + +// The sphere that the following sphere targets at all times: +#[derive(Component)] +struct TargetSphere; + +// The speed of the target sphere moving to its next location: +#[derive(Resource)] +struct TargetSphereSpeed(f32); + +// The position that the target sphere always moves linearly toward: +#[derive(Resource)] +struct TargetPosition(Vec3); + +// The decay rate used by the smooth following: +#[derive(Resource)] +struct DecayRate(f32); + +// The sphere that follows the target sphere by moving towards it with nudging: +#[derive(Component)] +struct FollowingSphere; + +/// The source of randomness used by this example. +#[derive(Resource)] +struct RandomSource(ChaCha8Rng); + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // A plane: + commands.spawn(PbrBundle { + mesh: meshes.add(Plane3d::default().mesh().size(12.0, 12.0)), + material: materials.add(Color::srgb(0.3, 0.15, 0.3)), + transform: Transform::from_xyz(0.0, -2.5, 0.0), + ..default() + }); + + // The target sphere: + commands.spawn(( + PbrBundle { + mesh: meshes.add(Sphere::new(0.3)), + material: materials.add(Color::srgb(0.3, 0.15, 0.9)), + ..default() + }, + TargetSphere, + )); + + // The sphere that follows it: + commands.spawn(( + PbrBundle { + mesh: meshes.add(Sphere::new(0.3)), + material: materials.add(Color::srgb(0.9, 0.3, 0.3)), + transform: Transform::from_translation(vec3(0.0, -2.0, 0.0)), + ..default() + }, + FollowingSphere, + )); + + // A light: + commands.spawn(PointLightBundle { + point_light: PointLight { + intensity: 15_000_000.0, + shadows_enabled: true, + ..default() + }, + transform: Transform::from_xyz(4.0, 8.0, 4.0), + ..default() + }); + + // A camera: + commands.spawn(Camera3dBundle { + transform: Transform::from_xyz(-2.0, 3.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + ..default() + }); + + // Set starting values for resources used by the systems: + commands.insert_resource(TargetSphereSpeed(5.0)); + commands.insert_resource(DecayRate(2.0)); + commands.insert_resource(TargetPosition(Vec3::ZERO)); + commands.insert_resource(RandomSource(ChaCha8Rng::seed_from_u64(68941654987813521))); +} + +fn move_target( + mut target: Query<&mut Transform, With>, + target_speed: Res, + mut target_pos: ResMut, + time: Res