Repository: bitshifter/glam-rs
Pull request #163 (2021-04-12 18:48:52)

add euler rotation sequence conversion
Rust-port to convert quaternions to Euler rotation sequences.
Equations from: http://bediyap.com/programming/convert-quaternion-to-euler-rotations/

The two-axis Euler rotations (e.g. ZYZ) are not fully tested have to be treated with caution.
This is due how the test-implementation works and I haven't found a better generic way to test it.

### Usage
```rust
let q = Quat::from_euler(EulerRot::YXZi, 0.3, 0.1, 0.0);
let (y, x, z) = q.to_euler(EulerRot::YXZi);  // the "i" indicates intrinsic
```

```rust
let q = DQuat::from_euler(EulerRot::YXZi, 0.3, 0.1, 0.0);
let (y, x, z) = q.to_euler(EulerRot::YXZi);  // the "i" indicates intrinsic
```

mickare: The testing takes some time, to speed up things; the skipped tested angles could be increased in the test part at:
```rust
if u % 2 != 0 {
  continue;
}
```

Patch:
diff --git a/Cargo.toml b/Cargo.toml
index 6a6c998..4820952 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -45,7 +45,7 @@ serde = { version = "1.0", optional = true, features = ["derive"] }
 spirv-std = { version = "0.4.0-alpha.4", optional = true }
 
 [dev-dependencies]
-criterion = "0.3"
+criterion = { version = "0.3", features = ["html_reports"] }
 # rand_xoshiro is required for tests if rand is enabled
 rand_xoshiro = "0.6"
 serde_json = "1.0"
diff --git a/benches/mat2.rs b/benches/mat2.rs
index a5b76fa..a66ab24 100644
--- a/benches/mat2.rs
+++ b/benches/mat2.rs
@@ -7,6 +7,14 @@ use criterion::{criterion_group, criterion_main, Criterion};
 use std::ops::Mul;
 use support::*;
 
