From e320c37981e5b90456a25f625249da9a7ec61ad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Crozet?= Date: Thu, 17 Sep 2026 21:06:25 +0200 Subject: [PATCH 1/2] fix Rot2 lerp --- CHANGELOG.md | 5 +++++ src/pose2.rs | 10 ++++++++++ src/rot2.rs | 35 +++++++++++++++++++++++++++++++++-- 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b025c36..bf00fa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# Unreleased + +- Fix `Rot2::slerp` (and therefore `Pose2::lerp`) taking the long way around whenever the arc + crosses the `-pi`/`pi` cut of `Rot2::angle`. It now interpolates along the shortest arc. + # v0.3.0 - Update to `glam 0.33`, `simba 0.10`, and `nalgebra 0.35`. ([#5](https://github.com/dimforge/glamx/pull/5)) diff --git a/src/pose2.rs b/src/pose2.rs index 0c8c461..44c10ce 100644 --- a/src/pose2.rs +++ b/src/pose2.rs @@ -512,6 +512,16 @@ mod tests { assert_relative_eq!(p.translation.y, p2.translation.y, epsilon = 1e-6); } + #[test] + fn test_pose2_lerp_across_branch_cut() { + let p1 = Pose2::new(glam::Vec2::new(0.0, 0.0), 3.13); + let p2 = Pose2::new(glam::Vec2::new(2.0, 4.0), 3.15); + let mid = p1.lerp(&p2, 0.5); + assert_relative_eq!(mid.rotation.angle(), 3.14, epsilon = 1e-6); + assert_relative_eq!(mid.translation.x, 1.0, epsilon = 1e-6); + assert_relative_eq!(mid.translation.y, 2.0, epsilon = 1e-6); + } + #[cfg(feature = "f64")] #[test] fn test_dpose2_new() { diff --git a/src/rot2.rs b/src/rot2.rs index cd58076..5fdc807 100644 --- a/src/rot2.rs +++ b/src/rot2.rs @@ -203,10 +203,14 @@ macro_rules! impl_rot2 { } /// Spherical linear interpolation between two rotations. + /// + /// Interpolates along the shortest arc: the result is `self` at `t == 0` and `other` + /// at `t == 1`, whichever branch of `angle` the two rotations fall on. Interpolating + /// their angles instead would take the long way around whenever the arc crosses the + /// `-pi`/`pi` cut. #[inline] pub fn slerp(&self, other: &Self, t: $Real) -> Self { - let angle_diff = other.angle() - self.angle(); - Self::new(self.angle() + t * angle_diff) + *self * Self::new(self.angle_between(other) * t) } /// Raises this rotation to a power. @@ -504,6 +508,33 @@ mod tests { assert_relative_eq!(r1.angle_between(&r2), PI / 2.0, epsilon = 1e-6); } + #[test] + fn test_rot2_slerp() { + let r1 = Rot2::new(0.2); + let r2 = Rot2::new(1.0); + assert_relative_eq!(r1.slerp(&r2, 0.0).angle(), 0.2, epsilon = 1e-6); + assert_relative_eq!(r1.slerp(&r2, 1.0).angle(), 1.0, epsilon = 1e-6); + assert_relative_eq!(r1.slerp(&r2, 0.5).angle(), 0.6, epsilon = 1e-6); + assert!(r1.slerp(&r2, 0.5).is_normalized()); + } + + #[test] + fn test_rot2_slerp_shortest_arc() { + // The interpolation from 0 to 3pi/2 goes backwards through -3pi/4, not forwards. + let r1 = Rot2::new(0.0); + let r2 = Rot2::new(3.0 * PI / 2.0); + assert_relative_eq!(r1.slerp(&r2, 0.5).angle(), -PI / 4.0, epsilon = 1e-6); + } + + #[test] + fn test_rot2_slerp_across_branch_cut() { + // `angle` wraps to (-pi, pi], so these two are 0.02 rad apart, not 6.26. + let r1 = Rot2::new(3.13); + let r2 = Rot2::new(3.15); + assert_relative_eq!(r1.slerp(&r2, 0.5).angle(), 3.14, epsilon = 1e-6); + assert_relative_eq!(r1.slerp(&r2, 1.0).angle(), r2.angle(), epsilon = 1e-6); + } + #[cfg(feature = "f64")] #[test] fn test_drot2_new() { From 99588d0874fc4d1039601c04b433f00a9eb67996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Crozet?= Date: Thu, 17 Sep 2026 22:44:45 +0200 Subject: [PATCH 2/2] fix clippy --- Cargo.toml | 2 +- src/pose3.rs | 22 ++++++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b21e3c5..4d00e4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,7 +92,7 @@ isize = ["glam/isize"] usize = ["glam/usize"] [dependencies] -glam = { version = "0.33", default-features = false } +glam = { version = "0.33.7", default-features = false } simba = { version = "0.10", default-features = false } num-traits = { version = "0.2", default-features = false, features = ["libm"] } approx = { version = "0.5", default-features = false, optional = true } diff --git a/src/pose3.rs b/src/pose3.rs index bd1b684..cfab565 100644 --- a/src/pose3.rs +++ b/src/pose3.rs @@ -7,7 +7,7 @@ use core::ops::{Mul, MulAssign}; /// Macro to generate a 3D pose type for a specific scalar type. macro_rules! impl_pose3 { - ($Pose3:ident, $Rot3:ident, $Real:ty, $Vec3:ty, $Mat4: ty $(, $Padding: ty, $bytemuck: ident)?) => { + ($Pose3:ident, $Rot3:ident, $Real:ty, $Vec3:ty, $Mat4: ty, $camera: ident $(, $Padding: ty, $bytemuck: ident)?) => { #[doc = concat!("A 3D pose (rotation + translation), representing a rigid body transformation (", stringify!($Real), " precision).")] #[derive(Copy, Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -225,7 +225,7 @@ macro_rules! impl_pose3 { /// requirement of this parameter is to not be collinear to target - eye. #[inline] pub fn look_at_rh(eye: $Vec3, target: $Vec3, up: $Vec3) -> $Pose3 { - let rotation = <$Rot3>::look_at_rh(eye.into(), target.into(), up.into()); + let rotation = glam::$camera::rh::view::look_at_quat(eye.into(), target.into(), up.into()); let translation = rotation * (-eye); $Pose3 { @@ -248,7 +248,8 @@ macro_rules! impl_pose3 { #[inline] pub fn face_towards(eye: $Vec3, target: $Vec3, up: $Vec3) -> $Pose3 { $Pose3 { - rotation: <$Rot3>::look_at_lh(eye.into(), target.into(), up.into()).inverse(), + rotation: glam::$camera::lh::view::look_at_quat(eye.into(), target.into(), up.into()) + .inverse(), translation: eye, $(padding: 0 as $Padding,)* } @@ -413,10 +414,19 @@ macro_rules! impl_pose3 { }; } -impl_pose3!(Pose3, Rot3, f32, glam::Vec3, glam::Mat4, u32, bytemuck); -impl_pose3!(Pose3A, Rot3, f32, glam::Vec3A, glam::Mat4); +impl_pose3!( + Pose3, + Rot3, + f32, + glam::Vec3, + glam::Mat4, + camera, + u32, + bytemuck +); +impl_pose3!(Pose3A, Rot3, f32, glam::Vec3A, glam::Mat4, camera); #[cfg(feature = "f64")] -impl_pose3!(DPose3, DRot3, f64, glam::DVec3, glam::DMat4); +impl_pose3!(DPose3, DRot3, f64, glam::DVec3, glam::DMat4, dcamera); // f32 <-> f64 conversions #[cfg(feature = "f64")]