Repository: bitshifter/glam-rs
Pull request #537 (2024-07-14 10:29:00)

Reimplement based on Ken Shoemake's Euler angle conversion from Graphics Gems IV
* Adds all 24 possible Euler angle combinations including intrinsic and extrinsic rotations.
* Improves numeric accuracy
* Fixes problem with resulting angles being outside of the expected range


Patch:
diff --git a/codegen/templates/mat.rs.tera b/codegen/templates/mat.rs.tera
index c6045f4..67b0700 100644
--- a/codegen/templates/mat.rs.tera
+++ b/codegen/templates/mat.rs.tera
@@ -72,7 +72,7 @@ use crate::{
         Mat3A,
     {% endif %}
 {% elif dim == 3 %}
-    EulerRot,
+    EulerRot, euler::{FromEuler, ToEuler},
     {{ mat2_t }}, {{ mat4_t }}, {{ quat_t }}, {{ vec2_t }}, {{ col_t }},
     {% if is_align %}
         {{ mat3_t }}, {{ vec3_t }},
@@ -80,7 +80,7 @@ use crate::{
         Mat3A, Vec3A,
     {% endif %}
 {% elif dim == 4 %}
-    EulerRot,
+    EulerRot, euler::{FromEuler, ToEuler},
     {{ mat3_t }}, {{ quat_t }}, {{ vec3_t }}, {{ col_t }},
     {% if scalar_t == "f32" %}
         Mat3A, Vec3A,
@@ -521,8 +521,24 @@ impl {{ self_t }} {
     #[inline]
     #[must_use]
     pub fn from_euler(order: EulerRot, a: {{ scalar_t }}, b: {{ scalar_t }}, c: {{ scalar_t }}) -> Self {
-        let quat = {{ quat_t }}::from_euler(order, a, b, c);
-        Self::from_quat(quat)
+        Self::from_euler_angles(order, a, b, c)
+    }
+
+    /// Extract Euler angles with the given Euler rotation order.
+    ///
+    /// Note if the input matrix contains scales, shears, or other non-rotation transformations then
+    /// the resulting Euler angles will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any input matrix column is not normalized when `glam_assert` is enabled.
+    #[inline]
+    #[must_use]
+    pub fn to_euler(&self, order: EulerRot) -> ({{ scalar_t }}, {{ scalar_t }}, {{ scalar_t }}) {
+        glam_assert!(self.x_axis.is_normalized()
+            && self.y_axis.is_normalized()
+            && self.z_axis.is_normalized());
+        self.to_euler_angles(order)
     }
 
     /// Creates a 3D rotation matrix from `angle` (in radians) around the x axis.
@@ -853,8 +869,25 @@ impl {{ self_t }} {
     #[inline]
     #[must_use]
     pub fn from_euler(order: EulerRot, a: {{ scalar_t }}, b: {{ scalar_t }}, c: {{ scalar_t }}) -> Self {
-        let quat = {{ quat_t }}::from_euler(order, a, b, c);
-        Self::from_quat(quat)
+        Self::from_euler_angles(order, a, b, c)
+    }
+
+    /// Extract Euler angles with the given Euler rotation order.
+    ///
+    /// Note if the upper 3x3 matrix contain scales, shears, or other non-rotation transformations
+    /// then the resulting Euler angles will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any column of the upper 3x3 rotation matrix is not normalized when
+    /// `glam_assert` is enabled.
+    #[inline]
+    #[must_use]
+    pub fn to_euler(&self, order: EulerRot) -> ({{ scalar_t }}, {{ scalar_t }}, {{ scalar_t }}) {
+        glam_assert!(self.x_axis.xyz().is_normalized()
+            && self.y_axis.xyz().is_normalized()
+            && self.z_axis.xyz().is_normalized());
+        self.to_euler_angles(order)
     }
 
     /// Creates an affine transformation matrix containing a 3D rotation around the x axis of
diff --git a/codegen/templates/quat.rs.tera b/codegen/templates/quat.rs.tera
index a9495df..cf8f7b1 100644
--- a/codegen/templates/quat.rs.tera
+++ b/codegen/templates/quat.rs.tera
@@ -33,7 +33,7 @@
 
 use crate::{
     {{ scalar_t }}::math,
-    euler::{EulerFromQuaternion, EulerRot, EulerToQuaternion},
+    euler::{EulerRot, FromEuler, ToEuler},
     {% if scalar_t == "f32" %}
         DQuat, Mat3, Mat3A, Mat4, Vec2, Vec3, Vec3A, Vec4,
     {% elif scalar_t == "f64" %}
@@ -294,14 +294,22 @@ impl {{ self_t }} {
     #[inline]
     #[must_use]
     pub fn from_euler(euler: EulerRot, a: {{ scalar_t }}, b: {{ scalar_t }}, c: {{ scalar_t }}) -> Self {
-        euler.new_quat(a, b, c)
+        Self::from_euler_angles(euler, a, b, c)
     }
 
     /// From the columns of a 3x3 rotation matrix.
+    ///
+    /// Note if the input axes contain scales, shears, or other non-rotation transformations then
+    /// the output of this function is ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any axis is not normalized when `glam_assert` is enabled.
     #[inline]
     #[must_use]
     pub(crate) fn from_rotation_axes(x_axis: {{ vec3_t }}, y_axis: {{ vec3_t }}, z_axis: {{ vec3_t }}) -> Self {
-        // Based on https://github.com/microsoft/DirectXMath `XM$quaternionRotationMatrix`
+        glam_assert!(x_axis.is_normalized() && y_axis.is_normalized() && z_axis.is_normalized());
+        // Based on https://github.com/microsoft/DirectXMath `XMQuaternionRotationMatrix`
         let (m00, m01, m02) = x_axis.into();
         let (m10, m11, m12) = y_axis.into();
         let (m20, m21, m22) = z_axis.into();
@@ -359,6 +367,13 @@ impl {{ self_t }} {
     }
 
     /// Creates a quaternion from a 3x3 rotation matrix.
+    ///
+    /// Note if the input matrix contain scales, shears, or other non-rotation transformations then
+    /// the resulting quaternion will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any input matrix column is not normalized when `glam_assert` is enabled.
     #[inline]
     #[must_use]
     pub fn from_mat3(mat: &{{ mat3_t }}) -> Self {
@@ -371,6 +386,13 @@ impl {{ self_t }} {
 
 {% if scalar_t == "f32" %}
     /// Creates a quaternion from a 3x3 SIMD aligned rotation matrix.
+    ///
+    /// Note if the input matrix contain scales, shears, or other non-rotation transformations then
+    /// the resulting quaternion will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any input matrix column is not normalized when `glam_assert` is enabled.
     #[inline]
     #[must_use]
     pub fn from_mat3a(mat: &Mat3A) -> Self {
@@ -382,7 +404,15 @@ impl {{ self_t }} {
     }
 {% endif %}
 
-    /// Creates a quaternion from a 3x3 rotation matrix inside a homogeneous 4x4 matrix.
+    /// Creates a quaternion from the upper 3x3 rotation matrix inside a homogeneous 4x4 matrix.
+    ///
+    /// Note if the upper 3x3 matrix contain scales, shears, or other non-rotation transformations
+    /// then the resulting quaternion will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any column of the upper 3x3 rotation matrix is not normalized when
+    /// `glam_assert` is enabled.
     #[inline]
     #[must_use]
     pub fn from_mat4(mat: &{{ mat4_t }}) -> Self {
@@ -515,8 +545,8 @@ impl {{ self_t }} {
     /// Returns the rotation angles for the given euler rotation sequence.
     #[inline]
     #[must_use]
-    pub fn to_euler(self, euler: EulerRot) -> ({{ scalar_t }}, {{ scalar_t }}, {{ scalar_t }}) {
-        euler.convert_quat(self)
+    pub fn to_euler(self, order: EulerRot) -> ({{ scalar_t }}, {{ scalar_t }}, {{ scalar_t }}) {
+        self.to_euler_angles(order)
     }
 
     /// `[x, y, z, w]`
@@ -1113,6 +1143,14 @@ impl {{ self_t }} {
     }
 
     /// Creates a quaternion from a 3x3 rotation matrix inside a 3D affine transform.
+    ///
+    /// Note if the input affine matrix contain scales, shears, or other non-rotation
+    /// transformations then the resulting quaternion will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any input affine matrix column is not normalized when `glam_assert` is
+    /// enabled.
     #[inline]
     #[must_use]
     pub fn from_affine3(a: &crate::{{ affine3_t }}) -> Self {
diff --git a/src/euler.rs b/src/euler.rs
index e180046..7a90d40 100644
--- a/src/euler.rs
+++ b/src/euler.rs
@@ -1,17 +1,28 @@
-/*
-Conversion from quaternions to Euler rotation sequences.
-
-From: http://bediyap.com/programming/convert-quaternion-to-euler-rotations/
-*/
-
-use crate::{DQuat, Quat};
+// Based on Ken Shoemake. 1994. Euler angle conversion. Graphics gems IV.  Academic Press
+// Professional, Inc., USA, 222–229.
+use crate::{DMat3, DMat4, DQuat, DVec3, Mat3, Mat3A, Mat4, Quat, Vec3, Vec3A, Vec3Swizzles};
 
 /// Euler rotation sequences.
 ///
-/// The angles are applied starting from the right.
-/// E.g. XYZ will first apply the z-axis rotation.
+/// The three elemental rotations may be extrinsic (rotations about the axes xyz of the original
+/// coordinate system, which is assumed to remain motionless), or intrinsic(rotations about the
+/// axes of the rotating coordinate system XYZ, solidary with the moving body, which changes its
+/// orientation after each elemental rotation).
+///
+/// ```
+/// # use glam::{EulerRot, Mat3};
+/// # let i = core::f32::consts::FRAC_PI_2;
+/// # let j = core::f32::consts::FRAC_PI_4;
+/// # let k = core::f32::consts::FRAC_PI_8;
+/// let m_intrinsic = Mat3::from_rotation_x(i) * Mat3::from_rotation_y(j) * Mat3::from_rotation_z(k);
+/// let n_intrinsic = Mat3::from_euler(EulerRot::XYZ, i, j, k);
+/// assert!(m_intrinsic.abs_diff_eq(n_intrinsic, 2e-6));
+///
+/// let m_extrinsic = Mat3::from_rotation_z(k) * Mat3::from_rotation_y(j) * Mat3::from_rotation_x(i);
+/// let n_extrinsic = Mat3::from_euler(EulerRot::XYZEx, i, j, k);
+/// assert!(m_extrinsic.abs_diff_eq(n_extrinsic, 2e-6));
+/// ```
 ///
-/// YXZ can be used for yaw (y-axis), pitch (x-axis), roll (z-axis).
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
 pub enum EulerRot {
     /// Intrinsic three-axis rotation ZYX
@@ -26,167 +37,401 @@ pub enum EulerRot {
     XYZ,
     /// Intrinsic three-axis rotation XZY
     XZY,
+
+    /// Intrinsic two-axis rotation ZYZ
+    ZYZ,
+    /// Intrinsic two-axis rotation ZXZ
+    ZXZ,
+    /// Intrinsic two-axis rotation YXY
+    YXY,
+    /// Intrinsic two-axis rotation YZY
+    YZY,
+    /// Intrinsic two-axis rotation XYX
+    XYX,
+    /// Intrinsic two-axis rotation XZX
+    XZX,
+
+    /// Extrinsic three-axis rotation ZYX
+    ZYXEx,
+    /// Extrinsic three-axis rotation ZXY
+    ZXYEx,
+    /// Extrinsic three-axis rotation YXZ
+    YXZEx,
+    /// Extrinsic three-axis rotation YZX
+    YZXEx,
+    /// Extrinsic three-axis rotation XYZ
+    XYZEx,
+    /// Extrinsic three-axis rotation XZY
+    XZYEx,
+
+    /// Extrinsic two-axis rotation ZYZ
+    ZYZEx,
+    /// Extrinsic two-axis rotation ZXZ
+    ZXZEx,
+    /// Extrinsic two-axis rotation YXY
+    YXYEx,
+    /// Extrinsic two-axis rotation YZY
+    YZYEx,
+    /// Extrinsic two-axis rotation XYX
+    XYXEx,
+    /// Extrinsic two-axis rotation XZX
+    XZXEx,
 }
 
-impl Default for EulerRot {
-    /// Default `YXZ` as yaw (y-axis), pitch (x-axis), roll (z-axis).
-    fn default() -> Self {
-        Self::YXZ
-    }
+pub(crate) trait ToEuler {
+    type Scalar;
+    fn to_euler_angles(self, order: EulerRot) -> (Self::Scalar, Self::Scalar, Self::Scalar);
+}
+
+pub(crate) trait FromEuler {
+    type Scalar;
+    fn from_euler_angles(
+        order: EulerRot,
+        i: Self::Scalar,
+        j: Self::Scalar,
+        k: Self::Scalar,
+    ) -> Self;
 }
 
-/// Conversion from quaternion to euler angles.
-pub(crate) trait EulerFromQuaternion<Q: Copy>: Sized + Copy {
-    type Output;
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+enum Axis {
+    X = 0,
+    Y = 1,
+    Z = 2,
+}
 
-    /// Compute all angles of a rotation in the notation order
-    fn convert_quat(self, q: Q) -> (Self::Output, Self::Output, Self::Output);
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+enum Parity {
+    Odd = 0,
+    Even = 1,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+enum Repeated {
+    No = 0,
+    Yes = 1,
+}
 
-    #[doc(hidden)]
-    fn sine_theta(self, q: Q) -> Self::Output;
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+enum Frame {
+    Relative = 0,
+    Static = 1,
 }
 
-/// Conversion from euler angles to quaternion.
-pub(crate) 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;
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+struct Order {
+    initial_axis: Axis,
+    parity_even: bool,
+    initial_repeated: bool,
+    frame_static: bool,
 }
 
-macro_rules! impl_from_quat {
-    ($t:ident, $quat:ident) => {
-        impl EulerFromQuaternion<$quat> for EulerRot {
-            type Output = $t;
-
-            fn sine_theta(self, q: $quat) -> $t {
-                use EulerRot::*;
-                match self {
-                    ZYX => -2.0 * (q.x * q.z - q.w * q.y),
-                    ZXY => 2.0 * (q.y * q.z + q.w * q.x),
-                    YXZ => -2.0 * (q.y * q.z - q.w * q.x),
-                    YZX => 2.0 * (q.x * q.y + q.w * q.z),
-                    XYZ => 2.0 * (q.x * q.z + q.w * q.y),
-                    XZY => -2.0 * (q.x * q.y - q.w * q.z),
+impl Order {
+    const fn new(
+        initial_axis: Axis,
+        parity: Parity,
+        initial_repeated: Repeated,
+        frame: Frame,
+    ) -> Self {
+        Self {
+            initial_axis,
+            parity_even: parity as u32 == Parity::Even as u32,
+            initial_repeated: initial_repeated as u32 == Repeated::Yes as u32,
+            frame_static: frame as u32 == Frame::Static as u32,
+        }
+    }
+
+    const fn from_euler(euler: EulerRot) -> Self {
+        match euler {
+            EulerRot::XYZ => Self::new(Axis::X, Parity::Even, Repeated::No, Frame::Static),
+            EulerRot::XYX => Self::new(Axis::X, Parity::Even, Repeated::Yes, Frame::Static),
+            EulerRot::XZY => Self::new(Axis::X, Parity::Odd, Repeated::No, Frame::Static),
+            EulerRot::XZX => Self::new(Axis::X, Parity::Odd, Repeated::Yes, Frame::Static),
+            EulerRot::YZX => Self::new(Axis::Y, Parity::Even, Repeated::No, Frame::Static),
+            EulerRot::YZY => Self::new(Axis::Y, Parity::Even, Repeated::Yes, Frame::Static),
+            EulerRot::YXZ => Self::new(Axis::Y, Parity::Odd, Repeated::No, Frame::Static),
+            EulerRot::YXY => Self::new(Axis::Y, Parity::Odd, Repeated::Yes, Frame::Static),
+            EulerRot::ZXY => Self::new(Axis::Z, Parity::Even, Repeated::No, Frame::Static),
+            EulerRot::ZXZ => Self::new(Axis::Z, Parity::Even, Repeated::Yes, Frame::Static),
+            EulerRot::ZYX => Self::new(Axis::Z, Parity::Odd, Repeated::No, Frame::Static),
+            EulerRot::ZYZ => Self::new(Axis::Z, Parity::Odd, Repeated::Yes, Frame::Static),
+            EulerRot::ZYXEx => Self::new(Axis::X, Parity::Even, Repeated::No, Frame::Relative),
+            EulerRot::XYXEx => Self::new(Axis::X, Parity::Even, Repeated::Yes, Frame::Relative),
+            EulerRot::YZXEx => Self::new(Axis::X, Parity::Odd, Repeated::No, Frame::Relative),
+            EulerRot::XZXEx => Self::new(Axis::X, Parity::Odd, Repeated::Yes, Frame::Relative),
+            EulerRot::XZYEx => Self::new(Axis::Y, Parity::Even, Repeated::No, Frame::Relative),
+            EulerRot::YZYEx => Self::new(Axis::Y, Parity::Even, Repeated::Yes, Frame::Relative),
+            EulerRot::ZXYEx => Self::new(Axis::Y, Parity::Odd, Repeated::No, Frame::Relative),
+            EulerRot::YXYEx => Self::new(Axis::Y, Parity::Odd, Repeated::Yes, Frame::Relative),
+            EulerRot::YXZEx => Self::new(Axis::Z, Parity::Even, Repeated::No, Frame::Relative),
+            EulerRot::ZXZEx => Self::new(Axis::Z, Parity::Even, Repeated::Yes, Frame::Relative),
+            EulerRot::XYZEx => Self::new(Axis::Z, Parity::Odd, Repeated::No, Frame::Relative),
+            EulerRot::ZYZEx => Self::new(Axis::Z, Parity::Odd, Repeated::Yes, Frame::Relative),
+        }
+    }
+
+    const fn next_axis(i: usize) -> usize {
+        (i + 1) % 3
+    }
+
+    const fn prev_axis(i: usize) -> usize {
+        if i > 0 {
+            i - 1
+        } else {
+            2
+        }
+    }
+
+    const fn angle_order(self) -> (usize, usize, usize) {
+        let i = self.initial_axis as usize;
+        let j = if self.parity_even {
+            Order::next_axis(i)
+        } else {
+            Order::prev_axis(i)
+        };
+        let k = if self.parity_even {
+            Order::prev_axis(i)
+        } else {
+            Order::next_axis(i)
+        };
+        (i, j, k)
+    }
+}
+
+macro_rules! impl_mat3_from_euler {
+    ($scalar:ident, $mat3:ident, $vec3:ident) => {
+        impl FromEuler for $mat3 {
+            type Scalar = $scalar;
+            fn from_euler_angles(
+                euler: EulerRot,
+                x: Self::Scalar,
+                y: Self::Scalar,
+                z: Self::Scalar,
+            ) -> Self {
+                use crate::$scalar::math;
+
+                let order = Order::from_euler(euler);
+                let (i, j, k) = order.angle_order();
+
+                let mut angles = if order.frame_static {
+                    $vec3::new(x, y, z)
+                } else {
+                    $vec3::new(z, y, x)
+                };
+
+                // rotation direction is reverse from original paper
+                if order.parity_even {
+                    angles = -angles;
                 }
-                .clamp(-1.0, 1.0)
-            }
 
-            fn convert_quat(self, q: $quat) -> ($t, $t, $t) {
-                use crate::$t::math;
-                use EulerRot::*;
+                let (si, ci) = math::sin_cos(angles.x);
+                let (sj, cj) = math::sin_cos(angles.y);
+                let (sh, ch) = math::sin_cos(angles.z);
 
-                let sine_theta = self.sine_theta(q);
-                let second = math::asin(sine_theta);
+                let cc = ci * ch;
+                let cs = ci * sh;
+                let sc = si * ch;
+                let ss = si * sh;
 
-                if math::abs(sine_theta) > 0.99999 {
-                    let scale = 2.0 * math::signum(sine_theta);
+                let mut m = [[0.0; 3]; 3];
 
-                    return match self {
-                        ZYX => (scale * math::atan2(-q.x, q.w), second, 0.0),
-                        ZXY => (scale * math::atan2(q.y, q.w), second, 0.0),
-                        YXZ => (scale * math::atan2(-q.z, q.w), second, 0.0),
-                        YZX => (scale * math::atan2(q.x, q.w), second, 0.0),
-                        XYZ => (scale * math::atan2(q.z, q.w), second, 0.0),
-                        XZY => (scale * math::atan2(-q.y, q.w), second, 0.0),
-                    };
+                if order.initial_repeated {
+                    m[i][i] = cj;
+                    m[i][j] = sj * si;
+                    m[i][k] = sj * ci;
+                    m[j][i] = sj * sh;
+                    m[j][j] = -cj * ss + cc;
+                    m[j][k] = -cj * cs - sc;
+                    m[k][i] = -sj * ch;
+                    m[k][j] = cj * sc + cs;
+                    m[k][k] = cj * cc - ss;
+                } else {
+                    m[i][i] = cj * ch;
+                    m[i][j] = sj * sc - cs;
+                    m[i][k] = sj * cc + ss;
+                    m[j][i] = cj * sh;
+                    m[j][j] = sj * ss + cc;
+                    m[j][k] = sj * cs - sc;
+                    m[k][i] = -sj;
+                    m[k][j] = cj * si;
+                    m[k][k] = cj * ci;
                 }
 
-                let first = match self {
-                    ZYX => math::atan2(
-                        2.0 * (q.x * q.y + q.w * q.z),
-                        q.w * q.w + q.x * q.x - q.y * q.y - q.z * q.z,
-                    ),
-                    ZXY => math::atan2(
-                        -2.0 * (q.x * q.y - q.w * q.z),
-                        q.w * q.w - q.x * q.x + q.y * q.y - q.z * q.z,
-                    ),
-                    YXZ => math::atan2(
-                        2.0 * (q.x * q.z + q.w * q.y),
-                        q.w * q.w - q.x * q.x - q.y * q.y + q.z * q.z,
-                    ),
-                    YZX => math::atan2(
-                        -2.0 * (q.x * q.z - q.w * q.y),
-                        q.w * q.w + q.x * q.x - q.y * q.y - q.z * q.z,
-                    ),
-                    XYZ => math::atan2(
-                        -2.0 * (q.y * q.z - q.w * q.x),
-                        q.w * q.w - q.x * q.x - q.y * q.y + q.z * q.z,
-                    ),
-                    XZY => math::atan2(
-                        2.0 * (q.y * q.z + q.w * q.x),
-                        q.w * q.w - q.x * q.x + q.y * q.y - q.z * q.z,
-                    ),
-                };
+                $mat3::from_cols_array_2d(&m)
+            }
+        }
+    };
+}
+
+macro_rules! impl_mat4_from_euler {
+    ($scalar:ident, $mat4:ident, $mat3:ident) => {
+        impl FromEuler for $mat4 {
+            type Scalar = $scalar;
+            fn from_euler_angles(
+                euler: EulerRot,
+                x: Self::Scalar,
+                y: Self::Scalar,
+                z: Self::Scalar,
+            ) -> Self {
+                $mat4::from_mat3($mat3::from_euler_angles(euler, x, y, z))
+            }
+        }
+    };
+}
+
+macro_rules! impl_quat_from_euler {
+    ($scalar:ident, $quat:ident, $vec3:ident) => {
+        impl FromEuler for $quat {
+            type Scalar = $scalar;
+            fn from_euler_angles(
+                euler: EulerRot,
+                x: Self::Scalar,
+                y: Self::Scalar,
+                z: Self::Scalar,
+            ) -> Self {
+                use crate::$scalar::math;
 
-                let third = match self {
-                    ZYX => math::atan2(
-                        2.0 * (q.y * q.z + q.w * q.x),
-                        q.w * q.w - q.x * q.x - q.y * q.y + q.z * q.z,
-                    ),
-                    ZXY => math::atan2(
-                        -2.0 * (q.x * q.z - q.w * q.y),
-                        q.w * q.w - q.x * q.x - q.y * q.y + q.z * q.z,
-                    ),
-                    YXZ => math::atan2(
-                        2.0 * (q.x * q.y + q.w * q.z),
-                        q.w * q.w - q.x * q.x + q.y * q.y - q.z * q.z,
-                    ),
-                    YZX => math::atan2(
-                        -2.0 * (q.y * q.z - q.w * q.x),
-                        q.w * q.w - q.x * q.x + q.y * q.y - q.z * q.z,
-                    ),
-                    XYZ => math::atan2(
-                        -2.0 * (q.x * q.y - q.w * q.z),
-                        q.w * q.w + q.x * q.x - q.y * q.y - q.z * q.z,
-                    ),
-                    XZY => math::atan2(
-                        2.0 * (q.x * q.z + q.w * q.y),
-                        q.w * q.w + q.x * q.x - q.y * q.y - q.z * q.z,
-                    ),
+                let order = Order::from_euler(euler);
+                let (i, j, k) = order.angle_order();
+
+                let mut angles = if order.frame_static {
+                    $vec3::new(x, y, z)
+                } else {
+                    $vec3::new(z, y, x)
                 };
 
-                (first, second, third)
+                if order.parity_even {
+                    angles.y = -angles.y;
+                }
+
+                let ti = angles.x * 0.5;
+                let tj = angles.y * 0.5;
+                let th = angles.z * 0.5;
+                let (si, ci) = math::sin_cos(ti);
+                let (sj, cj) = math::sin_cos(tj);
+                let (sh, ch) = math::sin_cos(th);
+                let cc = ci * ch;
+                let cs = ci * sh;
+                let sc = si * ch;
+                let ss = si * sh;
+
+                let parity = if !order.parity_even { 1.0 } else { -1.0 };
+
+                let mut a = [0.0; 4];
+
+                if order.initial_repeated {
+                    a[i] = cj * (cs + sc);
+                    a[j] = sj * (cc + ss) * parity;
+                    a[k] = sj * (cs - sc);
+                    a[3] = cj * (cc - ss);
+                } else {
+                    a[i] = cj * sc - sj * cs;
+                    a[j] = (cj * ss + sj * cc) * parity;
+                    a[k] = cj * cs - sj * sc;
+                    a[3] = cj * cc + sj * ss;
+                }
+
+                $quat::from_array(a)
             }
         }
-        // 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)
+macro_rules! impl_mat3_to_euler {
+    ($scalar:ident, $mat3:ident, $vec3:ident) => {
+        impl ToEuler for $mat3 {
+            type Scalar = $scalar;
+            fn to_euler_angles(
+                self,
+                euler: EulerRot,
+            ) -> (Self::Scalar, Self::Scalar, Self::Scalar) {
+                use crate::$scalar::math;
+
+                let order = Order::from_euler(euler);
+                let (i, j, k) = order.angle_order();
+
+                let mut ea = $vec3::ZERO;
+                if order.initial_repeated {
+                    let sy = math::sqrt(
+                        self.col(i)[j] * self.col(i)[j] + self.col(i)[k] * self.col(i)[k],
+                    );
+                    if (sy > 16.0 * $scalar::EPSILON) {
+                        ea.x = math::atan2(self.col(i)[j], self.col(i)[k]);
+                        ea.y = math::atan2(sy, self.col(i)[i]);
+                        ea.z = math::atan2(self.col(j)[i], -self.col(k)[i]);
+                    } else {
+                        ea.x = math::atan2(-self.col(j)[k], self.col(j)[j]);
+                        ea.y = math::atan2(sy, self.col(i)[i]);
+                    }
+                } else {
+                    let cy = math::sqrt(
+                        self.col(i)[i] * self.col(i)[i] + self.col(j)[i] * self.col(j)[i],
+                    );
+                    if (cy > 16.0 * $scalar::EPSILON) {
+                        ea.x = math::atan2(self.col(k)[j], self.col(k)[k]);
+                        ea.y = math::atan2(-self.col(k)[i], cy);
+                        ea.z = math::atan2(self.col(j)[i], self.col(i)[i]);
+                    } else {
+                        ea.x = math::atan2(-self.col(j)[k], self.col(j)[j]);
+                        ea.y = math::atan2(-self.col(k)[i], cy);
+                    }
                 }
-                #[inline(always)]
-                fn rot_z(a: $t) -> $quat {
-                    $quat::from_rotation_z(a)
+
+                // reverse rotation angle of original code
+                if order.parity_even {
+                    ea = -ea;
                 }
-                match self {
-                    ZYX => rot_z(u) * rot_y(v) * rot_x(w),
-                    ZXY => rot_z(u) * rot_x(v) * rot_y(w),
-                    YXZ => rot_y(u) * rot_x(v) * rot_z(w),
-                    YZX => rot_y(u) * rot_z(v) * rot_x(w),
-                    XYZ => rot_x(u) * rot_y(v) * rot_z(w),
-                    XZY => rot_x(u) * rot_z(v) * rot_y(w),
+
+                if !order.frame_static {
+                    ea = ea.zyx();
                 }
-                .normalize()
+
+                (ea.x, ea.y, ea.z)
             }
         }
-        // End - impl EulerToQuaternion
     };
 }
 
-impl_from_quat!(f32, Quat);
-impl_from_quat!(f64, DQuat);
-impl_to_quat!(f32, Quat);
-impl_to_quat!(f64, DQuat);
+macro_rules! impl_mat4_to_euler {
+    ($scalar:ident, $mat4:ident, $mat3:ident) => {
+        impl ToEuler for $mat4 {
+            type Scalar = $scalar;
+            fn to_euler_angles(
+                self,
+                order: EulerRot,
+            ) -> (Self::Scalar, Self::Scalar, Self::Scalar) {
+                $mat3::from_mat4(self).to_euler_angles(order)
+            }
+        }
+    };
+}
+
+macro_rules! impl_quat_to_euler {
+    ($scalar:ident, $quat:ident, $mat3:ident) => {
+        impl ToEuler for $quat {
+            type Scalar = $scalar;
+            fn to_euler_angles(
+                self,
+                order: EulerRot,
+            ) -> (Self::Scalar, Self::Scalar, Self::Scalar) {
+                $mat3::from_quat(self).to_euler_angles(order)
+            }
+        }
+    };
+}
+
+impl_mat3_to_euler!(f32, Mat3, Vec3);
+impl_mat3_from_euler!(f32, Mat3, Vec3);
+impl_mat3_to_euler!(f32, Mat3A, Vec3A);
+impl_mat3_from_euler!(f32, Mat3A, Vec3A);
+impl_mat4_from_euler!(f32, Mat4, Mat3);
+impl_mat4_to_euler!(f32, Mat4, Mat3);
+impl_quat_to_euler!(f32, Quat, Mat3);
+impl_quat_from_euler!(f32, Quat, Vec3);
+
+impl_mat3_to_euler!(f64, DMat3, DVec3);
+impl_mat3_from_euler!(f64, DMat3, DVec3);
+impl_mat4_to_euler!(f64, DMat4, DMat3);
+impl_mat4_from_euler!(f64, DMat4, DMat3);
+impl_quat_to_euler!(f64, DQuat, DMat3);
+impl_quat_from_euler!(f64, DQuat, DVec3);
diff --git a/src/f32/coresimd/mat3a.rs b/src/f32/coresimd/mat3a.rs
index b5fb94f..841b35e 100644
--- a/src/f32/coresimd/mat3a.rs
+++ b/src/f32/coresimd/mat3a.rs
@@ -1,6 +1,11 @@
 // Generated from mat.rs.tera template. Edit the template, not the generated file.
 
-use crate::{f32::math, swizzles::*, DMat3, EulerRot, Mat2, Mat3, Mat4, Quat, Vec2, Vec3, Vec3A};
+use crate::{
+    euler::{FromEuler, ToEuler},
+    f32::math,
+    swizzles::*,
+    DMat3, EulerRot, Mat2, Mat3, Mat4, Quat, Vec2, Vec3, Vec3A,
+};
 #[cfg(not(target_arch = "spirv"))]
 use core::fmt;
 use core::iter::{Product, Sum};
@@ -220,8 +225,26 @@ impl Mat3A {
     #[inline]
     #[must_use]
     pub fn from_euler(order: EulerRot, a: f32, b: f32, c: f32) -> Self {
-        let quat = Quat::from_euler(order, a, b, c);
-        Self::from_quat(quat)
+        Self::from_euler_angles(order, a, b, c)
+    }
+
+    /// Extract Euler angles with the given Euler rotation order.
+    ///
+    /// Note if the input matrix contains scales, shears, or other non-rotation transformations then
+    /// the resulting Euler angles will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any input matrix column is not normalized when `glam_assert` is enabled.
+    #[inline]
+    #[must_use]
+    pub fn to_euler(&self, order: EulerRot) -> (f32, f32, f32) {
+        glam_assert!(
+            self.x_axis.is_normalized()
+                && self.y_axis.is_normalized()
+                && self.z_axis.is_normalized()
+        );
+        self.to_euler_angles(order)
     }
 
     /// Creates a 3D rotation matrix from `angle` (in radians) around the x axis.
diff --git a/src/f32/coresimd/mat4.rs b/src/f32/coresimd/mat4.rs
index 7745dcb..2f106d7 100644
--- a/src/f32/coresimd/mat4.rs
+++ b/src/f32/coresimd/mat4.rs
@@ -1,7 +1,11 @@
 // Generated from mat.rs.tera template. Edit the template, not the generated file.
 
 use crate::{
-    coresimd::*, f32::math, swizzles::*, DMat4, EulerRot, Mat3, Mat3A, Quat, Vec3, Vec3A, Vec4,
+    coresimd::*,
+    euler::{FromEuler, ToEuler},
+    f32::math,
+    swizzles::*,
+    DMat4, EulerRot, Mat3, Mat3A, Quat, Vec3, Vec3A, Vec4,
 };
 #[cfg(not(target_arch = "spirv"))]
 use core::fmt;
@@ -382,8 +386,27 @@ impl Mat4 {
     #[inline]
     #[must_use]
     pub fn from_euler(order: EulerRot, a: f32, b: f32, c: f32) -> Self {
-        let quat = Quat::from_euler(order, a, b, c);
-        Self::from_quat(quat)
+        Self::from_euler_angles(order, a, b, c)
+    }
+
+    /// Extract Euler angles with the given Euler rotation order.
+    ///
+    /// Note if the upper 3x3 matrix contain scales, shears, or other non-rotation transformations
+    /// then the resulting Euler angles will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any column of the upper 3x3 rotation matrix is not normalized when
+    /// `glam_assert` is enabled.
+    #[inline]
+    #[must_use]
+    pub fn to_euler(&self, order: EulerRot) -> (f32, f32, f32) {
+        glam_assert!(
+            self.x_axis.xyz().is_normalized()
+                && self.y_axis.xyz().is_normalized()
+                && self.z_axis.xyz().is_normalized()
+        );
+        self.to_euler_angles(order)
     }
 
     /// Creates an affine transformation matrix containing a 3D rotation around the x axis of
diff --git a/src/f32/coresimd/quat.rs b/src/f32/coresimd/quat.rs
index 7814579..9ad130b 100644
--- a/src/f32/coresimd/quat.rs
+++ b/src/f32/coresimd/quat.rs
@@ -2,7 +2,7 @@
 
 use crate::{
     coresimd::*,
-    euler::{EulerFromQuaternion, EulerRot, EulerToQuaternion},
+    euler::{EulerRot, FromEuler, ToEuler},
     f32::math,
     DQuat, Mat3, Mat3A, Mat4, Vec2, Vec3, Vec3A, Vec4,
 };
@@ -175,14 +175,22 @@ impl Quat {
     #[inline]
     #[must_use]
     pub fn from_euler(euler: EulerRot, a: f32, b: f32, c: f32) -> Self {
-        euler.new_quat(a, b, c)
+        Self::from_euler_angles(euler, a, b, c)
     }
 
     /// From the columns of a 3x3 rotation matrix.
+    ///
+    /// Note if the input axes contain scales, shears, or other non-rotation transformations then
+    /// the output of this function is ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any axis is not normalized when `glam_assert` is enabled.
     #[inline]
     #[must_use]
     pub(crate) fn from_rotation_axes(x_axis: Vec3, y_axis: Vec3, z_axis: Vec3) -> Self {
-        // Based on https://github.com/microsoft/DirectXMath `XM$quaternionRotationMatrix`
+        glam_assert!(x_axis.is_normalized() && y_axis.is_normalized() && z_axis.is_normalized());
+        // Based on https://github.com/microsoft/DirectXMath `XMQuaternionRotationMatrix`
         let (m00, m01, m02) = x_axis.into();
         let (m10, m11, m12) = y_axis.into();
         let (m20, m21, m22) = z_axis.into();
@@ -240,6 +248,13 @@ impl Quat {
     }
 
     /// Creates a quaternion from a 3x3 rotation matrix.
+    ///
+    /// Note if the input matrix contain scales, shears, or other non-rotation transformations then
+    /// the resulting quaternion will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any input matrix column is not normalized when `glam_assert` is enabled.
     #[inline]
     #[must_use]
     pub fn from_mat3(mat: &Mat3) -> Self {
@@ -247,13 +262,28 @@ impl Quat {
     }
 
     /// Creates a quaternion from a 3x3 SIMD aligned rotation matrix.
+    ///
+    /// Note if the input matrix contain scales, shears, or other non-rotation transformations then
+    /// the resulting quaternion will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any input matrix column is not normalized when `glam_assert` is enabled.
     #[inline]
     #[must_use]
     pub fn from_mat3a(mat: &Mat3A) -> Self {
         Self::from_rotation_axes(mat.x_axis.into(), mat.y_axis.into(), mat.z_axis.into())
     }
 
-    /// Creates a quaternion from a 3x3 rotation matrix inside a homogeneous 4x4 matrix.
+    /// Creates a quaternion from the upper 3x3 rotation matrix inside a homogeneous 4x4 matrix.
+    ///
+    /// Note if the upper 3x3 matrix contain scales, shears, or other non-rotation transformations
+    /// then the resulting quaternion will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any column of the upper 3x3 rotation matrix is not normalized when
+    /// `glam_assert` is enabled.
     #[inline]
     #[must_use]
     pub fn from_mat4(mat: &Mat4) -> Self {
@@ -386,8 +416,8 @@ impl Quat {
     /// Returns the rotation angles for the given euler rotation sequence.
     #[inline]
     #[must_use]
-    pub fn to_euler(self, euler: EulerRot) -> (f32, f32, f32) {
-        euler.convert_quat(self)
+    pub fn to_euler(self, order: EulerRot) -> (f32, f32, f32) {
+        self.to_euler_angles(order)
     }
 
     /// `[x, y, z, w]`
@@ -714,6 +744,14 @@ impl Quat {
     }
 
     /// Creates a quaternion from a 3x3 rotation matrix inside a 3D affine transform.
+    ///
+    /// Note if the input affine matrix contain scales, shears, or other non-rotation
+    /// transformations then the resulting quaternion will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any input affine matrix column is not normalized when `glam_assert` is
+    /// enabled.
     #[inline]
     #[must_use]
     pub fn from_affine3(a: &crate::Affine3A) -> Self {
diff --git a/src/f32/mat3.rs b/src/f32/mat3.rs
index e82465e..3205d02 100644
--- a/src/f32/mat3.rs
+++ b/src/f32/mat3.rs
@@ -1,6 +1,11 @@
 // Generated from mat.rs.tera template. Edit the template, not the generated file.
 
-use crate::{f32::math, swizzles::*, DMat3, EulerRot, Mat2, Mat3A, Mat4, Quat, Vec2, Vec3, Vec3A};
+use crate::{
+    euler::{FromEuler, ToEuler},
+    f32::math,
+    swizzles::*,
+    DMat3, EulerRot, Mat2, Mat3A, Mat4, Quat, Vec2, Vec3, Vec3A,
+};
 #[cfg(not(target_arch = "spirv"))]
 use core::fmt;
 use core::iter::{Product, Sum};
@@ -221,8 +226,26 @@ impl Mat3 {
     #[inline]
     #[must_use]
     pub fn from_euler(order: EulerRot, a: f32, b: f32, c: f32) -> Self {
-        let quat = Quat::from_euler(order, a, b, c);
-        Self::from_quat(quat)
+        Self::from_euler_angles(order, a, b, c)
+    }
+
+    /// Extract Euler angles with the given Euler rotation order.
+    ///
+    /// Note if the input matrix contains scales, shears, or other non-rotation transformations then
+    /// the resulting Euler angles will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any input matrix column is not normalized when `glam_assert` is enabled.
+    #[inline]
+    #[must_use]
+    pub fn to_euler(&self, order: EulerRot) -> (f32, f32, f32) {
+        glam_assert!(
+            self.x_axis.is_normalized()
+                && self.y_axis.is_normalized()
+                && self.z_axis.is_normalized()
+        );
+        self.to_euler_angles(order)
     }
 
     /// Creates a 3D rotation matrix from `angle` (in radians) around the x axis.
diff --git a/src/f32/math.rs b/src/f32/math.rs
index 3e25875..127c9af 100644
--- a/src/f32/math.rs
+++ b/src/f32/math.rs
@@ -44,11 +44,6 @@ mod libm_math {
         super::acos_approx_f32(f)
     }
 
-    #[inline(always)]
-    pub(crate) fn asin(f: f32) -> f32 {
-        libm::asinf(f)
-    }
-
     #[inline(always)]
     pub(crate) fn atan2(f: f32, other: f32) -> f32 {
         libm::atan2f(f, other)
@@ -157,11 +152,6 @@ mod std_math {
         super::acos_approx_f32(f)
     }
 
-    #[inline(always)]
-    pub(crate) fn asin(f: f32) -> f32 {
-        f32::asin(f)
-    }
-
     #[inline(always)]
     pub(crate) fn atan2(f: f32, other: f32) -> f32 {
         f32::atan2(f, other)
diff --git a/src/f32/neon/mat3a.rs b/src/f32/neon/mat3a.rs
index b87bada..9423ffb 100644
--- a/src/f32/neon/mat3a.rs
+++ b/src/f32/neon/mat3a.rs
@@ -1,6 +1,11 @@
 // Generated from mat.rs.tera template. Edit the template, not the generated file.
 
-use crate::{f32::math, swizzles::*, DMat3, EulerRot, Mat2, Mat3, Mat4, Quat, Vec2, Vec3, Vec3A};
+use crate::{
+    euler::{FromEuler, ToEuler},
+    f32::math,
+    swizzles::*,
+    DMat3, EulerRot, Mat2, Mat3, Mat4, Quat, Vec2, Vec3, Vec3A,
+};
 #[cfg(not(target_arch = "spirv"))]
 use core::fmt;
 use core::iter::{Product, Sum};
@@ -220,8 +225,26 @@ impl Mat3A {
     #[inline]
     #[must_use]
     pub fn from_euler(order: EulerRot, a: f32, b: f32, c: f32) -> Self {
-        let quat = Quat::from_euler(order, a, b, c);
-        Self::from_quat(quat)
+        Self::from_euler_angles(order, a, b, c)
+    }
+
+    /// Extract Euler angles with the given Euler rotation order.
+    ///
+    /// Note if the input matrix contains scales, shears, or other non-rotation transformations then
+    /// the resulting Euler angles will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any input matrix column is not normalized when `glam_assert` is enabled.
+    #[inline]
+    #[must_use]
+    pub fn to_euler(&self, order: EulerRot) -> (f32, f32, f32) {
+        glam_assert!(
+            self.x_axis.is_normalized()
+                && self.y_axis.is_normalized()
+                && self.z_axis.is_normalized()
+        );
+        self.to_euler_angles(order)
     }
 
     /// Creates a 3D rotation matrix from `angle` (in radians) around the x axis.
diff --git a/src/f32/neon/mat4.rs b/src/f32/neon/mat4.rs
index b3e12c5..0c6dd74 100644
--- a/src/f32/neon/mat4.rs
+++ b/src/f32/neon/mat4.rs
@@ -1,7 +1,11 @@
 // Generated from mat.rs.tera template. Edit the template, not the generated file.
 
 use crate::{
-    f32::math, neon::*, swizzles::*, DMat4, EulerRot, Mat3, Mat3A, Quat, Vec3, Vec3A, Vec4,
+    euler::{FromEuler, ToEuler},
+    f32::math,
+    neon::*,
+    swizzles::*,
+    DMat4, EulerRot, Mat3, Mat3A, Quat, Vec3, Vec3A, Vec4,
 };
 #[cfg(not(target_arch = "spirv"))]
 use core::fmt;
@@ -382,8 +386,27 @@ impl Mat4 {
     #[inline]
     #[must_use]
     pub fn from_euler(order: EulerRot, a: f32, b: f32, c: f32) -> Self {
-        let quat = Quat::from_euler(order, a, b, c);
-        Self::from_quat(quat)
+        Self::from_euler_angles(order, a, b, c)
+    }
+
+    /// Extract Euler angles with the given Euler rotation order.
+    ///
+    /// Note if the upper 3x3 matrix contain scales, shears, or other non-rotation transformations
+    /// then the resulting Euler angles will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any column of the upper 3x3 rotation matrix is not normalized when
+    /// `glam_assert` is enabled.
+    #[inline]
+    #[must_use]
+    pub fn to_euler(&self, order: EulerRot) -> (f32, f32, f32) {
+        glam_assert!(
+            self.x_axis.xyz().is_normalized()
+                && self.y_axis.xyz().is_normalized()
+                && self.z_axis.xyz().is_normalized()
+        );
+        self.to_euler_angles(order)
     }
 
     /// Creates an affine transformation matrix containing a 3D rotation around the x axis of
diff --git a/src/f32/neon/quat.rs b/src/f32/neon/quat.rs
index 9ca8241..1f86fb5 100644
--- a/src/f32/neon/quat.rs
+++ b/src/f32/neon/quat.rs
@@ -1,7 +1,7 @@
 // Generated from quat.rs.tera template. Edit the template, not the generated file.
 
 use crate::{
-    euler::{EulerFromQuaternion, EulerRot, EulerToQuaternion},
+    euler::{EulerRot, FromEuler, ToEuler},
     f32::math,
     neon::*,
     DQuat, Mat3, Mat3A, Mat4, Vec2, Vec3, Vec3A, Vec4,
@@ -180,14 +180,22 @@ impl Quat {
     #[inline]
     #[must_use]
     pub fn from_euler(euler: EulerRot, a: f32, b: f32, c: f32) -> Self {
-        euler.new_quat(a, b, c)
+        Self::from_euler_angles(euler, a, b, c)
     }
 
     /// From the columns of a 3x3 rotation matrix.
+    ///
+    /// Note if the input axes contain scales, shears, or other non-rotation transformations then
+    /// the output of this function is ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any axis is not normalized when `glam_assert` is enabled.
     #[inline]
     #[must_use]
     pub(crate) fn from_rotation_axes(x_axis: Vec3, y_axis: Vec3, z_axis: Vec3) -> Self {
-        // Based on https://github.com/microsoft/DirectXMath `XM$quaternionRotationMatrix`
+        glam_assert!(x_axis.is_normalized() && y_axis.is_normalized() && z_axis.is_normalized());
+        // Based on https://github.com/microsoft/DirectXMath `XMQuaternionRotationMatrix`
         let (m00, m01, m02) = x_axis.into();
         let (m10, m11, m12) = y_axis.into();
         let (m20, m21, m22) = z_axis.into();
@@ -245,6 +253,13 @@ impl Quat {
     }
 
     /// Creates a quaternion from a 3x3 rotation matrix.
+    ///
+    /// Note if the input matrix contain scales, shears, or other non-rotation transformations then
+    /// the resulting quaternion will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any input matrix column is not normalized when `glam_assert` is enabled.
     #[inline]
     #[must_use]
     pub fn from_mat3(mat: &Mat3) -> Self {
@@ -252,13 +267,28 @@ impl Quat {
     }
 
     /// Creates a quaternion from a 3x3 SIMD aligned rotation matrix.
+    ///
+    /// Note if the input matrix contain scales, shears, or other non-rotation transformations then
+    /// the resulting quaternion will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any input matrix column is not normalized when `glam_assert` is enabled.
     #[inline]
     #[must_use]
     pub fn from_mat3a(mat: &Mat3A) -> Self {
         Self::from_rotation_axes(mat.x_axis.into(), mat.y_axis.into(), mat.z_axis.into())
     }
 
-    /// Creates a quaternion from a 3x3 rotation matrix inside a homogeneous 4x4 matrix.
+    /// Creates a quaternion from the upper 3x3 rotation matrix inside a homogeneous 4x4 matrix.
+    ///
+    /// Note if the upper 3x3 matrix contain scales, shears, or other non-rotation transformations
+    /// then the resulting quaternion will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any column of the upper 3x3 rotation matrix is not normalized when
+    /// `glam_assert` is enabled.
     #[inline]
     #[must_use]
     pub fn from_mat4(mat: &Mat4) -> Self {
@@ -391,8 +421,8 @@ impl Quat {
     /// Returns the rotation angles for the given euler rotation sequence.
     #[inline]
     #[must_use]
-    pub fn to_euler(self, euler: EulerRot) -> (f32, f32, f32) {
-        euler.convert_quat(self)
+    pub fn to_euler(self, order: EulerRot) -> (f32, f32, f32) {
+        self.to_euler_angles(order)
     }
 
     /// `[x, y, z, w]`
@@ -742,6 +772,14 @@ impl Quat {
     }
 
     /// Creates a quaternion from a 3x3 rotation matrix inside a 3D affine transform.
+    ///
+    /// Note if the input affine matrix contain scales, shears, or other non-rotation
+    /// transformations then the resulting quaternion will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any input affine matrix column is not normalized when `glam_assert` is
+    /// enabled.
     #[inline]
     #[must_use]
     pub fn from_affine3(a: &crate::Affine3A) -> Self {
diff --git a/src/f32/scalar/mat3a.rs b/src/f32/scalar/mat3a.rs
index 2801251..0a1e255 100644
--- a/src/f32/scalar/mat3a.rs
+++ b/src/f32/scalar/mat3a.rs
@@ -1,6 +1,11 @@
 // Generated from mat.rs.tera template. Edit the template, not the generated file.
 
-use crate::{f32::math, swizzles::*, DMat3, EulerRot, Mat2, Mat3, Mat4, Quat, Vec2, Vec3, Vec3A};
+use crate::{
+    euler::{FromEuler, ToEuler},
+    f32::math,
+    swizzles::*,
+    DMat3, EulerRot, Mat2, Mat3, Mat4, Quat, Vec2, Vec3, Vec3A,
+};
 #[cfg(not(target_arch = "spirv"))]
 use core::fmt;
 use core::iter::{Product, Sum};
@@ -221,8 +226,26 @@ impl Mat3A {
     #[inline]
     #[must_use]
     pub fn from_euler(order: EulerRot, a: f32, b: f32, c: f32) -> Self {
-        let quat = Quat::from_euler(order, a, b, c);
-        Self::from_quat(quat)
+        Self::from_euler_angles(order, a, b, c)
+    }
+
+    /// Extract Euler angles with the given Euler rotation order.
+    ///
+    /// Note if the input matrix contains scales, shears, or other non-rotation transformations then
+    /// the resulting Euler angles will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any input matrix column is not normalized when `glam_assert` is enabled.
+    #[inline]
+    #[must_use]
+    pub fn to_euler(&self, order: EulerRot) -> (f32, f32, f32) {
+        glam_assert!(
+            self.x_axis.is_normalized()
+                && self.y_axis.is_normalized()
+                && self.z_axis.is_normalized()
+        );
+        self.to_euler_angles(order)
     }
 
     /// Creates a 3D rotation matrix from `angle` (in radians) around the x axis.
diff --git a/src/f32/scalar/mat4.rs b/src/f32/scalar/mat4.rs
index b39bb3f..beec9b8 100644
--- a/src/f32/scalar/mat4.rs
+++ b/src/f32/scalar/mat4.rs
@@ -1,6 +1,11 @@
 // Generated from mat.rs.tera template. Edit the template, not the generated file.
 
-use crate::{f32::math, swizzles::*, DMat4, EulerRot, Mat3, Mat3A, Quat, Vec3, Vec3A, Vec4};
+use crate::{
+    euler::{FromEuler, ToEuler},
+    f32::math,
+    swizzles::*,
+    DMat4, EulerRot, Mat3, Mat3A, Quat, Vec3, Vec3A, Vec4,
+};
 #[cfg(not(target_arch = "spirv"))]
 use core::fmt;
 use core::iter::{Product, Sum};
@@ -393,8 +398,27 @@ impl Mat4 {
     #[inline]
     #[must_use]
     pub fn from_euler(order: EulerRot, a: f32, b: f32, c: f32) -> Self {
-        let quat = Quat::from_euler(order, a, b, c);
-        Self::from_quat(quat)
+        Self::from_euler_angles(order, a, b, c)
+    }
+
+    /// Extract Euler angles with the given Euler rotation order.
+    ///
+    /// Note if the upper 3x3 matrix contain scales, shears, or other non-rotation transformations
+    /// then the resulting Euler angles will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any column of the upper 3x3 rotation matrix is not normalized when
+    /// `glam_assert` is enabled.
+    #[inline]
+    #[must_use]
+    pub fn to_euler(&self, order: EulerRot) -> (f32, f32, f32) {
+        glam_assert!(
+            self.x_axis.xyz().is_normalized()
+                && self.y_axis.xyz().is_normalized()
+                && self.z_axis.xyz().is_normalized()
+        );
+        self.to_euler_angles(order)
     }
 
     /// Creates an affine transformation matrix containing a 3D rotation around the x axis of
diff --git a/src/f32/scalar/quat.rs b/src/f32/scalar/quat.rs
index a18bbc9..7487480 100644
--- a/src/f32/scalar/quat.rs
+++ b/src/f32/scalar/quat.rs
@@ -1,7 +1,7 @@
 // Generated from quat.rs.tera template. Edit the template, not the generated file.
 
 use crate::{
-    euler::{EulerFromQuaternion, EulerRot, EulerToQuaternion},
+    euler::{EulerRot, FromEuler, ToEuler},
     f32::math,
     DQuat, Mat3, Mat3A, Mat4, Vec2, Vec3, Vec3A, Vec4,
 };
@@ -183,14 +183,22 @@ impl Quat {
     #[inline]
     #[must_use]
     pub fn from_euler(euler: EulerRot, a: f32, b: f32, c: f32) -> Self {
-        euler.new_quat(a, b, c)
+        Self::from_euler_angles(euler, a, b, c)
     }
 
     /// From the columns of a 3x3 rotation matrix.
+    ///
+    /// Note if the input axes contain scales, shears, or other non-rotation transformations then
+    /// the output of this function is ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any axis is not normalized when `glam_assert` is enabled.
     #[inline]
     #[must_use]
     pub(crate) fn from_rotation_axes(x_axis: Vec3, y_axis: Vec3, z_axis: Vec3) -> Self {
-        // Based on https://github.com/microsoft/DirectXMath `XM$quaternionRotationMatrix`
+        glam_assert!(x_axis.is_normalized() && y_axis.is_normalized() && z_axis.is_normalized());
+        // Based on https://github.com/microsoft/DirectXMath `XMQuaternionRotationMatrix`
         let (m00, m01, m02) = x_axis.into();
         let (m10, m11, m12) = y_axis.into();
         let (m20, m21, m22) = z_axis.into();
@@ -248,6 +256,13 @@ impl Quat {
     }
 
     /// Creates a quaternion from a 3x3 rotation matrix.
+    ///
+    /// Note if the input matrix contain scales, shears, or other non-rotation transformations then
+    /// the resulting quaternion will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any input matrix column is not normalized when `glam_assert` is enabled.
     #[inline]
     #[must_use]
     pub fn from_mat3(mat: &Mat3) -> Self {
@@ -255,13 +270,28 @@ impl Quat {
     }
 
     /// Creates a quaternion from a 3x3 SIMD aligned rotation matrix.
+    ///
+    /// Note if the input matrix contain scales, shears, or other non-rotation transformations then
+    /// the resulting quaternion will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any input matrix column is not normalized when `glam_assert` is enabled.
     #[inline]
     #[must_use]
     pub fn from_mat3a(mat: &Mat3A) -> Self {
         Self::from_rotation_axes(mat.x_axis.into(), mat.y_axis.into(), mat.z_axis.into())
     }
 
-    /// Creates a quaternion from a 3x3 rotation matrix inside a homogeneous 4x4 matrix.
+    /// Creates a quaternion from the upper 3x3 rotation matrix inside a homogeneous 4x4 matrix.
+    ///
+    /// Note if the upper 3x3 matrix contain scales, shears, or other non-rotation transformations
+    /// then the resulting quaternion will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any column of the upper 3x3 rotation matrix is not normalized when
+    /// `glam_assert` is enabled.
     #[inline]
     #[must_use]
     pub fn from_mat4(mat: &Mat4) -> Self {
@@ -394,8 +424,8 @@ impl Quat {
     /// Returns the rotation angles for the given euler rotation sequence.
     #[inline]
     #[must_use]
-    pub fn to_euler(self, euler: EulerRot) -> (f32, f32, f32) {
-        euler.convert_quat(self)
+    pub fn to_euler(self, order: EulerRot) -> (f32, f32, f32) {
+        self.to_euler_angles(order)
     }
 
     /// `[x, y, z, w]`
@@ -698,6 +728,14 @@ impl Quat {
     }
 
     /// Creates a quaternion from a 3x3 rotation matrix inside a 3D affine transform.
+    ///
+    /// Note if the input affine matrix contain scales, shears, or other non-rotation
+    /// transformations then the resulting quaternion will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any input affine matrix column is not normalized when `glam_assert` is
+    /// enabled.
     #[inline]
     #[must_use]
     pub fn from_affine3(a: &crate::Affine3A) -> Self {
diff --git a/src/f32/sse2/mat3a.rs b/src/f32/sse2/mat3a.rs
index 24b5ce8..8d235c5 100644
--- a/src/f32/sse2/mat3a.rs
+++ b/src/f32/sse2/mat3a.rs
@@ -1,6 +1,11 @@
 // Generated from mat.rs.tera template. Edit the template, not the generated file.
 
-use crate::{f32::math, swizzles::*, DMat3, EulerRot, Mat2, Mat3, Mat4, Quat, Vec2, Vec3, Vec3A};
+use crate::{
+    euler::{FromEuler, ToEuler},
+    f32::math,
+    swizzles::*,
+    DMat3, EulerRot, Mat2, Mat3, Mat4, Quat, Vec2, Vec3, Vec3A,
+};
 #[cfg(not(target_arch = "spirv"))]
 use core::fmt;
 use core::iter::{Product, Sum};
@@ -223,8 +228,26 @@ impl Mat3A {
     #[inline]
     #[must_use]
     pub fn from_euler(order: EulerRot, a: f32, b: f32, c: f32) -> Self {
-        let quat = Quat::from_euler(order, a, b, c);
-        Self::from_quat(quat)
+        Self::from_euler_angles(order, a, b, c)
+    }
+
+    /// Extract Euler angles with the given Euler rotation order.
+    ///
+    /// Note if the input matrix contains scales, shears, or other non-rotation transformations then
+    /// the resulting Euler angles will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any input matrix column is not normalized when `glam_assert` is enabled.
+    #[inline]
+    #[must_use]
+    pub fn to_euler(&self, order: EulerRot) -> (f32, f32, f32) {
+        glam_assert!(
+            self.x_axis.is_normalized()
+                && self.y_axis.is_normalized()
+                && self.z_axis.is_normalized()
+        );
+        self.to_euler_angles(order)
     }
 
     /// Creates a 3D rotation matrix from `angle` (in radians) around the x axis.
diff --git a/src/f32/sse2/mat4.rs b/src/f32/sse2/mat4.rs
index c499b8e..81900f8 100644
--- a/src/f32/sse2/mat4.rs
+++ b/src/f32/sse2/mat4.rs
@@ -1,7 +1,11 @@
 // Generated from mat.rs.tera template. Edit the template, not the generated file.
 
 use crate::{
-    f32::math, sse2::*, swizzles::*, DMat4, EulerRot, Mat3, Mat3A, Quat, Vec3, Vec3A, Vec4,
+    euler::{FromEuler, ToEuler},
+    f32::math,
+    sse2::*,
+    swizzles::*,
+    DMat4, EulerRot, Mat3, Mat3A, Quat, Vec3, Vec3A, Vec4,
 };
 #[cfg(not(target_arch = "spirv"))]
 use core::fmt;
@@ -385,8 +389,27 @@ impl Mat4 {
     #[inline]
     #[must_use]
     pub fn from_euler(order: EulerRot, a: f32, b: f32, c: f32) -> Self {
-        let quat = Quat::from_euler(order, a, b, c);
-        Self::from_quat(quat)
+        Self::from_euler_angles(order, a, b, c)
+    }
+
+    /// Extract Euler angles with the given Euler rotation order.
+    ///
+    /// Note if the upper 3x3 matrix contain scales, shears, or other non-rotation transformations
+    /// then the resulting Euler angles will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any column of the upper 3x3 rotation matrix is not normalized when
+    /// `glam_assert` is enabled.
+    #[inline]
+    #[must_use]
+    pub fn to_euler(&self, order: EulerRot) -> (f32, f32, f32) {
+        glam_assert!(
+            self.x_axis.xyz().is_normalized()
+                && self.y_axis.xyz().is_normalized()
+                && self.z_axis.xyz().is_normalized()
+        );
+        self.to_euler_angles(order)
     }
 
     /// Creates an affine transformation matrix containing a 3D rotation around the x axis of
diff --git a/src/f32/sse2/quat.rs b/src/f32/sse2/quat.rs
index 6c8cdcb..5b369a4 100644
--- a/src/f32/sse2/quat.rs
+++ b/src/f32/sse2/quat.rs
@@ -1,7 +1,7 @@
 // Generated from quat.rs.tera template. Edit the template, not the generated file.
 
 use crate::{
-    euler::{EulerFromQuaternion, EulerRot, EulerToQuaternion},
+    euler::{EulerRot, FromEuler, ToEuler},
     f32::math,
     sse2::*,
     DQuat, Mat3, Mat3A, Mat4, Vec2, Vec3, Vec3A, Vec4,
@@ -183,14 +183,22 @@ impl Quat {
     #[inline]
     #[must_use]
     pub fn from_euler(euler: EulerRot, a: f32, b: f32, c: f32) -> Self {
-        euler.new_quat(a, b, c)
+        Self::from_euler_angles(euler, a, b, c)
     }
 
     /// From the columns of a 3x3 rotation matrix.
+    ///
+    /// Note if the input axes contain scales, shears, or other non-rotation transformations then
+    /// the output of this function is ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any axis is not normalized when `glam_assert` is enabled.
     #[inline]
     #[must_use]
     pub(crate) fn from_rotation_axes(x_axis: Vec3, y_axis: Vec3, z_axis: Vec3) -> Self {
-        // Based on https://github.com/microsoft/DirectXMath `XM$quaternionRotationMatrix`
+        glam_assert!(x_axis.is_normalized() && y_axis.is_normalized() && z_axis.is_normalized());
+        // Based on https://github.com/microsoft/DirectXMath `XMQuaternionRotationMatrix`
         let (m00, m01, m02) = x_axis.into();
         let (m10, m11, m12) = y_axis.into();
         let (m20, m21, m22) = z_axis.into();
@@ -248,6 +256,13 @@ impl Quat {
     }
 
     /// Creates a quaternion from a 3x3 rotation matrix.
+    ///
+    /// Note if the input matrix contain scales, shears, or other non-rotation transformations then
+    /// the resulting quaternion will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any input matrix column is not normalized when `glam_assert` is enabled.
     #[inline]
     #[must_use]
     pub fn from_mat3(mat: &Mat3) -> Self {
@@ -255,13 +270,28 @@ impl Quat {
     }
 
     /// Creates a quaternion from a 3x3 SIMD aligned rotation matrix.
+    ///
+    /// Note if the input matrix contain scales, shears, or other non-rotation transformations then
+    /// the resulting quaternion will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any input matrix column is not normalized when `glam_assert` is enabled.
     #[inline]
     #[must_use]
     pub fn from_mat3a(mat: &Mat3A) -> Self {
         Self::from_rotation_axes(mat.x_axis.into(), mat.y_axis.into(), mat.z_axis.into())
     }
 
-    /// Creates a quaternion from a 3x3 rotation matrix inside a homogeneous 4x4 matrix.
+    /// Creates a quaternion from the upper 3x3 rotation matrix inside a homogeneous 4x4 matrix.
+    ///
+    /// Note if the upper 3x3 matrix contain scales, shears, or other non-rotation transformations
+    /// then the resulting quaternion will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any column of the upper 3x3 rotation matrix is not normalized when
+    /// `glam_assert` is enabled.
     #[inline]
     #[must_use]
     pub fn from_mat4(mat: &Mat4) -> Self {
@@ -394,8 +424,8 @@ impl Quat {
     /// Returns the rotation angles for the given euler rotation sequence.
     #[inline]
     #[must_use]
-    pub fn to_euler(self, euler: EulerRot) -> (f32, f32, f32) {
-        euler.convert_quat(self)
+    pub fn to_euler(self, order: EulerRot) -> (f32, f32, f32) {
+        self.to_euler_angles(order)
     }
 
     /// `[x, y, z, w]`
@@ -737,6 +767,14 @@ impl Quat {
     }
 
     /// Creates a quaternion from a 3x3 rotation matrix inside a 3D affine transform.
+    ///
+    /// Note if the input affine matrix contain scales, shears, or other non-rotation
+    /// transformations then the resulting quaternion will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any input affine matrix column is not normalized when `glam_assert` is
+    /// enabled.
     #[inline]
     #[must_use]
     pub fn from_affine3(a: &crate::Affine3A) -> Self {
diff --git a/src/f32/wasm32/mat3a.rs b/src/f32/wasm32/mat3a.rs
index 9cd6573..4e35b9b 100644
--- a/src/f32/wasm32/mat3a.rs
+++ b/src/f32/wasm32/mat3a.rs
@@ -1,6 +1,11 @@
 // Generated from mat.rs.tera template. Edit the template, not the generated file.
 
-use crate::{f32::math, swizzles::*, DMat3, EulerRot, Mat2, Mat3, Mat4, Quat, Vec2, Vec3, Vec3A};
+use crate::{
+    euler::{FromEuler, ToEuler},
+    f32::math,
+    swizzles::*,
+    DMat3, EulerRot, Mat2, Mat3, Mat4, Quat, Vec2, Vec3, Vec3A,
+};
 #[cfg(not(target_arch = "spirv"))]
 use core::fmt;
 use core::iter::{Product, Sum};
@@ -220,8 +225,26 @@ impl Mat3A {
     #[inline]
     #[must_use]
     pub fn from_euler(order: EulerRot, a: f32, b: f32, c: f32) -> Self {
-        let quat = Quat::from_euler(order, a, b, c);
-        Self::from_quat(quat)
+        Self::from_euler_angles(order, a, b, c)
+    }
+
+    /// Extract Euler angles with the given Euler rotation order.
+    ///
+    /// Note if the input matrix contains scales, shears, or other non-rotation transformations then
+    /// the resulting Euler angles will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any input matrix column is not normalized when `glam_assert` is enabled.
+    #[inline]
+    #[must_use]
+    pub fn to_euler(&self, order: EulerRot) -> (f32, f32, f32) {
+        glam_assert!(
+            self.x_axis.is_normalized()
+                && self.y_axis.is_normalized()
+                && self.z_axis.is_normalized()
+        );
+        self.to_euler_angles(order)
     }
 
     /// Creates a 3D rotation matrix from `angle` (in radians) around the x axis.
diff --git a/src/f32/wasm32/mat4.rs b/src/f32/wasm32/mat4.rs
index 0fd7308..290dda3 100644
--- a/src/f32/wasm32/mat4.rs
+++ b/src/f32/wasm32/mat4.rs
@@ -1,7 +1,11 @@
 // Generated from mat.rs.tera template. Edit the template, not the generated file.
 
 use crate::{
-    f32::math, swizzles::*, wasm32::*, DMat4, EulerRot, Mat3, Mat3A, Quat, Vec3, Vec3A, Vec4,
+    euler::{FromEuler, ToEuler},
+    f32::math,
+    swizzles::*,
+    wasm32::*,
+    DMat4, EulerRot, Mat3, Mat3A, Quat, Vec3, Vec3A, Vec4,
 };
 #[cfg(not(target_arch = "spirv"))]
 use core::fmt;
@@ -382,8 +386,27 @@ impl Mat4 {
     #[inline]
     #[must_use]
     pub fn from_euler(order: EulerRot, a: f32, b: f32, c: f32) -> Self {
-        let quat = Quat::from_euler(order, a, b, c);
-        Self::from_quat(quat)
+        Self::from_euler_angles(order, a, b, c)
+    }
+
+    /// Extract Euler angles with the given Euler rotation order.
+    ///
+    /// Note if the upper 3x3 matrix contain scales, shears, or other non-rotation transformations
+    /// then the resulting Euler angles will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any column of the upper 3x3 rotation matrix is not normalized when
+    /// `glam_assert` is enabled.
+    #[inline]
+    #[must_use]
+    pub fn to_euler(&self, order: EulerRot) -> (f32, f32, f32) {
+        glam_assert!(
+            self.x_axis.xyz().is_normalized()
+                && self.y_axis.xyz().is_normalized()
+                && self.z_axis.xyz().is_normalized()
+        );
+        self.to_euler_angles(order)
     }
 
     /// Creates an affine transformation matrix containing a 3D rotation around the x axis of
diff --git a/src/f32/wasm32/quat.rs b/src/f32/wasm32/quat.rs
index 4bdf3eb..e05b2bb 100644
--- a/src/f32/wasm32/quat.rs
+++ b/src/f32/wasm32/quat.rs
@@ -1,7 +1,7 @@
 // Generated from quat.rs.tera template. Edit the template, not the generated file.
 
 use crate::{
-    euler::{EulerFromQuaternion, EulerRot, EulerToQuaternion},
+    euler::{EulerRot, FromEuler, ToEuler},
     f32::math,
     wasm32::*,
     DQuat, Mat3, Mat3A, Mat4, Vec2, Vec3, Vec3A, Vec4,
@@ -175,14 +175,22 @@ impl Quat {
     #[inline]
     #[must_use]
     pub fn from_euler(euler: EulerRot, a: f32, b: f32, c: f32) -> Self {
-        euler.new_quat(a, b, c)
+        Self::from_euler_angles(euler, a, b, c)
     }
 
     /// From the columns of a 3x3 rotation matrix.
+    ///
+    /// Note if the input axes contain scales, shears, or other non-rotation transformations then
+    /// the output of this function is ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any axis is not normalized when `glam_assert` is enabled.
     #[inline]
     #[must_use]
     pub(crate) fn from_rotation_axes(x_axis: Vec3, y_axis: Vec3, z_axis: Vec3) -> Self {
-        // Based on https://github.com/microsoft/DirectXMath `XM$quaternionRotationMatrix`
+        glam_assert!(x_axis.is_normalized() && y_axis.is_normalized() && z_axis.is_normalized());
+        // Based on https://github.com/microsoft/DirectXMath `XMQuaternionRotationMatrix`
         let (m00, m01, m02) = x_axis.into();
         let (m10, m11, m12) = y_axis.into();
         let (m20, m21, m22) = z_axis.into();
@@ -240,6 +248,13 @@ impl Quat {
     }
 
     /// Creates a quaternion from a 3x3 rotation matrix.
+    ///
+    /// Note if the input matrix contain scales, shears, or other non-rotation transformations then
+    /// the resulting quaternion will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any input matrix column is not normalized when `glam_assert` is enabled.
     #[inline]
     #[must_use]
     pub fn from_mat3(mat: &Mat3) -> Self {
@@ -247,13 +262,28 @@ impl Quat {
     }
 
     /// Creates a quaternion from a 3x3 SIMD aligned rotation matrix.
+    ///
+    /// Note if the input matrix contain scales, shears, or other non-rotation transformations then
+    /// the resulting quaternion will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any input matrix column is not normalized when `glam_assert` is enabled.
     #[inline]
     #[must_use]
     pub fn from_mat3a(mat: &Mat3A) -> Self {
         Self::from_rotation_axes(mat.x_axis.into(), mat.y_axis.into(), mat.z_axis.into())
     }
 
-    /// Creates a quaternion from a 3x3 rotation matrix inside a homogeneous 4x4 matrix.
+    /// Creates a quaternion from the upper 3x3 rotation matrix inside a homogeneous 4x4 matrix.
+    ///
+    /// Note if the upper 3x3 matrix contain scales, shears, or other non-rotation transformations
+    /// then the resulting quaternion will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any column of the upper 3x3 rotation matrix is not normalized when
+    /// `glam_assert` is enabled.
     #[inline]
     #[must_use]
     pub fn from_mat4(mat: &Mat4) -> Self {
@@ -386,8 +416,8 @@ impl Quat {
     /// Returns the rotation angles for the given euler rotation sequence.
     #[inline]
     #[must_use]
-    pub fn to_euler(self, euler: EulerRot) -> (f32, f32, f32) {
-        euler.convert_quat(self)
+    pub fn to_euler(self, order: EulerRot) -> (f32, f32, f32) {
+        self.to_euler_angles(order)
     }
 
     /// `[x, y, z, w]`
@@ -728,6 +758,14 @@ impl Quat {
     }
 
     /// Creates a quaternion from a 3x3 rotation matrix inside a 3D affine transform.
+    ///
+    /// Note if the input affine matrix contain scales, shears, or other non-rotation
+    /// transformations then the resulting quaternion will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any input affine matrix column is not normalized when `glam_assert` is
+    /// enabled.
     #[inline]
     #[must_use]
     pub fn from_affine3(a: &crate::Affine3A) -> Self {
diff --git a/src/f64/dmat3.rs b/src/f64/dmat3.rs
index 25162f9..687d176 100644
--- a/src/f64/dmat3.rs
+++ b/src/f64/dmat3.rs
@@ -1,6 +1,11 @@
 // Generated from mat.rs.tera template. Edit the template, not the generated file.
 
-use crate::{f64::math, swizzles::*, DMat2, DMat4, DQuat, DVec2, DVec3, EulerRot, Mat3};
+use crate::{
+    euler::{FromEuler, ToEuler},
+    f64::math,
+    swizzles::*,
+    DMat2, DMat4, DQuat, DVec2, DVec3, EulerRot, Mat3,
+};
 #[cfg(not(target_arch = "spirv"))]
 use core::fmt;
 use core::iter::{Product, Sum};
@@ -221,8 +226,26 @@ impl DMat3 {
     #[inline]
     #[must_use]
     pub fn from_euler(order: EulerRot, a: f64, b: f64, c: f64) -> Self {
-        let quat = DQuat::from_euler(order, a, b, c);
-        Self::from_quat(quat)
+        Self::from_euler_angles(order, a, b, c)
+    }
+
+    /// Extract Euler angles with the given Euler rotation order.
+    ///
+    /// Note if the input matrix contains scales, shears, or other non-rotation transformations then
+    /// the resulting Euler angles will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any input matrix column is not normalized when `glam_assert` is enabled.
+    #[inline]
+    #[must_use]
+    pub fn to_euler(&self, order: EulerRot) -> (f64, f64, f64) {
+        glam_assert!(
+            self.x_axis.is_normalized()
+                && self.y_axis.is_normalized()
+                && self.z_axis.is_normalized()
+        );
+        self.to_euler_angles(order)
     }
 
     /// Creates a 3D rotation matrix from `angle` (in radians) around the x axis.
diff --git a/src/f64/dmat4.rs b/src/f64/dmat4.rs
index d1d10b2..82c0071 100644
--- a/src/f64/dmat4.rs
+++ b/src/f64/dmat4.rs
@@ -1,6 +1,11 @@
 // Generated from mat.rs.tera template. Edit the template, not the generated file.
 
-use crate::{f64::math, swizzles::*, DMat3, DQuat, DVec3, DVec4, EulerRot, Mat4};
+use crate::{
+    euler::{FromEuler, ToEuler},
+    f64::math,
+    swizzles::*,
+    DMat3, DQuat, DVec3, DVec4, EulerRot, Mat4,
+};
 #[cfg(not(target_arch = "spirv"))]
 use core::fmt;
 use core::iter::{Product, Sum};
@@ -375,8 +380,27 @@ impl DMat4 {
     #[inline]
     #[must_use]
     pub fn from_euler(order: EulerRot, a: f64, b: f64, c: f64) -> Self {
-        let quat = DQuat::from_euler(order, a, b, c);
-        Self::from_quat(quat)
+        Self::from_euler_angles(order, a, b, c)
+    }
+
+    /// Extract Euler angles with the given Euler rotation order.
+    ///
+    /// Note if the upper 3x3 matrix contain scales, shears, or other non-rotation transformations
+    /// then the resulting Euler angles will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any column of the upper 3x3 rotation matrix is not normalized when
+    /// `glam_assert` is enabled.
+    #[inline]
+    #[must_use]
+    pub fn to_euler(&self, order: EulerRot) -> (f64, f64, f64) {
+        glam_assert!(
+            self.x_axis.xyz().is_normalized()
+                && self.y_axis.xyz().is_normalized()
+                && self.z_axis.xyz().is_normalized()
+        );
+        self.to_euler_angles(order)
     }
 
     /// Creates an affine transformation matrix containing a 3D rotation around the x axis of
diff --git a/src/f64/dquat.rs b/src/f64/dquat.rs
index b4433be..55bd338 100644
--- a/src/f64/dquat.rs
+++ b/src/f64/dquat.rs
@@ -1,7 +1,7 @@
 // Generated from quat.rs.tera template. Edit the template, not the generated file.
 
 use crate::{
-    euler::{EulerFromQuaternion, EulerRot, EulerToQuaternion},
+    euler::{EulerRot, FromEuler, ToEuler},
     f64::math,
     DMat3, DMat4, DVec2, DVec3, DVec4, Quat,
 };
@@ -179,14 +179,22 @@ impl DQuat {
     #[inline]
     #[must_use]
     pub fn from_euler(euler: EulerRot, a: f64, b: f64, c: f64) -> Self {
-        euler.new_quat(a, b, c)
+        Self::from_euler_angles(euler, a, b, c)
     }
 
     /// From the columns of a 3x3 rotation matrix.
+    ///
+    /// Note if the input axes contain scales, shears, or other non-rotation transformations then
+    /// the output of this function is ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any axis is not normalized when `glam_assert` is enabled.
     #[inline]
     #[must_use]
     pub(crate) fn from_rotation_axes(x_axis: DVec3, y_axis: DVec3, z_axis: DVec3) -> Self {
-        // Based on https://github.com/microsoft/DirectXMath `XM$quaternionRotationMatrix`
+        glam_assert!(x_axis.is_normalized() && y_axis.is_normalized() && z_axis.is_normalized());
+        // Based on https://github.com/microsoft/DirectXMath `XMQuaternionRotationMatrix`
         let (m00, m01, m02) = x_axis.into();
         let (m10, m11, m12) = y_axis.into();
         let (m20, m21, m22) = z_axis.into();
@@ -244,13 +252,28 @@ impl DQuat {
     }
 
     /// Creates a quaternion from a 3x3 rotation matrix.
+    ///
+    /// Note if the input matrix contain scales, shears, or other non-rotation transformations then
+    /// the resulting quaternion will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any input matrix column is not normalized when `glam_assert` is enabled.
     #[inline]
     #[must_use]
     pub fn from_mat3(mat: &DMat3) -> Self {
         Self::from_rotation_axes(mat.x_axis, mat.y_axis, mat.z_axis)
     }
 
-    /// Creates a quaternion from a 3x3 rotation matrix inside a homogeneous 4x4 matrix.
+    /// Creates a quaternion from the upper 3x3 rotation matrix inside a homogeneous 4x4 matrix.
+    ///
+    /// Note if the upper 3x3 matrix contain scales, shears, or other non-rotation transformations
+    /// then the resulting quaternion will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any column of the upper 3x3 rotation matrix is not normalized when
+    /// `glam_assert` is enabled.
     #[inline]
     #[must_use]
     pub fn from_mat4(mat: &DMat4) -> Self {
@@ -383,8 +406,8 @@ impl DQuat {
     /// Returns the rotation angles for the given euler rotation sequence.
     #[inline]
     #[must_use]
-    pub fn to_euler(self, euler: EulerRot) -> (f64, f64, f64) {
-        euler.convert_quat(self)
+    pub fn to_euler(self, order: EulerRot) -> (f64, f64, f64) {
+        self.to_euler_angles(order)
     }
 
     /// `[x, y, z, w]`
@@ -687,6 +710,14 @@ impl DQuat {
     }
 
     /// Creates a quaternion from a 3x3 rotation matrix inside a 3D affine transform.
+    ///
+    /// Note if the input affine matrix contain scales, shears, or other non-rotation
+    /// transformations then the resulting quaternion will be ill-defined.
+    ///
+    /// # Panics
+    ///
+    /// Will panic if any input affine matrix column is not normalized when `glam_assert` is
+    /// enabled.
     #[inline]
     #[must_use]
     pub fn from_affine3(a: &crate::DAffine3) -> Self {
diff --git a/src/f64/math.rs b/src/f64/math.rs
index 45ace5c..2885342 100644
--- a/src/f64/math.rs
+++ b/src/f64/math.rs
@@ -10,11 +10,6 @@ mod libm_math {
         libm::acos(f.clamp(-1.0, 1.0))
     }
 
-    #[inline(always)]
-    pub(crate) fn asin(f: f64) -> f64 {
-        libm::asin(f)
-    }
-
     #[inline(always)]
     pub(crate) fn atan2(f: f64, other: f64) -> f64 {
         libm::atan2(f, other)
@@ -122,11 +117,6 @@ mod std_math {
         f64::acos(f64::clamp(f, -1.0, 1.0))
     }
 
-    #[inline(always)]
-    pub(crate) fn asin(f: f64) -> f64 {
-        f64::asin(f)
-    }
-
     #[inline(always)]
     pub(crate) fn atan2(f: f64, other: f64) -> f64 {
         f64::atan2(f, other)
diff --git a/src/features/impl_serde.rs b/src/features/impl_serde.rs
index ab0db6c..497a909 100644
--- a/src/features/impl_serde.rs
+++ b/src/features/impl_serde.rs
@@ -1117,6 +1117,60 @@ mod euler {
                 EulerRot::XZY => {
                     serde::Serializer::serialize_unit_variant(serializer, "EulerRot", 5u32, "XZY")
                 }
+                EulerRot::ZYZ => {
+                    serde::Serializer::serialize_unit_variant(serializer, "EulerRot", 6u32, "ZYZ")
+                }
+                EulerRot::ZXZ => {
+                    serde::Serializer::serialize_unit_variant(serializer, "EulerRot", 7u32, "ZXZ")
+                }
+                EulerRot::YXY => {
+                    serde::Serializer::serialize_unit_variant(serializer, "EulerRot", 8u32, "YXY")
+                }
+                EulerRot::YZY => {
+                    serde::Serializer::serialize_unit_variant(serializer, "EulerRot", 9u32, "YZY")
+                }
+                EulerRot::XYX => {
+                    serde::Serializer::serialize_unit_variant(serializer, "EulerRot", 10u32, "XYX")
+                }
+                EulerRot::XZX => {
+                    serde::Serializer::serialize_unit_variant(serializer, "EulerRot", 11u32, "XZX")
+                }
+                EulerRot::ZYXEx => serde::Serializer::serialize_unit_variant(
+                    serializer, "EulerRot", 12u32, "ZYXEx",
+                ),
+                EulerRot::ZXYEx => serde::Serializer::serialize_unit_variant(
+                    serializer, "EulerRot", 13u32, "ZXYEx",
+                ),
+                EulerRot::YXZEx => serde::Serializer::serialize_unit_variant(
+                    serializer, "EulerRot", 14u32, "YXZEx",
+                ),
+                EulerRot::YZXEx => serde::Serializer::serialize_unit_variant(
+                    serializer, "EulerRot", 15u32, "YZXEx",
+                ),
+                EulerRot::XYZEx => serde::Serializer::serialize_unit_variant(
+                    serializer, "EulerRot", 16u32, "XYZEx",
+                ),
+                EulerRot::XZYEx => serde::Serializer::serialize_unit_variant(
+                    serializer, "EulerRot", 17u32, "XZYEx",
+                ),
+                EulerRot::ZYZEx => serde::Serializer::serialize_unit_variant(
+                    serializer, "EulerRot", 18u32, "ZYZEx",
+                ),
+                EulerRot::ZXZEx => serde::Serializer::serialize_unit_variant(
+                    serializer, "EulerRot", 19u32, "ZXZEx",
+                ),
+                EulerRot::YXYEx => serde::Serializer::serialize_unit_variant(
+                    serializer, "EulerRot", 20u32, "YXYEx",
+                ),
+                EulerRot::YZYEx => serde::Serializer::serialize_unit_variant(
+                    serializer, "EulerRot", 21u32, "YZYEx",
+                ),
+                EulerRot::XYXEx => serde::Serializer::serialize_unit_variant(
+                    serializer, "EulerRot", 22u32, "XYXEx",
+                ),
+                EulerRot::XZXEx => serde::Serializer::serialize_unit_variant(
+                    serializer, "EulerRot", 23u32, "XZXEx",
+                ),
             }
         }
     }
@@ -1134,6 +1188,24 @@ mod euler {
                 YZX,
                 XYZ,
                 XZY,
+                ZYZ,
+                ZXZ,
+                YXY,
+                YZY,
+                XYX,
+                XZX,
+                ZYXEx,
+                ZXYEx,
+                YXZEx,
+                YZXEx,
+                XYZEx,
+                XZYEx,
+                ZYZEx,
+                ZXZEx,
+                YXYEx,
+                YZYEx,
+                XYXEx,
+                XZXEx,
             }
             struct FieldVisitor;
 
@@ -1153,9 +1225,28 @@ mod euler {
                         3u64 => Ok(Field::YZX),
                         4u64 => Ok(Field::XYZ),
                         5u64 => Ok(Field::XZY),
+                        6u64 => Ok(Field::ZYZ),
+                        7u64 => Ok(Field::ZXZ),
+                        8u64 => Ok(Field::YXY),
+                        9u64 => Ok(Field::YZY),
+                        10u64 => Ok(Field::XYX),
+                        11u64 => Ok(Field::XZX),
+
+                        12u64 => Ok(Field::ZYXEx),
+                        13u64 => Ok(Field::ZXYEx),
+                        14u64 => Ok(Field::YXZEx),
+                        15u64 => Ok(Field::YZXEx),
+                        16u64 => Ok(Field::XYZEx),
+                        17u64 => Ok(Field::XZYEx),
+                        18u64 => Ok(Field::ZYZEx),
+                        19u64 => Ok(Field::ZXZEx),
+                        20u64 => Ok(Field::YXYEx),
+                        21u64 => Ok(Field::YZYEx),
+                        22u64 => Ok(Field::XYXEx),
+                        23u64 => Ok(Field::XZXEx),
                         _ => Err(serde::de::Error::invalid_value(
                             serde::de::Unexpected::Unsigned(value),
-                            &"variant index 0 <= i < 6",
+                            &"variant index 0 <= i < 24",
                         )),
                     }
                 }
@@ -1170,6 +1261,24 @@ mod euler {
                         "YZX" => Ok(Field::YZX),
                         "XYZ" => Ok(Field::XYZ),
                         "XZY" => Ok(Field::XZY),
+                        "ZYZ" => Ok(Field::ZYZ),
+                        "ZXZ" => Ok(Field::ZXZ),
+                        "YXY" => Ok(Field::YXY),
+                        "YZY" => Ok(Field::YZY),
+                        "XYX" => Ok(Field::XYX),
+                        "XZX" => Ok(Field::XZX),
+                        "ZYXEx" => Ok(Field::ZYXEx),
+                        "ZXYEx" => Ok(Field::ZXYEx),
+                        "YXZEx" => Ok(Field::YXZEx),
+                        "YZXEx" => Ok(Field::YZXEx),
+                        "XYZEx" => Ok(Field::XYZEx),
+                        "XZYEx" => Ok(Field::XZYEx),
+                        "ZYZEx" => Ok(Field::ZYZEx),
+                        "ZXZEx" => Ok(Field::ZXZEx),
+                        "YXYEx" => Ok(Field::YXYEx),
+                        "YZYEx" => Ok(Field::YZYEx),
+                        "XYXEx" => Ok(Field::XYXEx),
+                        "XZXEx" => Ok(Field::XZXEx),
                         _ => Err(serde::de::Error::unknown_variant(value, VARIANTS)),
                     }
                 }
@@ -1184,6 +1293,24 @@ mod euler {
                         b"YZX" => Ok(Field::YZX),
                         b"XYZ" => Ok(Field::XYZ),
                         b"XZY" => Ok(Field::XZY),
+                        b"ZYZ" => Ok(Field::ZYZ),
+                        b"ZXZ" => Ok(Field::ZXZ),
+                        b"YXY" => Ok(Field::YXY),
+                        b"YZY" => Ok(Field::YZY),
+                        b"XYX" => Ok(Field::XYX),
+                        b"XZX" => Ok(Field::XZX),
+                        b"ZYXEx" => Ok(Field::ZYXEx),
+                        b"ZXYEx" => Ok(Field::ZXYEx),
+                        b"YXZEx" => Ok(Field::YXZEx),
+                        b"YZXEx" => Ok(Field::YZXEx),
+                        b"XYZEx" => Ok(Field::XYZEx),
+                        b"XZYEx" => Ok(Field::XZYEx),
+                        b"ZYZEx" => Ok(Field::ZYZEx),
+                        b"ZXZEx" => Ok(Field::ZXZEx),
+                        b"YXYEx" => Ok(Field::YXYEx),
+                        b"YZYEx" => Ok(Field::YZYEx),
+                        b"XYXEx" => Ok(Field::XYXEx),
+                        b"XZXEx" => Ok(Field::XZXEx),
                         _ => {
                             #[cfg(feature = "std")]
                             let value = &String::from_utf8_lossy(value);
@@ -1245,10 +1372,87 @@ mod euler {
                             serde::de::VariantAccess::unit_variant(variant)?;
                             Ok(EulerRot::XZY)
                         }
+
+                        (Field::ZYZ, variant) => {
+                            serde::de::VariantAccess::unit_variant(variant)?;
+                            Ok(EulerRot::ZYZ)
+                        }
+                        (Field::ZXZ, variant) => {
+                            serde::de::VariantAccess::unit_variant(variant)?;
+                            Ok(EulerRot::ZXZ)
+                        }
+                        (Field::YXY, variant) => {
+                            serde::de::VariantAccess::unit_variant(variant)?;
+                            Ok(EulerRot::YXY)
+                        }
+                        (Field::YZY, variant) => {
+                            serde::de::VariantAccess::unit_variant(variant)?;
+                            Ok(EulerRot::YZY)
+                        }
+                        (Field::XYX, variant) => {
+                            serde::de::VariantAccess::unit_variant(variant)?;
+                            Ok(EulerRot::XYX)
+                        }
+                        (Field::XZX, variant) => {
+                            serde::de::VariantAccess::unit_variant(variant)?;
+                            Ok(EulerRot::XZX)
+                        }
+                        (Field::ZYXEx, variant) => {
+                            serde::de::VariantAccess::unit_variant(variant)?;
+                            Ok(EulerRot::ZYXEx)
+                        }
+                        (Field::ZXYEx, variant) => {
+                            serde::de::VariantAccess::unit_variant(variant)?;
+                            Ok(EulerRot::ZXYEx)
+                        }
+                        (Field::YXZEx, variant) => {
+                            serde::de::VariantAccess::unit_variant(variant)?;
+                            Ok(EulerRot::YXZEx)
+                        }
+                        (Field::YZXEx, variant) => {
+                            serde::de::VariantAccess::unit_variant(variant)?;
+                            Ok(EulerRot::YZXEx)
+                        }
+                        (Field::XYZEx, variant) => {
+                            serde::de::VariantAccess::unit_variant(variant)?;
+                            Ok(EulerRot::XYZEx)
+                        }
+                        (Field::XZYEx, variant) => {
+                            serde::de::VariantAccess::unit_variant(variant)?;
+                            Ok(EulerRot::XZYEx)
+                        }
+                        (Field::ZYZEx, variant) => {
+                            serde::de::VariantAccess::unit_variant(variant)?;
+                            Ok(EulerRot::ZYZEx)
+                        }
+                        (Field::ZXZEx, variant) => {
+                            serde::de::VariantAccess::unit_variant(variant)?;
+                            Ok(EulerRot::ZXZEx)
+                        }
+                        (Field::YXYEx, variant) => {
+                            serde::de::VariantAccess::unit_variant(variant)?;
+                            Ok(EulerRot::YXYEx)
+                        }
+                        (Field::YZYEx, variant) => {
+                            serde::de::VariantAccess::unit_variant(variant)?;
+                            Ok(EulerRot::YZYEx)
+                        }
+                        (Field::XYXEx, variant) => {
+                            serde::de::VariantAccess::unit_variant(variant)?;
+                            Ok(EulerRot::XYXEx)
+                        }
+                        (Field::XZXEx, variant) => {
+                            serde::de::VariantAccess::unit_variant(variant)?;
+                            Ok(EulerRot::XZXEx)
+                        }
                     }
                 }
             }
-            const VARIANTS: &[&str] = &["ZYX", "ZXY", "YXZ", "YZX", "XYZ", "XZY"];
+            const VARIANTS: &[&str] = &[
+                "ZYX", "ZXY", "YXZ", "YZX", "XYZ", "XZY", "ZYZ", "ZXZ", "YXY", "YZY", "XYX", "XZX",
+                "ZYXEx", "ZXYEx", "YXZEx", "YZXEx", "XYZEx", "XZYEx", "ZYZEx", "ZXZEx", "YXYEx",
+                "YZYEx", "XYXEx", "XZXEx",
+            ];
             serde::Deserializer::deserialize_enum(
                 deserializer,
                 "EulerRot",
@@ -1263,10 +1467,37 @@ mod euler {
 
     #[test]
     fn test_euler_rot_serde() {
-        let a = EulerRot::XYZ;
-        let serialized = serde_json::to_string(&a).unwrap();
-        assert_eq!("\"XYZ\"", serialized);
-        let deserialized = serde_json::from_str(&serialized).unwrap();
-        assert_eq!(a, deserialized);
+        const PAIRS: [(EulerRot, &str); 24] = [
+            (EulerRot::ZYX, "\"ZYX\""),
+            (EulerRot::ZXY, "\"ZXY\""),
+            (EulerRot::YXZ, "\"YXZ\""),
+            (EulerRot::YZX, "\"YZX\""),
+            (EulerRot::XYZ, "\"XYZ\""),
+            (EulerRot::XZY, "\"XZY\""),
+            (EulerRot::ZYZ, "\"ZYZ\""),
+            (EulerRot::ZXZ, "\"ZXZ\""),
+            (EulerRot::YXY, "\"YXY\""),
+            (EulerRot::YZY, "\"YZY\""),
+            (EulerRot::XYX, "\"XYX\""),
+            (EulerRot::XZX, "\"XZX\""),
+            (EulerRot::ZYXEx, "\"ZYXEx\""),
+            (EulerRot::ZXYEx, "\"ZXYEx\""),
+            (EulerRot::YXZEx, "\"YXZEx\""),
+            (EulerRot::YZXEx, "\"YZXEx\""),
+            (EulerRot::XYZEx, "\"XYZEx\""),
+            (EulerRot::XZYEx, "\"XZYEx\""),
+            (EulerRot::ZYZEx, "\"ZYZEx\""),
+            (EulerRot::ZXZEx, "\"ZXZEx\""),
+            (EulerRot::YXYEx, "\"YXYEx\""),
+            (EulerRot::YZYEx, "\"YZYEx\""),
+            (EulerRot::XYXEx, "\"XYXEx\""),
+            (EulerRot::XZXEx, "\"XZXEx\""),
+        ];
+        for (enum_value, enum_string) in PAIRS {
+            let serialized = serde_json::to_string(&enum_value).unwrap();
+            assert_eq!(enum_string, serialized);
+            let deserialized = serde_json::from_str(&serialized).unwrap();
+            assert_eq!(enum_value, deserialized);
+        }
     }
 }


Test patch:
diff --git a/tests/euler.rs b/tests/euler.rs
index b66880e..4cf6d35 100644
--- a/tests/euler.rs
+++ b/tests/euler.rs
@@ -1,117 +1,280 @@
 #[macro_use]
 mod support;
 
-/// Helper to get the 'canonical' version of a `Quat`. We define the canonical of quat `q` as:
-/// * `q`, if q.w > epsilon
-/// * `-q`, if q.w < -epsilon
-/// * `(0, 0, 0, 1)` otherwise
-/// The rationale is that q and -q represent the same rotation, and any (_, _, _, 0) represent no rotation at all.
-trait CanonicalQuat: Copy {
-    fn canonical(self) -> Self;
-}
+mod euler {
+    use glam::*;
+    use std::ops::RangeInclusive;
+
+    /// Helper to get the 'canonical' version of a `Quat`. We define the canonical of quat `q` as:
+    /// * `q`, if q.w > epsilon
+    /// * `-q`, if q.w < -epsilon
+    /// * `(0, 0, 0, 1)` otherwise
+    /// The rationale is that q and -q represent the same rotation, and any (_, _, _, 0) represent no rotation at all.
+    trait CanonicalQuat: Copy {
+        fn canonical(self) -> Self;
+    }
+
+    /// Helper to set some alternative epsilons based on the floating point type used
+    trait EulerEpsilon {
+        /// epsilon for comparing quaternion round-tripped through eulers (quat -> euler -> quat)
+        const E_EPS: f32;
+    }
+
+    impl EulerEpsilon for f32 {
+        const E_EPS: f32 = 2e-6;
+    }
+
+    impl EulerEpsilon for f64 {
+        const E_EPS: f32 = 1e-8;
+    }
+
+    fn axis_order(order: EulerRot) -> (usize, usize, usize) {
+        match order {
+            EulerRot::XYZ => (0, 1, 2),
+            EulerRot::XYX => (0, 1, 0),
+            EulerRot::XZY => (0, 2, 1),
+            EulerRot::XZX => (0, 2, 0),
+            EulerRot::YZX => (1, 2, 0),
+            EulerRot::YZY => (1, 2, 1),
+            EulerRot::YXZ => (1, 0, 2),
+            EulerRot::YXY => (1, 0, 1),
+            EulerRot::ZXY => (2, 0, 1),
+            EulerRot::ZXZ => (2, 0, 2),
+            EulerRot::ZYX => (2, 1, 0),
+            EulerRot::ZYZ => (2, 1, 2),
+            EulerRot::ZYXEx => (2, 1, 0),
+            EulerRot::XYXEx => (0, 1, 0),
+            EulerRot::YZXEx => (1, 2, 0),
+            EulerRot::XZXEx => (0, 2, 0),
+            EulerRot::XZYEx => (0, 2, 1),
+            EulerRot::YZYEx => (1, 2, 1),
+            EulerRot::ZXYEx => (2, 0, 1),
+            EulerRot::YXYEx => (1, 0, 1),
+            EulerRot::YXZEx => (1, 0, 2),
+            EulerRot::ZXZEx => (2, 0, 2),
+            EulerRot::XYZEx => (0, 1, 2),
+            EulerRot::ZYZEx => (2, 1, 2),
+        }
+    }
+
+    fn is_intrinsic(order: EulerRot) -> bool {
+        match order {
+            EulerRot::XYZ
+            | EulerRot::XYX
+            | EulerRot::XZY
+            | EulerRot::XZX
+            | EulerRot::YZX
+            | EulerRot::YZY
+            | EulerRot::YXZ
+            | EulerRot::YXY
+            | EulerRot::ZXY
+            | EulerRot::ZXZ
+            | EulerRot::ZYX
+            | EulerRot::ZYZ => true,
+            EulerRot::ZYXEx
+            | EulerRot::XYXEx
+            | EulerRot::YZXEx
+            | EulerRot::XZXEx
+            | EulerRot::XZYEx
+            | EulerRot::YZYEx
+            | EulerRot::ZXYEx
+            | EulerRot::YXYEx
+            | EulerRot::YXZEx
+            | EulerRot::ZXZEx
+            | EulerRot::XYZEx
+            | EulerRot::ZYZEx => false,
+        }
+    }
+
+    mod f32 {
+        pub fn deg_to_rad(a: i32, b: i32, c: i32) -> (f32, f32, f32) {
+            (
+                (a as f32).to_radians(),
+                (b as f32).to_radians(),
+                (c as f32).to_radians(),
+            )
+        }
+    }
+
+    mod f64 {
+        pub fn deg_to_rad(a: i32, b: i32, c: i32) -> (f64, f64, f64) {
+            (
+                (a as f64).to_radians(),
+                (b as f64).to_radians(),
+                (c as f64).to_radians(),
+            )
+        }
+    }
 
-macro_rules! impl_canonical_quat {
-    ($t:ty) => {
-        impl CanonicalQuat for $t {
-            fn canonical(self) -> Self {
-                match self {
-                    _ if self.w >= 1e-5 => self,
-                    _ if self.w <= -1e-5 => -self,
-                    _ => <$t>::from_xyzw(0.0, 0.0, 0.0, 1.0),
+    fn test_order_angles<F: Fn(EulerRot, i32, i32, i32)>(order: EulerRot, test: &F) {
+        const RANGE: RangeInclusive<i32> = -180..=180;
+        const STEP: usize = 15;
+        for i in RANGE.step_by(STEP) {
+            for j in RANGE.step_by(STEP) {
+                for k in RANGE.step_by(STEP) {
+                    test(order, i, j, k);
                 }
             }
         }
-    };
-}
+    }
 
-impl_canonical_quat!(glam::Quat);
-impl_canonical_quat!(glam::DQuat);
+    fn test_all_orders<F: Fn(EulerRot)>(test: &F) {
+        test(EulerRot::XYZ);
+        test(EulerRot::XZY);
+        test(EulerRot::YZX);
+        test(EulerRot::YXZ);
+        test(EulerRot::ZXY);
+        test(EulerRot::ZYX);
 
-/// Helper to set some alternative epsilons based on the floating point type used
-trait EulerEpsilon {
-    /// epsilon for comparing quaterions built from eulers and axis-angles
-    const Q_EPS: f32;
+        test(EulerRot::XZX);
+        test(EulerRot::XYX);
+        test(EulerRot::YXY);
+        test(EulerRot::YZY);
+        test(EulerRot::ZYZ);
+        test(EulerRot::ZXZ);
 
-    /// epsilon for comparing quaternion round-tripped through eulers (quat -> euler -> quat)
-    const E_EPS: f32;
-}
-impl EulerEpsilon for f32 {
-    const Q_EPS: f32 = 1e-5;
-    // TODO: sse2 impl passes with 1e-5 but scalar-math does not, why?
-    const E_EPS: f32 = 2e-4;
-}
+        test(EulerRot::XYZEx);
+        test(EulerRot::XZYEx);
+        test(EulerRot::YZXEx);
+        test(EulerRot::YXZEx);
+        test(EulerRot::ZXYEx);
+        test(EulerRot::ZYXEx);
 
-impl EulerEpsilon for f64 {
-    const Q_EPS: f32 = 1e-8;
-    const E_EPS: f32 = 1e-8;
-}
+        test(EulerRot::XZXEx);
+        test(EulerRot::XYXEx);
+        test(EulerRot::YXYEx);
+        test(EulerRot::YZYEx);
+        test(EulerRot::ZYZEx);
+        test(EulerRot::ZXZEx);
+    }
+
+    macro_rules! impl_quat_euler_test {
+        ($quat:ident, $t:ident) => {
+            use super::{
+                axis_order, is_intrinsic, test_all_orders, test_order_angles, $t::deg_to_rad,
+                CanonicalQuat, EulerEpsilon,
+            };
+            use glam::{$quat, EulerRot};
 
-macro_rules! impl_3axis_test {
-    ($name:ident, $t:ty, $quat:ident, $euler:path, $U:path, $V:path, $W:path, $vec:ident) => {
-        glam_test!($name, {
-            let euler = $euler;
-            assert!($U != $W); // First and last axis must be different for three axis
-            for u in (-180..=180).step_by(15) {
-                for v in (-180..=180).step_by(15) {
-                    for w in (-180..=180).step_by(15) {
-                        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.canonical(), q2.canonical(), <$t>::Q_EPS);
-
-                        // Test quat reconstruction from angles
-                        let (u2, v2, w2) = q2.to_euler(euler);
-                        let q3 = $quat::from_euler(euler, u2, v2, w2).normalize();
-                        assert_approx_eq!(
-                            q2.canonical(),
-                            q3.canonical(),
-                            <$t>::E_EPS,
-                            format!(
-                                "angles {:?} -> {:?}",
-                                (u, v, w),
-                                (u2.to_degrees(), v2.to_degrees(), w2.to_degrees())
-                            )
-                        );
+            const AXIS_ANGLE: [fn($t) -> $quat; 3] = [
+                $quat::from_rotation_x,
+                $quat::from_rotation_y,
+                $quat::from_rotation_z,
+            ];
+
+            impl CanonicalQuat for $quat {
+                fn canonical(self) -> Self {
+                    match self {
+                        _ if self.w >= 1e-5 => self,
+                        _ if self.w <= -1e-5 => -self,
+                        _ => $quat::from_xyzw(0.0, 0.0, 0.0, 1.0),
                     }
                 }
             }
-        });
-    };
-}
 
-macro_rules! impl_all_quat_tests_three_axis {
-    ($t:ty, $q:ident, $v:ident) => {
-        impl_3axis_test!(test_euler_zyx, $t, $q, ER::ZYX, $v::Z, $v::Y, $v::X, $v);
-        impl_3axis_test!(test_euler_zxy, $t, $q, ER::ZXY, $v::Z, $v::X, $v::Y, $v);
-        impl_3axis_test!(test_euler_yxz, $t, $q, ER::YXZ, $v::Y, $v::X, $v::Z, $v);
-        impl_3axis_test!(test_euler_yzx, $t, $q, ER::YZX, $v::Y, $v::Z, $v::X, $v);
-        impl_3axis_test!(test_euler_xyz, $t, $q, ER::XYZ, $v::X, $v::Y, $v::Z, $v);
-        impl_3axis_test!(test_euler_xzy, $t, $q, ER::XZY, $v::X, $v::Z, $v::Y, $v);
-    };
-}
+            fn test_euler(order: EulerRot, a: i32, b: i32, c: i32) {
+                println!(
+                    "test_euler: {} {order:?} ({a}, {b}, {c})",
+                    stringify!($quat)
+                );
 
-mod euler {
-    use super::{CanonicalQuat, EulerEpsilon};
-    use glam::*;
-    type ER = EulerRot;
+                let (a, b, c) = deg_to_rad(a, b, c);
+                let m = $quat::from_euler(order, a, b, c);
+
+                let n = {
+                    let (i, j, k) = m.to_euler(order);
+                    $quat::from_euler(order, i, j, k)
+                };
+                assert_approx_eq!(m.canonical(), n.canonical(), $t::E_EPS);
+
+                let o = {
+                    let (i, j, k) = axis_order(order);
+                    if is_intrinsic(order) {
+                        AXIS_ANGLE[i](a) * AXIS_ANGLE[j](b) * AXIS_ANGLE[k](c)
+                    } else {
+                        AXIS_ANGLE[k](c) * AXIS_ANGLE[j](b) * AXIS_ANGLE[i](a)
+                    }
+                };
+                assert_approx_eq!(m.canonical(), o.canonical(), $t::E_EPS);
+            }
+
+            #[test]
+            fn test_all_euler_orders() {
+                let test = |order| test_order_angles(order, &test_euler);
+                test_all_orders(&test);
+            }
+        };
+    }
+
+    macro_rules! impl_mat_euler_test {
+        ($mat:ident, $t:ident) => {
+            use super::{
+                axis_order, is_intrinsic, test_all_orders, test_order_angles, $t::deg_to_rad,
+                EulerEpsilon,
+            };
+            use glam::{$mat, EulerRot};
+
+            const AXIS_ANGLE: [fn($t) -> $mat; 3] = [
+                $mat::from_rotation_x,
+                $mat::from_rotation_y,
+                $mat::from_rotation_z,
+            ];
+
+            fn test_euler(order: EulerRot, a: i32, b: i32, c: i32) {
+                println!("test_euler: {} {order:?} ({a}, {b}, {c})", stringify!($mat));
+
+                let (a, b, c) = deg_to_rad(a, b, c);
+                let m = $mat::from_euler(order, a, b, c);
+                let n = {
+                    let (i, j, k) = m.to_euler(order);
+                    $mat::from_euler(order, i, j, k)
+                };
+                assert_approx_eq!(m, n, $t::E_EPS);
+
+                let o = {
+                    let (i, j, k) = axis_order(order);
+                    if is_intrinsic(order) {
+                        AXIS_ANGLE[i](a) * AXIS_ANGLE[j](b) * AXIS_ANGLE[k](c)
+                    } else {
+                        AXIS_ANGLE[k](c) * AXIS_ANGLE[j](b) * AXIS_ANGLE[i](a)
+                    }
+                };
+                assert_approx_eq!(m, o, $t::E_EPS);
+            }
+
+            #[test]
+            fn test_all_euler_orders() {
+                let test = |order| test_order_angles(order, &test_euler);
+                test_all_orders(&test);
+            }
+        };
+    }
 
     mod quat {
-        use super::*;
+        impl_quat_euler_test!(Quat, f32);
+    }
 
-        impl_all_quat_tests_three_axis!(f32, Quat, Vec3);
+    mod mat3 {
+        impl_mat_euler_test!(Mat3, f32);
+    }
+
+    mod mat3a {
+        impl_mat_euler_test!(Mat3A, f32);
+    }
+
+    mod mat4 {
+        impl_mat_euler_test!(Mat4, f32);
     }
 
     mod dquat {
-        use super::*;
+        impl_quat_euler_test!(DQuat, f64);
+    }
+
+    mod dmat3 {
+        impl_mat_euler_test!(DMat3, f64);
+    }
 
-        impl_all_quat_tests_three_axis!(f64, DQuat, DVec3);
+    mod dmat4 {
+        impl_mat_euler_test!(DMat4, f64);
     }
 }