Repository: bitshifter/glam-rs
Pull request #617 (2025-03-19 04:54:16)

Add `nostd-libm` feature
# Objective

In [Bevy](https://github.com/bevyengine/bevy) we're working on adding `no_std` support, which has forced us to interact with the `std` or `libm` feature pair that Glam has. An issue we have is we want to support compiling `bevy` without default features, but our use of Glam makes that currently impossible. The workaround we've settled on is to just make all of our floating point maths related code optional, but that degrades the developer experience by making what was previously included functionality gated behind a seemingly unrelated question of `std` or `libm`.

Since `libm` compiles relatively instantly in the context of Bevy's other dependencies, and the fact that it's typically included by other dependencies already (e.g., `rand`), we'd like a way to enable `libm` purely for compatibility purposes, but still allow users to choose `std` as the floating point backend if they prefer it.

## Solution

- Add a new feature flag, `libm-fallback`, which includes `libm` as a dependency but only uses it if `std` is disabled.
- This feature is intended for libraries relying on Glam as a way to compile with default features disabled without denying the user the ability to choose their preferred backend.

## Code generation

- Modified code is not generated.

## Testing and linting

- CI


bitshifter: I'm a bit confused on how this works on the bevy side. Is it that you are adding the glam dependency with no default features but always use this libm-fallback feature flag in bevy?



Patch:
diff --git a/Cargo.toml b/Cargo.toml
index 4719b60..15d50c9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -43,6 +43,12 @@ core-simd = []
 
 bytecheck = ["rkyv/bytecheck"]
 
+# enables libm and prefers it over std
+libm = ["dep:libm"]
+
+# enables libm but will prefer std if available
+nostd-libm = ["dep:libm"]
+
 [dependencies]
 approx = { version = "0.5", optional = true, default-features = false }
 bytemuck = { version = "1.9", optional = true, default-features = false }
diff --git a/README.md b/README.md
index 6753834..4df7740 100644
--- a/README.md
+++ b/README.md
@@ -103,12 +103,27 @@ libm = ["glam/libm"]
 glam = { version = "0.30.0", default-features = false }
 ```
 
+Alternatively, you can use the `nostd-libm` feature.
+This will always include a `libm` dependency, but allows the user to still override it with `std` if they prefer.
+This will allow your crate to compile with default features disabled, instead of forcing the user to enable either `std` or `libm`.
+
+```toml
+[features]
+default = ["std"]
+
+std = ["glam/std"]
+libm = ["glam/libm"]
+
+[dependencies]
+glam = { version = "0.30.0", default-features = false, features = ["nostd-libm"] }
+```
+
 ### Optional features
 
 * [`approx`] - traits and macros for approximate float comparisons
 * [`bytemuck`] - for casting into slices of bytes
-* [`libm`] - uses `libm` math functions instead of `std`, required to compile
-  with `no_std`
+* [`libm`] - uses `libm` math functions instead of `std`
+* [`nostd-libm`] - uses `libm` math functions if `std` is not available
 * [`mint`] - for interoperating with other 3D math libraries
 * [`rand`] - implementations of `Distribution` trait for all `glam` types.
 * [`serde`] - implementations of `Serialize` and `Deserialize` for all `glam`
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/math.rs b/src/f32/math.rs
index 20b3be9..7e6ccfd 100644
--- a/src/f32/math.rs
+++ b/src/f32/math.rs
@@ -32,7 +32,7 @@ fn acos_approx_f32(v: f32) -> f32 {
     }
 }
 
-#[cfg(feature = "libm")]
+#[cfg(any(feature = "libm", all(feature = "nostd-libm", not(feature = "std"))))]
 mod libm_math {
     #[inline(always)]
     pub(crate) fn abs(f: f32) -> f32 {
@@ -236,7 +236,11 @@ mod std_math {
 
 // Used to reduce the number of compilation errors, in the event that no other
 // math backend is specified.
-#[cfg(all(not(feature = "libm"), not(feature = "std")))]
+#[cfg(all(
+    not(feature = "libm"),
+    not(feature = "std"),
+    not(feature = "nostd-libm")
+))]
 mod no_backend_math {
     pub(crate) fn abs(_: f32) -> f32 {
         unimplemented!()
@@ -311,11 +315,15 @@ mod no_backend_math {
     }
 }
 
-#[cfg(feature = "libm")]
+#[cfg(any(feature = "libm", all(feature = "nostd-libm", not(feature = "std"))))]
 pub(crate) use libm_math::*;
 
 #[cfg(all(not(feature = "libm"), feature = "std"))]
 pub(crate) use std_math::*;
 
-#[cfg(all(not(feature = "libm"), not(feature = "std")))]
+#[cfg(all(
+    not(feature = "libm"),
+    not(feature = "std"),
+    not(feature = "nostd-libm")
+))]
 pub(crate) use no_backend_math::*;
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/src/f64/math.rs b/src/f64/math.rs
index 1fb8c8f..b3e68fe 100644
--- a/src/f64/math.rs
+++ b/src/f64/math.rs
@@ -1,4 +1,4 @@
-#[cfg(feature = "libm")]
+#[cfg(any(feature = "libm", all(feature = "nostd-libm", not(feature = "std"))))]
 mod libm_math {
     #[inline(always)]
     pub(crate) fn abs(f: f64) -> f64 {
@@ -200,7 +200,11 @@ mod std_math {
 
 // Used to reduce the number of compilation errors, in the event that no other
 // math backend is specified.
-#[cfg(all(not(feature = "libm"), not(feature = "std")))]
+#[cfg(all(
+    not(feature = "libm"),
+    not(feature = "std"),
+    not(feature = "nostd-libm")
+))]
 mod no_backend_math {
     pub(crate) fn abs(_: f64) -> f64 {
         unimplemented!()
@@ -275,11 +279,15 @@ mod no_backend_math {
     }
 }
 
-#[cfg(feature = "libm")]
+#[cfg(any(feature = "libm", all(feature = "nostd-libm", not(feature = "std"))))]
 pub(crate) use libm_math::*;
 
 #[cfg(all(not(feature = "libm"), feature = "std"))]
 pub(crate) use std_math::*;
 
-#[cfg(all(not(feature = "libm"), not(feature = "std")))]
+#[cfg(all(
+    not(feature = "libm"),
+    not(feature = "std"),
+    not(feature = "nostd-libm")
+))]
 pub(crate) use no_backend_math::*;
diff --git a/src/lib.rs b/src/lib.rs
index 8fa10a7..eed9d38 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -229,7 +229,8 @@ and benchmarks.
 * `std` - the default feature, has no dependencies.
 * `approx` - traits and macros for approximate float comparisons
 * `bytemuck` - for casting into slices of bytes
-* `libm` - uses `libm` math functions instead of `std`, required to compile with `no_std`
+* `libm` - uses `libm` math functions instead of `std`
+* `nostd-libm` - uses `libm` math functions if `std` is not available
 * `mint` - for interoperating with other 3D math libraries
 * `rand` - implementations of `Distribution` trait for all `glam` types.
 * `rkyv` - implementations of `Archive`, `Serialize` and `Deserialize` for all
@@ -274,8 +275,14 @@ The minimum supported Rust version is `1.68.2`.
     feature(portable_simd)
 )]
 
-#[cfg(all(not(feature = "std"), not(feature = "libm")))]
-compile_error!("You must specify a math backend using either the `std` feature or `libm` feature");
+#[cfg(all(
+    not(feature = "std"),
+    not(feature = "libm"),
+    not(feature = "nostd-libm")
+))]
+compile_error!(
+    "You must specify a math backend. Consider enabling either `std`, `libm`, or `nostd-libm`."
+);
 
 #[macro_use]
 mod macros;
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);