+bench_binop!(
+    mat2_mul_vec2,
+    "mat2 mul vec2",
+    op => mul,
+    from1 => random_mat2,
+    from2 => random_vec2
+);
+
 bench_unop!(
     mat2_transpose,
     "mat2 transpose",
@@ -27,7 +35,7 @@ criterion_group!(
     mat2_transpose,
     mat2_determinant,
     mat2_inverse,
-    // mat2_mul_op_mat2,
+    mat2_mul_vec2,
     mat2_mul_mat2,
 );
 
diff --git a/benches/mat3.rs b/benches/mat3.rs
index e51c7e9..d0faad2 100644
--- a/benches/mat3.rs
+++ b/benches/mat3.rs
@@ -24,13 +24,49 @@ bench_unop!(mat3_inverse, "mat3 inverse", op => inverse, from => random_mat3);
 bench_binop!(mat3_mul_mat3, "mat3 mul mat3", op => mul, from => random_mat3);
 bench_from_ypr!(mat3_from_ypr, "mat3 from ypr", ty => Mat3);
 
+bench_binop!(
+    mat3_mul_vec3,
+    "mat3 mul vec3",
+    op => mul,
+    from1 => random_mat3,
+    from2 => random_vec3
+);
+
+bench_binop!(
+    mat3_mul_vec3a,
+    "mat3 mul vec3a",
+    op => mul,
+    from1 => random_mat3,
+    from2 => random_vec3a
+);
+
+bench_binop!(
+    mat3_transform_point2,
+    "mat3 transform point2",
+    op => transform_point2,
+    from1 => random_srt_mat3,
+    from2 => random_vec2
+);
+
+bench_binop!(
+    mat3_transform_vector2,
+    "mat3 transform vector2",
+    op => transform_vector2,
+    from1 => random_srt_mat3,
+    from2 => random_vec2
+);
+
 criterion_group!(
     benches,
     mat3_transpose,
     mat3_determinant,
     mat3_inverse,
+    mat3_mul_vec3,
+    mat3_mul_vec3a,
     mat3_mul_mat3,
     mat3_from_ypr,
+    mat3_transform_vector2,
+    mat3_transform_point2,
 );
 
 criterion_main!(benches);
diff --git a/benches/quat.rs b/benches/quat.rs
index aa28158..507c1c3 100644
--- a/benches/quat.rs
+++ b/benches/quat.rs
@@ -6,7 +6,7 @@ mod support;
 use criterion::{criterion_group, criterion_main, Criterion};
 use glam::Quat;
 use std::ops::Mul;
-use support::{random_f32, random_quat, random_radians};
+use support::*;
 
 bench_unop!(
     quat_conjugate,
@@ -15,6 +15,22 @@ bench_unop!(
     from => random_quat
 );
 
+bench_binop!(
+    quat_mul_vec3,
+    "quat mul vec3",
+    op => mul,
+    from1 => random_quat,
+    from2 => random_vec3
+);
+
+bench_binop!(
+    quat_mul_vec3a,
+    "quat mul vec3a",
+    op => mul,
+    from1 => random_quat,
+    from2 => random_vec3a
+);
+
 bench_binop!(
     quat_mul_quat,
     "quat mul quat",
@@ -56,6 +72,8 @@ criterion_group!(
     quat_lerp,
     quat_slerp,
     quat_mul_quat,
+    quat_mul_vec3,
+    quat_mul_vec3a,
     quat_from_ypr
 );
 
diff --git a/benches/vec2.rs b/benches/vec2.rs
index d90d8aa..66f3f02 100644
--- a/benches/vec2.rs
+++ b/benches/vec2.rs
@@ -5,8 +5,7 @@ mod support;
 
 use criterion::{criterion_group, criterion_main, Criterion};
 use glam::Vec2;
-use std::ops::Mul;
-use support::{random_mat2, random_srt_mat3, random_vec2};
+use support::*;
 
 euler!(
     vec2_euler,
@@ -16,30 +15,6 @@ euler!(
     zero => Vec2::ZERO,
     rand => random_vec2);
 
-bench_binop!(
-    mat2_mul_vec2,
-    "mat2 mul vec2",
-    op => mul,
-    from1 => random_mat2,
-    from2 => random_vec2
-);
-
-bench_binop!(
-    mat3_transform_point2,
-    "mat3 transform point2",
-    op => transform_point2,
-    from1 => random_srt_mat3,
-    from2 => random_vec2
-);
-
-bench_binop!(
-    mat3_transform_vector2,
-    "mat3 transform vector2",
-    op => transform_vector2,
-    from1 => random_srt_mat3,
-    from2 => random_vec2
-);
-
 bench_binop!(
     vec2_angle_between,
     "vec2 angle_between",
@@ -56,14 +31,6 @@ bench_select!(
     from => random_vec2
 );
 
-criterion_group!(
-    benches,
-    mat2_mul_vec2,
-    mat3_transform_point2,
-    mat3_transform_vector2,
-    vec2_euler,
-    vec2_select,
-    vec2_angle_between
-);
+criterion_group!(benches, vec2_euler, vec2_select, vec2_angle_between);
 
 criterion_main!(benches);
diff --git a/benches/vec3.rs b/benches/vec3.rs
index 5f7366a..a8d8b6a 100644
--- a/benches/vec3.rs
+++ b/benches/vec3.rs
@@ -5,30 +5,14 @@ mod support;
 
 use criterion::{criterion_group, criterion_main, Criterion};
 use glam::Vec3;
-use std::ops::{Add, Mul};
-use support::{random_mat3, random_quat, random_vec3};
+use std::ops::Mul;
+use support::*;
 
 bench_binop!(
-    vec3_add_vec3,
-    "vec3 add vec3",
-    op => add,
-    from1 => random_vec3,
-    from2 => random_vec3
-);
-
-bench_binop!(
-    quat_mul_vec3,
-    "quat mul vec3",
+    vec3_mul_vec3,
+    "vec3 mul vec3",
     op => mul,
-    from1 => random_quat,
-    from2 => random_vec3
-);
-
-bench_binop!(
-    mat3_mul_vec3,
-    "mat3 mul vec3",
-    op => mul,
-    from1 => random_mat3,
+    from1 => random_vec3,
     from2 => random_vec3
 );
 
@@ -167,9 +151,7 @@ bench_select!(
 
 criterion_group!(
     benches,
-    mat3_mul_vec3,
-    quat_mul_vec3,
-    vec3_add_vec3,
+    vec3_mul_vec3,
     vec3_angle_between,
     vec3_normalize_bench,
     vec3_normalize_or_zero_bench,
diff --git a/benches/vec3a.rs b/benches/vec3a.rs
index 74e4505..b23e511 100644
--- a/benches/vec3a.rs
+++ b/benches/vec3a.rs
@@ -6,21 +6,13 @@ mod support;
 use criterion::{criterion_group, criterion_main, Criterion};
 use glam::{Vec3, Vec3A};
 use std::ops::Mul;
-use support::{random_mat3, random_quat, random_vec3a};
+use support::*;
 
 bench_binop!(
-    quat_mul_vec3a,
-    "quat mul vec3a",
+    vec3a_mul_vec3a,
+    "vec3a mul vec3a",
     op => mul,
-    from1 => random_quat,
-    from2 => random_vec3a
-);
-
-bench_binop!(
-    mat3_mul_vec3a,
-    "mat3 mul vec3a",
-    op => mul,
-    from1 => random_mat3,
+    from1 => random_vec3a,
     from2 => random_vec3a
 );
 
@@ -105,8 +97,7 @@ bench_select!(
 
 criterion_group!(
     benches,
-    mat3_mul_vec3a,
-    quat_mul_vec3a,
+    vec3a_mul_vec3a,
     vec3a_angle_between,
     vec3a_euler,
     vec3a_select,
diff --git a/benches/vec4.rs b/benches/vec4.rs
index d936c01..ec926d8 100644
--- a/benches/vec4.rs
+++ b/benches/vec4.rs
@@ -5,8 +5,17 @@ mod support;
 
 use criterion::{criterion_group, criterion_main, Criterion};
 use glam::Vec4;
+use std::ops::Mul;
 use support::random_vec4;
 
+bench_binop!(
+    vec4_mul_vec4,
+    "vec4 mul vec4",
+    op => mul,
+    from1 => random_vec4,
+    from2 => random_vec4
+);
+
 bench_select!(
     vec4_select,
     "vec4 select",
@@ -15,6 +24,6 @@ bench_select!(
     from => random_vec4
 );
 
-criterion_group!(benches, vec4_select);
+criterion_group!(benches, vec4_mul_vec4, vec4_select);
 
 criterion_main!(benches);
diff --git a/src/affine3d.rs b/src/affine3d.rs
index 632a540..d84e42f 100644
--- a/src/affine3d.rs
+++ b/src/affine3d.rs
@@ -423,8 +423,6 @@ impl Affine3D {
     }
 
     /// Return the inverse of this transform.
-    ///
-    /// The result of this is only valid if [`Self::is_invertible`] is true.
     pub fn inverse(&self) -> Self {
         // invert 3x3 matrix:
         let x_row = self.y_col.cross(self.z_col);
diff --git a/src/core/sse2/matrix.rs b/src/core/sse2/matrix.rs
index dc7c23f..838d0c1 100644
--- a/src/core/sse2/matrix.rs
+++ b/src/core/sse2/matrix.rs
@@ -395,12 +395,21 @@ impl FloatMatrix4x4<f32, __m128> for Vector4x4<__m128> {
 
     #[inline(always)]
     fn transform_point3(&self, other: XYZ<f32>) -> XYZ<f32> {
-        self.transform_float4_as_point3(other.into()).into()
+        self.x_axis
+            .mul_scalar(other.x)
+            .add(self.y_axis.mul_scalar(other.y))
+            .add(self.z_axis.mul_scalar(other.z))
+            .add(self.w_axis)
+            .into()
     }
 
     #[inline(always)]
     fn transform_vector3(&self, other: XYZ<f32>) -> XYZ<f32> {
-        self.transform_float4_as_vector3(other.into()).into()
+        self.x_axis
+            .mul_scalar(other.x)
+            .add(self.y_axis.mul_scalar(other.y))
+            .add(self.z_axis.mul_scalar(other.z))
+            .into()
     }
 
     #[inline]
diff --git a/src/core/traits/scalar.rs b/src/core/traits/scalar.rs
index 7ffea98..fc1a399 100644
--- a/src/core/traits/scalar.rs
+++ b/src/core/traits/scalar.rs
@@ -19,6 +19,7 @@ pub trait Signed: Sized + Num + core::ops::Neg<Output = Self> {
 
 #[cfg(not(feature = "libm"))]
 pub trait Float: Num + Copy + core::ops::Neg<Output = Self> {
+    fn asin(self) -> Self;
     fn acos(self) -> Self;
     fn ceil(self) -> Self;
     fn exp(self) -> Self;
@@ -65,6 +66,10 @@ macro_rules! impl_float_trait {
         impl_signed_trait!($t);
 
         impl Float for $t {
+            #[inline(always)]
+            fn asin(self) -> Self {
+                $t::asin(self)
+            }
             #[inline(always)]
             fn acos(self) -> Self {
                 $t::acos(self)
diff --git a/src/euler.rs b/src/euler.rs
new file mode 100644
index 0000000..0ead147
--- /dev/null
+++ b/src/euler.rs
@@ -0,0 +1,271 @@
+/*
+Conversion from quaternions to Euler rotation sequences.
+
+From: http://bediyap.com/programming/convert-quaternion-to-euler-rotations/
+*/
+
+use super::quat::{DQuat, Quat};
+use crate::core::traits::scalar::*;
+
+/// Euler rotation sequences.
+///
+/// The angles are applied starting from the right.
+/// E.g. XYZ will first apply the z-axis rotation.
+///
+/// YXZ can be used for yaw (y-axis), pitch (x-axis), role (z-axis).
+///
+/// The two-axis rotations (e.g. ZYZ) are not fully tested and have to be treated with caution.
+#[derive(Debug, Clone, Copy)]
+pub enum EulerRot {
+    /// Intrinsic three-axis rotation ZYX
+    ZYXi,
+    /// Intrinsic three-axis rotation ZXY
+    ZXYi,
+    /// Intrinsic three-axis rotation YXZ
+    YXZi,
+    /// Intrinsic three-axis rotation YZX
+    YZXi,
+    /// Intrinsic three-axis rotation XYZ
+    XYZi,
+    /// Intrinsic three-axis rotation XZY
+    XZYi,
+
+    /// Intrinsic two-axis rotation ZYZ
+    #[deprecated(note = "Untested! Use at own risk!")]
+    ZYZi,
+    /// Intrinsic two-axis rotation ZXZ
+    #[deprecated(note = "Untested! Use at own risk!")]
+    ZXZi,
+    /// Intrinsic two-axis rotation YXY
+    #[deprecated(note = "Untested! Use at own risk!")]
+    YXYi,
+    /// Intrinsic two-axis rotation YZY
+    #[deprecated(note = "Untested! Use at own risk!")]
+    YZYi,
+    /// Intrinsic two-axis rotation XYX
+    #[deprecated(note = "Untested! Use at own risk!")]
+    XYXi,
+    /// Intrinsic two-axis rotation XZX
+    #[deprecated(note = "Untested! Use at own risk!")]
+    XZXi,
+}
+
+impl Default for EulerRot {
+    /// Default YXZi as yaw (y-axis), pitch (x-axis), role (z-axis).
+    fn default() -> Self {
+        Self::YXZi
+    }
+}
+
+/// Conversion from quaternion to euler angles.
+pub trait EulerFromQuaternion<Q: Copy>: Sized + Copy {
+    type Output: FloatEx;
+    /// Compute the angle of the first axis (X-x-x)
+    fn first(self, q: Q) -> Self::Output;
+    /// Compute then angle of the second axis (x-X-x)
+    fn second(self, q: Q) -> Self::Output;
+    /// Compute then angle of the third axis (x-x-X)
+    fn third(self, q: Q) -> Self::Output;
+
+    /// Compute all angles of a rotation in the notation order
+    fn convert_quat(self, q: Q) -> (Self::Output, Self::Output, Self::Output) {
+        (self.first(q), self.second(q), self.third(q))
+    }
+}
+
+/// Conversion from euler angles to quaternion.
+pub trait EulerToQuaternion<T>: Copy {
+    type Output;
+    /// Create the rotation quaternion for the three angles of this euler rotation sequence.
+    fn new_quat(self, u: T, v: T, w: T) -> Self::Output;
+}
+
+/// Helper, until std::f32::clamp is stable.
+fn clamp<T: PartialOrd>(value: T, min: T, max: T) -> T {
+    assert!(min <= max);
+    if value < min {
+        min
+    } else if value > max {
+        max
+    } else {
+        value
+    }
+}
+
+/// Adds a atan2 that handles the negative zero case.
+/// Basically forces positive zero in the x-argument for atan2.
+pub trait Atan2Fixed<T = Self> {
+    fn atan2_fixed(self, other: T) -> T;
+}
+impl Atan2Fixed for f32 {
+    fn atan2_fixed(self, other: f32) -> f32 {
+        self.atan2(if other == 0.0f32 { 0.0f32 } else { other })
+    }
+}
+impl Atan2Fixed for f64 {
+    fn atan2_fixed(self, other: f64) -> f64 {
+        self.atan2(if other == 0.0f64 { 0.0f64 } else { other })
+    }
+}
+
+macro_rules! impl_from_quat {
+    ($t:ty, $quat:ident) => {
+        impl EulerFromQuaternion<$quat> for EulerRot {
+            type Output = $t;
+            fn first(self, q: $quat) -> $t {
+                use EulerRot::*;
+                match self {
+                    ZYXi => (Self::Output::TWO * (q.x * q.y + q.w * q.z))
+                        .atan2(q.w * q.w + q.x * q.x - q.y * q.y - q.z * q.z),
+                    ZXYi => (-Self::Output::TWO * (q.x * q.y - q.w * q.z))
+                        .atan2(q.w * q.w - q.x * q.x + q.y * q.y - q.z * q.z),
+                    YXZi => (Self::Output::TWO * (q.x * q.z + q.w * q.y))
+                        .atan2(q.w * q.w - q.x * q.x - q.y * q.y + q.z * q.z),
+                    YZXi => (-Self::Output::TWO * (q.x * q.z - q.w * q.y))
+                        .atan2(q.w * q.w + q.x * q.x - q.y * q.y - q.z * q.z),
+                    XYZi => (-Self::Output::TWO * (q.y * q.z - q.w * q.x))
+                        .atan2(q.w * q.w - q.x * q.x - q.y * q.y + q.z * q.z),
+                    XZYi => (Self::Output::TWO * (q.y * q.z + q.w * q.x))
+                        .atan2(q.w * q.w - q.x * q.x + q.y * q.y - q.z * q.z),
+                    #[allow(deprecated)]
+                    ZYZi => (Self::Output::TWO * (q.y * q.z + q.w * q.x))
+                        .atan2_fixed(-Self::Output::TWO * (q.x * q.z - q.w * q.y)),
+                    #[allow(deprecated)]
+                    ZXZi => (Self::Output::TWO * (q.x * q.z - q.w * q.y))
+                        .atan2_fixed(Self::Output::TWO * (q.y * q.z + q.w * q.x)),
+                    #[allow(deprecated)]
+                    YXYi => (Self::Output::TWO * (q.x * q.y + q.w * q.z))
+                        .atan2_fixed(-Self::Output::TWO * (q.y * q.z - q.w * q.x)),
+                    #[allow(deprecated)]
+                    YZYi => (Self::Output::TWO * (q.y * q.z - q.w * q.x))
+                        .atan2_fixed(Self::Output::TWO * (q.x * q.y + q.w * q.z)),
+                    #[allow(deprecated)]
+                    XYXi => (Self::Output::TWO * (q.x * q.y - q.w * q.z))
+                        .atan2_fixed(Self::Output::TWO * (q.x * q.z + q.w * q.y)),
+                    #[allow(deprecated)]
+                    XZXi => (Self::Output::TWO * (q.x * q.z + q.w * q.y))
+                        .atan2_fixed(-Self::Output::TWO * (q.x * q.y - q.w * q.z)),
+                }
+            }
+
+            fn second(self, q: $quat) -> $t {
+                use EulerRot::*;
+
+                /// Clamp number to range [-1,1](-1,1) for asin() and acos(), else NaN is possible.
+                #[inline(always)]
+                fn arc_clamp<T: FloatEx>(val: T) -> T {
+                    clamp(val, T::NEG_ONE, T::ONE)
+                }
+                match self {
+                    ZYXi => arc_clamp(-Self::Output::TWO * (q.x * q.z - q.w * q.y)).asin(),
+                    ZXYi => arc_clamp(Self::Output::TWO * (q.y * q.z + q.w * q.x)).asin(),
+                    YXZi => arc_clamp(-Self::Output::TWO * (q.y * q.z - q.w * q.x)).asin(),
+                    YZXi => arc_clamp(Self::Output::TWO * (q.x * q.y + q.w * q.z)).asin(),
+                    XYZi => arc_clamp(Self::Output::TWO * (q.x * q.z + q.w * q.y)).asin(),
+                    XZYi => arc_clamp(-Self::Output::TWO * (q.x * q.y - q.w * q.z)).asin(),
+                    #[allow(deprecated)]
+                    ZYZi => arc_clamp(q.w * q.w - q.x * q.x - q.y * q.y + q.z * q.z).acos(),
+                    #[allow(deprecated)]
+                    ZXZi => arc_clamp(q.w * q.w - q.x * q.x - q.y * q.y + q.z * q.z).acos(),
+                    #[allow(deprecated)]
+                    YXYi => arc_clamp(q.w * q.w - q.x * q.x + q.y * q.y - q.z * q.z).acos(),
+                    #[allow(deprecated)]
+                    YZYi => arc_clamp(q.w * q.w - q.x * q.x + q.y * q.y - q.z * q.z).acos(),
+                    #[allow(deprecated)]
+                    XYXi => arc_clamp(q.w * q.w + q.x * q.x - q.y * q.y - q.z * q.z).acos(),
+                    #[allow(deprecated)]
+                    XZXi => arc_clamp(q.w * q.w + q.x * q.x - q.y * q.y - q.z * q.z).acos(),
+                }
+            }
+
+            fn third(self, q: $quat) -> $t {
+                use EulerRot::*;
+                #[allow(deprecated)]
+                match self {
+                    ZYXi => (Self::Output::TWO * (q.y * q.z + q.w * q.x))
+                        .atan2(q.w * q.w - q.x * q.x - q.y * q.y + q.z * q.z),
+                    ZXYi => (-Self::Output::TWO * (q.x * q.z - q.w * q.y))
+                        .atan2(q.w * q.w - q.x * q.x - q.y * q.y + q.z * q.z),
+                    YXZi => (Self::Output::TWO * (q.x * q.y + q.w * q.z))
+                        .atan2(q.w * q.w - q.x * q.x + q.y * q.y - q.z * q.z),
+                    YZXi => (-Self::Output::TWO * (q.y * q.z - q.w * q.x))
+                        .atan2(q.w * q.w - q.x * q.x + q.y * q.y - q.z * q.z),
+                    XYZi => (-Self::Output::TWO * (q.x * q.y - q.w * q.z))
+                        .atan2(q.w * q.w + q.x * q.x - q.y * q.y - q.z * q.z),
+                    XZYi => (Self::Output::TWO * (q.x * q.z + q.w * q.y))
+                        .atan2(q.w * q.w + q.x * q.x - q.y * q.y - q.z * q.z),
+                    #[allow(deprecated)]
+                    ZYZi => (Self::Output::TWO * (q.y * q.z - q.w * q.x))
+                        .atan2_fixed(Self::Output::TWO * (q.x * q.z + q.w * q.y)),
+                    #[allow(deprecated)]
+                    ZXZi => (Self::Output::TWO * (q.x * q.z + q.w * q.y))
+                        .atan2_fixed(-Self::Output::TWO * (q.y * q.z - q.w * q.x)),
+                    #[allow(deprecated)]
+                    YXYi => (Self::Output::TWO * (q.x * q.y - q.w * q.z))
+                        .atan2_fixed(Self::Output::TWO * (q.y * q.z + q.w * q.x)),
+                    #[allow(deprecated)]
+                    YZYi => (Self::Output::TWO * (q.y * q.z + q.w * q.x))
+                        .atan2_fixed(-Self::Output::TWO * (q.x * q.y - q.w * q.z)),
+                    #[allow(deprecated)]
+                    XYXi => (Self::Output::TWO * (q.x * q.y + q.w * q.z))
+                        .atan2_fixed(-Self::Output::TWO * (q.x * q.z - q.w * q.y)),
+                    #[allow(deprecated)]
+                    XZXi => (Self::Output::TWO * (q.x * q.z - q.w * q.y))
+                        .atan2_fixed(Self::Output::TWO * (q.x * q.y + q.w * q.z)),
+                }
+            }
+        }
+        // End - impl EulerFromQuaternion
+    };
+}
+
+macro_rules! impl_to_quat {
+    ($t:ty, $quat:ident) => {
+        impl EulerToQuaternion<$t> for EulerRot {
+            type Output = $quat;
+            #[inline(always)]
+            fn new_quat(self, u: $t, v: $t, w: $t) -> $quat {
+                use EulerRot::*;
+                #[inline(always)]
+                fn rot_x(a: $t) -> $quat {
+                    $quat::from_rotation_x(a)
+                }
+                #[inline(always)]
+                fn rot_y(a: $t) -> $quat {
+                    $quat::from_rotation_y(a)
+                }
+                #[inline(always)]
+                fn rot_z(a: $t) -> $quat {
+                    $quat::from_rotation_z(a)
+                }
+                match self {
+                    ZYXi => rot_z(u) * rot_y(v) * rot_x(w),
+                    ZXYi => rot_z(u) * rot_x(v) * rot_y(w),
+                    YXZi => rot_y(u) * rot_x(v) * rot_z(w),
+                    YZXi => rot_y(u) * rot_z(v) * rot_x(w),
+                    XYZi => rot_x(u) * rot_y(v) * rot_z(w),
+                    XZYi => rot_x(u) * rot_z(v) * rot_y(w),
+                    #[allow(deprecated)]
+                    ZYZi => rot_z(u) * rot_y(v) * rot_z(w),
+                    #[allow(deprecated)]
+                    ZXZi => rot_z(u) * rot_x(v) * rot_z(w),
+                    #[allow(deprecated)]
+                    YXYi => rot_y(u) * rot_x(v) * rot_y(w),
+                    #[allow(deprecated)]
+                    YZYi => rot_y(u) * rot_z(v) * rot_y(w),
+                    #[allow(deprecated)]
+                    XYXi => rot_x(u) * rot_y(v) * rot_x(w),
+                    #[allow(deprecated)]
+                    XZXi => rot_x(u) * rot_z(v) * rot_x(w),
+                }
+                .normalize()
+            }
+        }
+        // End - impl EulerToQuaternion
+    };
+}
+
+impl_from_quat!(f32, Quat);
+impl_from_quat!(f64, DQuat);
+impl_to_quat!(f32, Quat);
+impl_to_quat!(f64, DQuat);
diff --git a/src/lib.rs b/src/lib.rs
index 20bdf14..6d9aba9 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -198,6 +198,7 @@ pub mod cast;
 
 mod affine3d;
 mod core;
+mod euler;
 mod mat2;
 mod mat3;
 mod mat4;
@@ -271,3 +272,6 @@ pub use self::u32::*;
 pub mod swizzles;
 
 pub use self::swizzles::{Vec2Swizzles, Vec3Swizzles, Vec4Swizzles};
+
+/** Rotation Helper */
+pub use euler::EulerRot;
diff --git a/src/mat3.rs b/src/mat3.rs
index d49aa4d..bfceda0 100644
--- a/src/mat3.rs
+++ b/src/mat3.rs
@@ -2,7 +2,7 @@ use crate::core::{
     storage::{Vector3x3, XYZ},
     traits::matrix::{FloatMatrix3x3, Matrix3x3, MatrixConst},
 };
-use crate::{DMat4, DQuat, DVec2, DVec3, Mat4, Quat, Vec2, Vec3, Vec3A, Vec3Swizzles};
+use crate::{DMat2, DMat4, DQuat, DVec2, DVec3, Mat2, Mat4, Quat, Vec2, Vec3, Vec3A, Vec3Swizzles};
 #[cfg(not(target_arch = "spirv"))]
 use core::fmt;
 use core::{
@@ -44,7 +44,7 @@ macro_rules! define_mat3_struct {
 }
 
 macro_rules! impl_mat3_methods {
-    ($t:ty, $vec3: ident, $vec2:ident, $quat:ident, $inner:ident) => {
+    ($t:ty, $vec3: ident, $vec2:ident, $quat:ident, $mat2:ident, $inner:ident) => {
         /// A 3x3 matrix with all elements set to `0.0`.
         pub const ZERO: Self = Self($inner::ZERO);
 
@@ -331,7 +331,7 @@ macro_rules! impl_mat3_methods {
         /// This method assumes that `self` contains a valid affine transform.
         #[inline(always)]
         pub fn transform_point2(&self, other: $vec2) -> $vec2 {
-            self.transform_point2_as_vec3a(other)
+            $mat2::from_cols(self.x_axis.into(), self.y_axis.into()) * other + self.z_axis.into()
         }
 
         /// Rotates the given 2D vector.
@@ -341,7 +341,7 @@ macro_rules! impl_mat3_methods {
         /// This method assumes that `self` contains a valid affine transform.
         #[inline(always)]
         pub fn transform_vector2(&self, other: $vec2) -> $vec2 {
-            self.transform_vector2_as_vec3a(other)
+            $mat2::from_cols(self.x_axis.into(), self.y_axis.into()) * other
         }
 
         /// Returns true if the absolute difference of all elements between `self` and `other`
@@ -518,7 +518,7 @@ type InnerF32 = Vector3x3<XYZ<f32>>;
 define_mat3_struct!(Mat3, InnerF32);
 
 impl Mat3 {
-    impl_mat3_methods!(f32, Vec3, Vec2, Quat, InnerF32);
+    impl_mat3_methods!(f32, Vec3, Vec2, Quat, Mat2, InnerF32);
 
     /// Transforms a `Vec3A`.
     #[inline]
@@ -535,21 +535,6 @@ impl Mat3 {
         Vec3::from(self.mul_vec3a(Vec3A::from(other)))
     }
 
-    #[inline]
-    fn transform_point2_as_vec3a(&self, other: Vec2) -> Vec2 {
-        let mut res = Vec3A::from(self.x_axis).mul(Vec3A::splat(other.x));
-        res = Vec3A::from(self.y_axis).mul_add(Vec3A::splat(other.y), res);
-        res = Vec3A::from(self.z_axis).add(res);
-        res.xy()
-    }
-
-    #[inline]
-    fn transform_vector2_as_vec3a(&self, other: Vec2) -> Vec2 {
-        let mut res = Vec3A::from(self.x_axis).mul(Vec3A::splat(other.x));
-        res = Vec3A::from(self.y_axis).mul_add(Vec3A::splat(other.y), res);
-        res.xy()
-    }
-
     #[inline(always)]
     pub fn as_f64(&self) -> DMat3 {
         DMat3::from_cols(
@@ -573,23 +558,13 @@ type InnerF64 = Vector3x3<XYZ<f64>>;
 define_mat3_struct!(DMat3, InnerF64);
 
 impl DMat3 {
-    impl_mat3_methods!(f64, DVec3, DVec2, DQuat, InnerF64);
+    impl_mat3_methods!(f64, DVec3, DVec2, DQuat, DMat2, InnerF64);
 
     #[inline(always)]
     fn mul_vec3_as_vec3a(&self, other: DVec3) -> DVec3 {
         DVec3(self.0.mul_vector(other.0))
     }
 
-    #[inline(always)]
-    fn transform_point2_as_vec3a(&self, other: DVec2) -> DVec2 {
-        DVec2(self.0.transform_point2(other.0))
-    }
-
-    #[inline(always)]
-    fn transform_vector2_as_vec3a(&self, other: DVec2) -> DVec2 {
-        DVec2(self.0.transform_vector2(other.0))
-    }
-
     #[inline(always)]
     pub fn as_f32(&self) -> Mat3 {
         Mat3::from_cols(
diff --git a/src/quat.rs b/src/quat.rs
index e9087be..76babd8 100644
--- a/src/quat.rs
+++ b/src/quat.rs
@@ -2,6 +2,7 @@ use crate::core::traits::{
     quaternion::Quaternion,
     vector::{FloatVector4, MaskVector4, Vector, Vector4, Vector4Const},
 };
+use crate::euler::{EulerFromQuaternion, EulerRot, EulerToQuaternion};
 use crate::{DMat3, DMat4, DVec3, DVec4};
 use crate::{Mat3, Mat4, Vec3, Vec3A, Vec4};
 
@@ -126,6 +127,12 @@ macro_rules! impl_quat_methods {
             Self($inner::from_rotation_ypr(yaw, pitch, roll))
         }
 
+        #[inline(always)]
+        /// Create a quaternion from the given euler rotation sequence and the angles in radians.
+        pub fn from_euler(euler: EulerRot, a: $t, b: $t, c: $t) -> Self {
+            euler.new_quat(a, b, c)
+        }
+
         /// Creates a quaternion from a 3x3 rotation matrix.
         #[inline]
         pub fn from_rotation_mat3(mat: &$mat3) -> Self {
@@ -208,6 +215,12 @@ macro_rules! impl_quat_methods {
             $vec3(axis) * angle
         }
 
+        /// Returns the rotation angles for the given euler rotation sequence.
+        #[inline(always)]
+        pub fn to_euler(self, euler: EulerRot) -> ($t, $t, $t) {
+            euler.convert_quat(self)
+        }
+
         /// Returns the quaternion conjugate of `self`. For a unit quaternion the
         /// conjugate is also the inverse.
         #[inline(always)]


Test patch:
diff --git a/tests/affine3d.rs b/tests/affine3d.rs
index c24406f..b979804 100644
--- a/tests/affine3d.rs
+++ b/tests/affine3d.rs
@@ -2,7 +2,7 @@
 mod support;
 
 macro_rules! impl_affine3d_tests {
-    ($t:ident, $affine3d:ident, $quat:ident, $vec4:ident, $vec3:ident) => {
+    ($t:ident, $affine3d:ident, $quat:ident, $vec3:ident) => {
         use core::$t::NAN;
         use core::$t::NEG_INFINITY;
 
@@ -235,7 +235,7 @@ macro_rules! impl_affine3d_tests {
 
 mod affine3d {
     use super::support::deg;
-    use glam::{Affine3D, Quat, Vec3, Vec4};
+    use glam::{Affine3D, Quat, Vec3};
 
     #[test]
     fn test_align() {
@@ -244,5 +244,5 @@ mod affine3d {
         assert_eq!(16, mem::align_of::<Affine3D>());
     }
 
-    impl_affine3d_tests!(f32, Affine3D, Quat, Vec4, Vec3);
+    impl_affine3d_tests!(f32, Affine3D, Quat, Vec3);
 }
diff --git a/tests/euler.rs b/tests/euler.rs
new file mode 100644
index 0000000..572d7ea
--- /dev/null
+++ b/tests/euler.rs
@@ -0,0 +1,264 @@
+use glam::{DQuat, Quat};
+
+#[macro_use]
+mod support;
+
+/// Helper to calculate the inner angle in the range [0, 2*PI)
+trait AngleDiff {
+    type Output;
+    fn angle_diff(self, other: Self) -> Self::Output;
+}
+#[macro_export]
+macro_rules! impl_angle_diff {
+    ($t:ty, $pi:expr) => {
+        impl AngleDiff for $t {
+            type Output = $t;
+            fn angle_diff(self, other: $t) -> $t {
+                const PI2: $t = $pi + $pi;
+                let s = self.rem_euclid(PI2);
+                let o = other.rem_euclid(PI2);
+                if s > o {
+                    (s - o).min(PI2 + o - s)
+                } else {
+                    (o - s).min(PI2 + s - o)
+                }
+            }
+        }
+    };
+}
+impl_angle_diff!(f32, std::f32::consts::PI);
+impl_angle_diff!(f64, std::f64::consts::PI);
+
+#[macro_export]
+macro_rules! assert_approx_angle {
+    ($a:expr, $b:expr, $eps:expr) => {{
+        let (a, b) = ($a, $b);
+        let eps = $eps;
+        let diff = a.angle_diff(b);
+        assert!(
+            diff < $eps,
+            "assertion failed: `(left !== right)` \
+             (left: `{:?}`, right: `{:?}`, expect diff: `{:?}`, real diff: `{:?}`)",
+            a,
+            b,
+            eps,
+            diff
+        );
+    }};
+}
+
+trait EqApprox {
+    type EPS;
+    fn eq_approx(self, other: Self, axis_eps: Self::EPS, rot_axis: Self::EPS) -> bool;
+}
+
+macro_rules! impl_eq_approx {
+    ($t:ty, $quat:ident, $pi:expr) => {
+        impl EqApprox for $quat {
+            type EPS = $t;
+            fn eq_approx(self, other: Self, axis_eps: Self::EPS, rot_axis: Self::EPS) -> bool {
+                let (s_axis, s_angle) = self.to_axis_angle();
+                let (o_axis, o_angle) = other.to_axis_angle();
+                if s_angle.abs() < rot_axis && o_angle.abs() < rot_axis {
+                    // No rotation
+                    true
+                } else {
+                    let a = s_axis.angle_between(o_axis);
+                    if a < axis_eps {
+                        // Same axes
+                        (s_angle - o_angle).abs() < rot_axis
+                    } else if ($pi - a).abs() < axis_eps {
+                        // Inverted axes (180°)
+                        (s_angle + o_angle).abs() < rot_axis
+                    } else {
+                        // Other
+                        false
+                    }
+                }
+            }
+        }
+    };
+}
+impl_eq_approx!(f32, Quat, std::f32::consts::PI);
+impl_eq_approx!(f64, DQuat, std::f64::consts::PI);
+
+#[macro_export]
+macro_rules! impl_3axis_test {
+    ($name:ident, $t:ty, $quat:ident, $euler:path, $U:path, $V:path, $W:path, $vec:ident) => {
+        #[test]
+        fn $name() {
+            let euler = $euler;
+            assert!($U != $W); // First and last axis must be different for three axis
+            for u in -179..=179 {
+                if u % 2 != 0 {
+                    continue;
+                }
+                for v in -89..=89 {
+                    if v % 2 != 0 {
+                        continue;
+                    }
+                    for w in -179..=179 {
+                        if w % 2 != 0 {
+                            continue;
+                        }
+                        let u1 = (u as $t).to_radians();
+                        let v1 = (v as $t).to_radians();
+                        let w1 = (w as $t).to_radians();
+
+                        let q1: $quat = ($quat::from_axis_angle($U, u1)
+                            * $quat::from_axis_angle($V, v1)
+                            * $quat::from_axis_angle($W, w1))
+                        .normalize();
+
+                        // Test if the rotation is the expected
+                        let q2: $quat = $quat::from_euler(euler, u1, v1, w1).normalize();
+                        assert_approx_eq!(q1, q2, 1e-5);
+
+                        // Test angle reconstruction
+                        let (u2, v2, w2) = q1.to_euler(euler);
+                        let q3 = $quat::from_euler(euler, u2, v2, w2).normalize();
+
+                        assert_approx_angle!(u1, u2, 1e-4 as $t);
+                        assert_approx_angle!(v1, v2, 1e-4 as $t);
+                        assert_approx_angle!(w1, w2, 1e-4 as $t);
+
+                        assert_approx_eq!(q1 * $vec::X, q3 * $vec::X, 1e-4);
+                        assert_approx_eq!(q1 * $vec::Y, q3 * $vec::Y, 1e-4);
+                        assert_approx_eq!(q1 * $vec::Z, q3 * $vec::Z, 1e-4);
+                    }
+                }
+            }
+        }
+    };
+}
+
+#[macro_export]
+macro_rules! impl_2axis_test {
+    ($name:ident, $t:ty, $quat:ident, $euler:path, $U:path, $V:path, $W:path, $vec:ident) => {
+        #[test]
+        fn $name() {
+            #[allow(deprecated)]
+            let euler = $euler;
+            assert!($U == $W); // First and last axis must be different for three axis
+            for u in -179..=179 {
+                if u % 2 != 0 {
+                    continue;
+                }
+                for v in -89..=89 {
+                    if v % 2 != 0 {
+                        continue;
+                    }
+                    for w in -179..=179 {
+                        if w % 2 != 0 {
+                            continue;
+                        }
+                        let u1 = (u as $t).to_radians();
+                        let v1 = (v as $t).to_radians();
+                        let w1 = (w as $t).to_radians();
+
+                        let q1: $quat = ($quat::from_axis_angle($U, u1)
+                            * $quat::from_axis_angle($V, v1)
+                            * $quat::from_axis_angle($W, w1))
+                        .normalize();
+
+                        // Test if the rotation is the expected
+                        let q2 = $quat::from_euler(euler, u1, v1, w1).normalize();
+                        assert_approx_eq!(q1, q2, 1e-5);
+
+                        // Test angle reconstruction
+                        let (u2, v2, w2) = q1.to_euler(euler);
+                        let _q3 = $quat::from_euler(euler, u2, v2, w2).normalize();
+
+                        // Disabled tests, since no generic tests for ambiguous results in the two-axis results...
+                        // assert_approx_angle!(u1, u2, 1e-4 as $t);
+                        // assert_approx_angle!(v1, v2, 1e-4 as $t);
+                        // assert_approx_angle!(w1, w2, 1e-4 as $t);
+
+                        // assert_approx_eq!(q1 * $vec::X, q3 * $vec::X, 1e-4);
+                        // assert_approx_eq!(q1 * $vec::Y, q3 * $vec::Y, 1e-4);
+                        // assert_approx_eq!(q1 * $vec::Z, q3 * $vec::Z, 1e-4);
+                    }
+                }
+            }
+        }
+    };
+}
+
+macro_rules! impl_all_quat_tests_three_axis {
+    ($t:ty, $q:ident, $v:ident) => {
+        impl_3axis_test!(test_euler_zyx, $t, $q, ER::ZYXi, $v::Z, $v::Y, $v::X, $v);
+        impl_3axis_test!(test_euler_zxy, $t, $q, ER::ZXYi, $v::Z, $v::X, $v::Y, $v);
+        impl_3axis_test!(test_euler_yxz, $t, $q, ER::YXZi, $v::Y, $v::X, $v::Z, $v);
+        impl_3axis_test!(test_euler_yzx, $t, $q, ER::YZXi, $v::Y, $v::Z, $v::X, $v);
+        impl_3axis_test!(test_euler_xyz, $t, $q, ER::XYZi, $v::X, $v::Y, $v::Z, $v);
+        impl_3axis_test!(test_euler_xzy, $t, $q, ER::XZYi, $v::X, $v::Z, $v::Y, $v);
+    };
+}
+
+macro_rules! impl_all_quat_tests_two_axis {
+    ($t:ty, $q:ident, $v:ident) => {
+        impl_2axis_test!(test_euler_zyz, $t, $q, ER::ZYZi, $v::Z, $v::Y, $v::Z, $v);
+        impl_2axis_test!(test_euler_zxz, $t, $q, ER::ZXZi, $v::Z, $v::X, $v::Z, $v);
+        impl_2axis_test!(test_euler_yxy, $t, $q, ER::YXYi, $v::Y, $v::X, $v::Y, $v);
+        impl_2axis_test!(test_euler_yzy, $t, $q, ER::YZYi, $v::Y, $v::Z, $v::Y, $v);
+        impl_2axis_test!(test_euler_xyx, $t, $q, ER::XYXi, $v::X, $v::Y, $v::X, $v);
+        impl_2axis_test!(test_euler_xzx, $t, $q, ER::XZXi, $v::X, $v::Z, $v::X, $v);
+    };
+}
+
+macro_rules! impl_ypr_test {
+    ($t:ty, $quat:ident, $euler:path) => {
+        #[test]
+        fn test_ypr() {
+            let euler = $euler;
+            for u in -179..=179 {
+                if u % 2 != 0 {
+                    continue;
+                }
+                for v in -89..=89 {
+                    if v % 2 != 0 {
+                        continue;
+                    }
+                    for w in -179..=179 {
+                        if w % 2 != 0 {
+                            continue;
+                        }
+                        let u1 = (u as $t).to_radians();
+                        let v1 = (v as $t).to_radians();
+                        let w1 = (w as $t).to_radians();
+
+                        let q1: $quat = $quat::from_euler(euler, u1, v1, w1);
+                        let q2: $quat = $quat::from_rotation_ypr(u1, v1, w1);
+                        assert_approx_eq!(q1, q2, 1e-5);
+                    }
+                }
+            }
+        }
+    };
+}
+
+mod euler {
+    use super::AngleDiff;
+    use glam::*;
+    type ER = EulerRot;
+
+    mod quat {
+        use super::*;
+
+        impl_all_quat_tests_three_axis!(f32, Quat, Vec3);
+
+        impl_all_quat_tests_two_axis!(f32, Quat, Vec3);
+
+        impl_ypr_test!(f32, Quat, ER::YXZi);
+    }
+
+    mod dquat {
+        use super::*;
+
+        impl_all_quat_tests_three_axis!(f64, DQuat, DVec3);
+
+        impl_all_quat_tests_two_axis!(f64, DQuat, DVec3);
+
+        impl_ypr_test!(f64, DQuat, ER::YXZi);
+    }
+}