Repository: bitshifter/glam-rs
Pull request #618 (2025-03-19 08:46:38)

Vec3 rotate towards
# Objective

- Added Vec3::rotate_towards with identical behavior to Vec2::rotate_towards
- Supersedes #586 

## Solution

- Taken from #586 it calculates the angle and rotation axis between two vectors and clamps the angle of rotation to `max_angle`. 
- There are a number of optimisations that could be made, vector length squared is calculated multiple times for example.

## Testing and linting

- Additional tests were added



Patch:
diff --git a/benches/vec3.rs b/benches/vec3.rs
index 45350a3..c378e1e 100644
--- a/benches/vec3.rs
+++ b/benches/vec3.rs
@@ -184,6 +184,15 @@ bench_select!(
     from => random_vec3
 );
 
+bench_trinop!(
+    vec3_rotate_towards,
+    "vec3 rotate_towards",
+    op => rotate_towards,
+    from1 => random_vec3,
+    from2 => random_vec3,
+    from3 => random_f32
+);
+
 bench_trinop!(
     vec3_slerp,
     "vec3 slerp",
@@ -213,6 +222,7 @@ criterion_group!(
     vec3_to_rgb,
     vec3_to_tuple_into,
     vec3_slerp,
+    vec3_rotate_towards,
 );
 
 criterion_main!(benches);
diff --git a/src/f32/coresimd/vec3a.rs b/src/f32/coresimd/vec3a.rs
index a6a1d36..71ab4a1 100644
--- a/src/f32/coresimd/vec3a.rs
+++ b/src/f32/coresimd/vec3a.rs
@@ -918,6 +918,24 @@ impl Vec3A {
         )
     }
 
+    /// Rotates towards `rhs` up to `max_angle` (in radians).
+    ///
+    /// When `max_angle` is `0.0`, the result will be equal to `self`. When `max_angle` is equal to
+    /// `self.angle_between(rhs)`, the result will be parallel to `rhs`. If `max_angle` is negative,
+    /// rotates towards the exact opposite of `rhs`. Will not go past the target.
+    #[inline]
+    #[must_use]
+    pub fn rotate_towards(self, rhs: Self, max_angle: f32) -> Self {
+        let angle_between = self.angle_between(rhs);
+        // When `max_angle < 0`, rotate no further than `PI` radians away
+        let angle = max_angle.clamp(angle_between - core::f32::consts::PI, angle_between);
+        let axis = self
+            .cross(rhs)
+            .try_normalize()
+            .unwrap_or_else(|| self.any_orthogonal_vector().normalize());
+        Quat::from_axis_angle(axis.into(), angle) * self
+    }
+
     /// Returns some vector that is orthogonal to the given one.
     ///
     /// The input vector must be finite and non-zero.
diff --git a/src/f32/neon/vec3a.rs b/src/f32/neon/vec3a.rs
index 8eb6ae9..869a47d 100644
--- a/src/f32/neon/vec3a.rs
+++ b/src/f32/neon/vec3a.rs
@@ -962,6 +962,24 @@ impl Vec3A {
         )
     }
 
+    /// Rotates towards `rhs` up to `max_angle` (in radians).
+    ///
+    /// When `max_angle` is `0.0`, the result will be equal to `self`. When `max_angle` is equal to
+    /// `self.angle_between(rhs)`, the result will be parallel to `rhs`. If `max_angle` is negative,
+    /// rotates towards the exact opposite of `rhs`. Will not go past the target.
+    #[inline]
+    #[must_use]
+    pub fn rotate_towards(self, rhs: Self, max_angle: f32) -> Self {
+        let angle_between = self.angle_between(rhs);
+        // When `max_angle < 0`, rotate no further than `PI` radians away
+        let angle = max_angle.clamp(angle_between - core::f32::consts::PI, angle_between);
+        let axis = self
+            .cross(rhs)
+            .try_normalize()
+            .unwrap_or_else(|| self.any_orthogonal_vector().normalize());
+        Quat::from_axis_angle(axis.into(), angle) * self
+    }
+
     /// Returns some vector that is orthogonal to the given one.
     ///
     /// The input vector must be finite and non-zero.
diff --git a/src/f32/scalar/vec3a.rs b/src/f32/scalar/vec3a.rs
index 46d3273..33a4bd3 100644
--- a/src/f32/scalar/vec3a.rs
+++ b/src/f32/scalar/vec3a.rs
@@ -965,6 +965,24 @@ impl Vec3A {
         )
     }
 
+    /// Rotates towards `rhs` up to `max_angle` (in radians).
+    ///
+    /// When `max_angle` is `0.0`, the result will be equal to `self`. When `max_angle` is equal to
+    /// `self.angle_between(rhs)`, the result will be parallel to `rhs`. If `max_angle` is negative,
+    /// rotates towards the exact opposite of `rhs`. Will not go past the target.
+    #[inline]
+    #[must_use]
+    pub fn rotate_towards(self, rhs: Self, max_angle: f32) -> Self {
+        let angle_between = self.angle_between(rhs);
+        // When `max_angle < 0`, rotate no further than `PI` radians away
+        let angle = max_angle.clamp(angle_between - core::f32::consts::PI, angle_between);
+        let axis = self
+            .cross(rhs)
+            .try_normalize()
+            .unwrap_or_else(|| self.any_orthogonal_vector().normalize());
+        Quat::from_axis_angle(axis.into(), angle) * self
+    }
+
     /// Returns some vector that is orthogonal to the given one.
     ///
     /// The input vector must be finite and non-zero.
diff --git a/src/f32/sse2/vec3a.rs b/src/f32/sse2/vec3a.rs
index b2e9c43..855473a 100644
--- a/src/f32/sse2/vec3a.rs
+++ b/src/f32/sse2/vec3a.rs
@@ -970,6 +970,24 @@ impl Vec3A {
         )
     }
 
+    /// Rotates towards `rhs` up to `max_angle` (in radians).
+    ///
+    /// When `max_angle` is `0.0`, the result will be equal to `self`. When `max_angle` is equal to
+    /// `self.angle_between(rhs)`, the result will be parallel to `rhs`. If `max_angle` is negative,
+    /// rotates towards the exact opposite of `rhs`. Will not go past the target.
+    #[inline]
+    #[must_use]
+    pub fn rotate_towards(self, rhs: Self, max_angle: f32) -> Self {
+        let angle_between = self.angle_between(rhs);
+        // When `max_angle < 0`, rotate no further than `PI` radians away
+        let angle = max_angle.clamp(angle_between - core::f32::consts::PI, angle_between);
+        let axis = self
+            .cross(rhs)
+            .try_normalize()
+            .unwrap_or_else(|| self.any_orthogonal_vector().normalize());
+        Quat::from_axis_angle(axis.into(), angle) * self
+    }
+
     /// Returns some vector that is orthogonal to the given one.
     ///
     /// The input vector must be finite and non-zero.
diff --git a/src/f32/vec2.rs b/src/f32/vec2.rs
index 1bcbf49..50f8afa 100644
--- a/src/f32/vec2.rs
+++ b/src/f32/vec2.rs
@@ -952,9 +952,6 @@ impl Vec2 {
     pub fn rotate_towards(&self, rhs: Self, max_angle: f32) -> Self {
         let a = self.angle_to(rhs);
         let abs_a = math::abs(a);
-        if abs_a <= 1e-4 {
-            return rhs;
-        }
         // When `max_angle < 0`, rotate no further than `PI` radians away
         let angle = max_angle.clamp(abs_a - core::f32::consts::PI, abs_a) * math::signum(a);
         Self::from_angle(angle).rotate(*self)
diff --git a/src/f32/vec3.rs b/src/f32/vec3.rs
index beb054f..cddc1bc 100644
--- a/src/f32/vec3.rs
+++ b/src/f32/vec3.rs
@@ -955,6 +955,24 @@ impl Vec3 {
         )
     }
 
+    /// Rotates towards `rhs` up to `max_angle` (in radians).
+    ///
+    /// When `max_angle` is `0.0`, the result will be equal to `self`. When `max_angle` is equal to
+    /// `self.angle_between(rhs)`, the result will be parallel to `rhs`. If `max_angle` is negative,
+    /// rotates towards the exact opposite of `rhs`. Will not go past the target.
+    #[inline]
+    #[must_use]
+    pub fn rotate_towards(self, rhs: Self, max_angle: f32) -> Self {
+        let angle_between = self.angle_between(rhs);
+        // When `max_angle < 0`, rotate no further than `PI` radians away
+        let angle = max_angle.clamp(angle_between - core::f32::consts::PI, angle_between);
+        let axis = self
+            .cross(rhs)
+            .try_normalize()
+            .unwrap_or_else(|| self.any_orthogonal_vector().normalize());
+        Quat::from_axis_angle(axis, angle) * self
+    }
+
     /// Returns some vector that is orthogonal to the given one.
     ///
     /// The input vector must be finite and non-zero.
diff --git a/src/f32/wasm32/vec3a.rs b/src/f32/wasm32/vec3a.rs
index f97b41b..d72e102 100644
--- a/src/f32/wasm32/vec3a.rs
+++ b/src/f32/wasm32/vec3a.rs
@@ -933,6 +933,24 @@ impl Vec3A {
         )
     }
 
+    /// Rotates towards `rhs` up to `max_angle` (in radians).
+    ///
+    /// When `max_angle` is `0.0`, the result will be equal to `self`. When `max_angle` is equal to
+    /// `self.angle_between(rhs)`, the result will be parallel to `rhs`. If `max_angle` is negative,
+    /// rotates towards the exact opposite of `rhs`. Will not go past the target.
+    #[inline]
+    #[must_use]
+    pub fn rotate_towards(self, rhs: Self, max_angle: f32) -> Self {
+        let angle_between = self.angle_between(rhs);
+        // When `max_angle < 0`, rotate no further than `PI` radians away
+        let angle = max_angle.clamp(angle_between - core::f32::consts::PI, angle_between);
+        let axis = self
+            .cross(rhs)
+            .try_normalize()
+            .unwrap_or_else(|| self.any_orthogonal_vector().normalize());
+        Quat::from_axis_angle(axis.into(), angle) * self
+    }
+
     /// Returns some vector that is orthogonal to the given one.
     ///
     /// The input vector must be finite and non-zero.
diff --git a/src/f64/dvec2.rs b/src/f64/dvec2.rs
index c56c604..2a0b9d9 100644
--- a/src/f64/dvec2.rs
+++ b/src/f64/dvec2.rs
@@ -952,9 +952,6 @@ impl DVec2 {
     pub fn rotate_towards(&self, rhs: Self, max_angle: f64) -> Self {
         let a = self.angle_to(rhs);
         let abs_a = math::abs(a);
-        if abs_a <= 1e-4 {
-            return rhs;
-        }
         // When `max_angle < 0`, rotate no further than `PI` radians away
         let angle = max_angle.clamp(abs_a - core::f64::consts::PI, abs_a) * math::signum(a);
         Self::from_angle(angle).rotate(*self)
diff --git a/src/f64/dvec3.rs b/src/f64/dvec3.rs
index 651f4e8..56014bd 100644
--- a/src/f64/dvec3.rs
+++ b/src/f64/dvec3.rs
@@ -955,6 +955,24 @@ impl DVec3 {
         )
     }
 
+    /// Rotates towards `rhs` up to `max_angle` (in radians).
+    ///
+    /// When `max_angle` is `0.0`, the result will be equal to `self`. When `max_angle` is equal to
+    /// `self.angle_between(rhs)`, the result will be parallel to `rhs`. If `max_angle` is negative,
+    /// rotates towards the exact opposite of `rhs`. Will not go past the target.
+    #[inline]
+    #[must_use]
+    pub fn rotate_towards(self, rhs: Self, max_angle: f64) -> Self {
+        let angle_between = self.angle_between(rhs);
+        // When `max_angle < 0`, rotate no further than `PI` radians away
+        let angle = max_angle.clamp(angle_between - core::f64::consts::PI, angle_between);
+        let axis = self
+            .cross(rhs)
+            .try_normalize()
+            .unwrap_or_else(|| self.any_orthogonal_vector().normalize());
+        DQuat::from_axis_angle(axis, angle) * self
+    }
+
     /// Returns some vector that is orthogonal to the given one.
     ///
     /// The input vector must be finite and non-zero.
diff --git a/templates/vec.rs.tera b/templates/vec.rs.tera
index 1d694ab..78a471c 100644
--- a/templates/vec.rs.tera
+++ b/templates/vec.rs.tera
@@ -2211,6 +2211,24 @@ impl {{ self_t }} {
                 math::sqrt(self.length_squared().mul(rhs.length_squared()))))
     }
 
+    /// Rotates towards `rhs` up to `max_angle` (in radians).
+    ///
+    /// When `max_angle` is `0.0`, the result will be equal to `self`. When `max_angle` is equal to
+    /// `self.angle_between(rhs)`, the result will be parallel to `rhs`. If `max_angle` is negative,
+    /// rotates towards the exact opposite of `rhs`. Will not go past the target.
+    #[inline]
+    #[must_use]
+    pub fn rotate_towards(self, rhs: Self, max_angle: {{ scalar_t }}) -> Self {
+        let angle_between = self.angle_between(rhs);
+        // When `max_angle < 0`, rotate no further than `PI` radians away
+        let angle = max_angle.clamp(angle_between - core::{{ scalar_t }}::consts::PI, angle_between);
+        let axis = self
+            .cross(rhs)
+            .try_normalize()
+            .unwrap_or_else(|| self.any_orthogonal_vector().normalize());
+        {{ quat_t }}::from_axis_angle(axis{% if self_t == "Vec3A" %}.into(){% endif%}, angle) * self
+    }
+
     /// Returns some vector that is orthogonal to the given one.
     ///
     /// The input vector must be finite and non-zero.
@@ -2412,9 +2430,6 @@ impl {{ self_t }} {
     pub fn rotate_towards(&self, rhs: Self, max_angle: {{ scalar_t }}) -> Self {
         let a = self.angle_to(rhs);
         let abs_a = math::abs(a);
-        if abs_a <= 1e-4 {
-            return rhs;
-        }
         // When `max_angle < 0`, rotate no further than `PI` radians away
         let angle = max_angle.clamp(abs_a - core::{{ scalar_t }}::consts::PI, abs_a) * math::signum(a);
         Self::from_angle(angle).rotate(*self)


Test patch:
diff --git a/tests/vec2.rs b/tests/vec2.rs
index 317696d..521fb8f 100644
--- a/tests/vec2.rs
+++ b/tests/vec2.rs
@@ -1110,29 +1110,66 @@ macro_rules! impl_vec2_float_tests {
             use core::$t::consts::{FRAC_PI_2, FRAC_PI_4};
             let eps = 10.0 * $t::EPSILON as f32;
 
-            // Setup such that `v0` is `PI/2` and `-PI/2` radians away from `v1` and `v2` respectively.
-            let v0 = $vec2::new(1.0, 0.0);
-            let v1 = $vec2::new(0.0, -1.0);
-            let v2 = $vec2::new(0.0, 1.0);
+            // Self
+            assert_approx_eq!($vec2::X, $vec2::X.rotate_towards($vec2::X, FRAC_PI_2), eps);
+            assert_approx_eq!(
+                $vec3::Y * 2.0,
+                ($vec3::Y * 2.0).rotate_towards($vec3::Y, FRAC_PI_2)
+            );
+            assert_approx_eq!(
+                $vec3::NEG_X,
+                $vec3::NEG_X.rotate_towards($vec3::NEG_X, FRAC_PI_2)
+            );
 
             // Positive delta
-            assert_approx_eq!(v0, v0.rotate_towards(v1, 0.0), eps);
+            assert_approx_eq!($vec2::X, $vec2::X.rotate_towards($vec2::NEG_Y, 0.0), eps);
             assert_approx_eq!(
                 $vec2::new($t::sqrt(2.0) / 2.0, -$t::sqrt(2.0) / 2.0),
-                v0.rotate_towards(v1, FRAC_PI_4),
+                $vec2::X.rotate_towards($vec2::NEG_Y, FRAC_PI_4),
+                eps
+            );
+            assert_approx_eq!(
+                $vec2::NEG_Y,
+                $vec2::X.rotate_towards($vec2::NEG_Y, FRAC_PI_2),
+                eps
+            );
+            assert_approx_eq!(
+                $vec2::NEG_Y,
+                $vec2::X.rotate_towards($vec2::NEG_Y, FRAC_PI_2 * 1.5),
                 eps
             );
-            assert_approx_eq!(v1, v0.rotate_towards(v1, FRAC_PI_2), eps);
-            assert_approx_eq!(v1, v0.rotate_towards(v1, FRAC_PI_2 * 1.5), eps);
 
             // Negative delta
             assert_approx_eq!(
                 $vec2::new($t::sqrt(2.0) / 2.0, $t::sqrt(2.0) / 2.0),
-                v0.rotate_towards(v1, -FRAC_PI_4),
+                $vec2::X.rotate_towards($vec2::NEG_Y, -FRAC_PI_4),
+                eps
+            );
+            assert_approx_eq!(
+                $vec2::Y,
+                $vec2::X.rotate_towards($vec2::NEG_Y, -FRAC_PI_2),
+                eps
+            );
+            assert_approx_eq!(
+                $vec2::Y,
+                $vec2::X.rotate_towards($vec2::NEG_Y, -FRAC_PI_2 * 1.5),
+                eps
+            );
+
+            // Not normalized
+            assert_approx_eq!(
+                $vec2::NEG_Y * 2.,
+                ($vec2::X * 2.).rotate_towards($vec2::NEG_Y, FRAC_PI_2),
+                eps
+            );
+            assert_approx_eq!(
+                $vec2::NEG_Y,
+                $vec2::X.rotate_towards($vec2::NEG_Y * 2., FRAC_PI_2),
                 eps
             );
-            assert_approx_eq!(v2, v0.rotate_towards(v1, -FRAC_PI_2), eps);
-            assert_approx_eq!(v2, v0.rotate_towards(v1, -FRAC_PI_2 * 1.5), eps);
+
+            // Parallel
+            assert_approx_eq!($vec2::Y, $vec2::X.rotate_towards(-$vec2::X, FRAC_PI_2), eps);
         });
 
         glam_test!(test_midpoint, {
diff --git a/tests/vec3.rs b/tests/vec3.rs
index 259f5b1..6139019 100644
--- a/tests/vec3.rs
+++ b/tests/vec3.rs
@@ -1337,6 +1337,53 @@ macro_rules! impl_vec3_float_tests {
             assert_approx_eq!(v1, v0.move_towards(v1, v0.distance(v1) + 1.0));
         });
 
+        glam_test!(test_rotate_towards, {
+            use core::$t::consts::PI;
+
+            // Self
+            assert_approx_eq!($vec3::X, $vec3::X.rotate_towards($vec3::X, PI / 2.));
+            assert_approx_eq!(
+                $vec3::Y * 2.0,
+                ($vec3::Y * 2.0).rotate_towards($vec3::Y, PI / 2.)
+            );
+            assert_approx_eq!($vec3::Z, $vec3::Z.rotate_towards($vec3::Z, PI / 2.));
+
+            // Positive angle
+            assert_approx_eq!($vec3::X, $vec3::X.rotate_towards($vec3::NEG_Y, 0.0));
+            assert_approx_eq!(
+                $vec3::new($t::sqrt(2.0) / 2.0, -$t::sqrt(2.0) / 2.0, 0.0),
+                $vec3::X.rotate_towards($vec3::NEG_Y, PI / 4.)
+            );
+            assert_approx_eq!(
+                $vec3::new($t::sqrt(2.0) / 2.0, 0.0, $t::sqrt(2.0) / 2.0),
+                $vec3::X.rotate_towards($vec3::Z, PI / 4.)
+            );
+            assert_approx_eq!($vec3::NEG_Y, $vec3::X.rotate_towards($vec3::NEG_Y, PI / 2.));
+            assert_approx_eq!($vec3::NEG_Y, $vec3::X.rotate_towards($vec3::NEG_Y, PI));
+
+            // Negative angle
+            assert_approx_eq!(
+                $vec3::new($t::sqrt(2.0) / 2.0, $t::sqrt(2.0) / 2.0, 0.0),
+                $vec3::X.rotate_towards($vec3::NEG_Y, -PI / 4.)
+            );
+            assert_approx_eq!($vec3::Y, $vec3::X.rotate_towards($vec3::NEG_Y, -PI / 2.));
+            assert_approx_eq!($vec3::Y, $vec3::X.rotate_towards($vec3::NEG_Y, -PI), 2e-7);
+
+            // Not normalized
+            assert_approx_eq!(
+                $vec3::NEG_Y * 2.,
+                ($vec3::X * 2.).rotate_towards($vec3::NEG_Y, PI / 2.),
+                2e-7
+            );
+            assert_approx_eq!(
+                $vec3::NEG_Y,
+                $vec3::X.rotate_towards($vec3::NEG_Y * 2., PI / 2.)
+            );
+
+            // Parallel
+            assert_approx_eq!($vec3::Y, $vec3::X.rotate_towards($vec3::NEG_X, PI / 2.));
+        });
+
         glam_test!(test_midpoint, {
             let v0 = $vec3::new(-1.0, -1.0, -1.0);
             let v1 = $vec3::new(1.0, 1.0, 1.0);