Skip to main content

scenix_math/
euler.rs

1use crate::{Mat4, Quat, asin, atan2, clamp, cos, sin};
2
3/// Rotation order for Euler angles.
4#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6pub enum RotationOrder {
7    /// X, then Y, then Z.
8    #[default]
9    XYZ,
10    /// Y, then X, then Z.
11    YXZ,
12    /// Z, then X, then Y.
13    ZXY,
14    /// Z, then Y, then X.
15    ZYX,
16    /// Y, then Z, then X.
17    YZX,
18    /// X, then Z, then Y.
19    XZY,
20}
21
22/// Euler angles in radians plus a rotation order.
23#[derive(Clone, Copy, Debug, PartialEq)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25pub struct Euler {
26    /// Rotation around X in radians.
27    pub x: f32,
28    /// Rotation around Y in radians.
29    pub y: f32,
30    /// Rotation around Z in radians.
31    pub z: f32,
32    /// Rotation order.
33    pub order: RotationOrder,
34}
35
36impl Euler {
37    /// Creates Euler angles from components.
38    #[inline]
39    pub const fn new(x: f32, y: f32, z: f32, order: RotationOrder) -> Self {
40        Self { x, y, z, order }
41    }
42
43    /// Extracts Euler angles from a quaternion.
44    #[inline]
45    pub fn from_quat(q: Quat, order: RotationOrder) -> Self {
46        Self::from_mat4(q.to_mat4(), order)
47    }
48
49    /// Extracts Euler angles from a rotation matrix.
50    pub fn from_mat4(matrix: Mat4, order: RotationOrder) -> Self {
51        let m11 = matrix.get(0, 0);
52        let m12 = matrix.get(0, 1);
53        let m13 = matrix.get(0, 2);
54        let m21 = matrix.get(1, 0);
55        let m22 = matrix.get(1, 1);
56        let m23 = matrix.get(1, 2);
57        let m31 = matrix.get(2, 0);
58        let m32 = matrix.get(2, 1);
59        let m33 = matrix.get(2, 2);
60
61        let (x, y, z) = match order {
62            RotationOrder::XYZ => {
63                let y = asin(clamp(m13, -1.0, 1.0));
64                if m13.abs() < 0.999_999_9 {
65                    (atan2(-m23, m33), y, atan2(-m12, m11))
66                } else {
67                    (atan2(m32, m22), y, 0.0)
68                }
69            }
70            RotationOrder::YXZ => {
71                let x = asin(-clamp(m23, -1.0, 1.0));
72                if m23.abs() < 0.999_999_9 {
73                    (x, atan2(m13, m33), atan2(m21, m22))
74                } else {
75                    (x, atan2(-m31, m11), 0.0)
76                }
77            }
78            RotationOrder::ZXY => {
79                let x = asin(clamp(m32, -1.0, 1.0));
80                if m32.abs() < 0.999_999_9 {
81                    (x, atan2(-m31, m33), atan2(-m12, m22))
82                } else {
83                    (x, 0.0, atan2(m21, m11))
84                }
85            }
86            RotationOrder::ZYX => {
87                let y = asin(-clamp(m31, -1.0, 1.0));
88                if m31.abs() < 0.999_999_9 {
89                    (atan2(m32, m33), y, atan2(m21, m11))
90                } else {
91                    (0.0, y, atan2(-m12, m22))
92                }
93            }
94            RotationOrder::YZX => {
95                let z = asin(clamp(m21, -1.0, 1.0));
96                if m21.abs() < 0.999_999_9 {
97                    (atan2(-m23, m22), atan2(-m31, m11), z)
98                } else {
99                    (0.0, atan2(m13, m33), z)
100                }
101            }
102            RotationOrder::XZY => {
103                let z = asin(-clamp(m12, -1.0, 1.0));
104                if m12.abs() < 0.999_999_9 {
105                    (atan2(m32, m22), atan2(m13, m11), z)
106                } else {
107                    (atan2(-m23, m33), 0.0, z)
108                }
109            }
110        };
111
112        Self::new(x, y, z, order)
113    }
114
115    /// Converts these Euler angles to a quaternion.
116    pub fn to_quat(self) -> Quat {
117        let c1 = cos(self.x * 0.5);
118        let c2 = cos(self.y * 0.5);
119        let c3 = cos(self.z * 0.5);
120        let s1 = sin(self.x * 0.5);
121        let s2 = sin(self.y * 0.5);
122        let s3 = sin(self.z * 0.5);
123
124        match self.order {
125            RotationOrder::XYZ => Quat::new(
126                s1 * c2 * c3 + c1 * s2 * s3,
127                c1 * s2 * c3 - s1 * c2 * s3,
128                c1 * c2 * s3 + s1 * s2 * c3,
129                c1 * c2 * c3 - s1 * s2 * s3,
130            ),
131            RotationOrder::YXZ => Quat::new(
132                s1 * c2 * c3 + c1 * s2 * s3,
133                c1 * s2 * c3 - s1 * c2 * s3,
134                c1 * c2 * s3 - s1 * s2 * c3,
135                c1 * c2 * c3 + s1 * s2 * s3,
136            ),
137            RotationOrder::ZXY => Quat::new(
138                s1 * c2 * c3 - c1 * s2 * s3,
139                c1 * s2 * c3 + s1 * c2 * s3,
140                c1 * c2 * s3 + s1 * s2 * c3,
141                c1 * c2 * c3 - s1 * s2 * s3,
142            ),
143            RotationOrder::ZYX => Quat::new(
144                s1 * c2 * c3 - c1 * s2 * s3,
145                c1 * s2 * c3 + s1 * c2 * s3,
146                c1 * c2 * s3 - s1 * s2 * c3,
147                c1 * c2 * c3 + s1 * s2 * s3,
148            ),
149            RotationOrder::YZX => Quat::new(
150                s1 * c2 * c3 + c1 * s2 * s3,
151                c1 * s2 * c3 + s1 * c2 * s3,
152                c1 * c2 * s3 - s1 * s2 * c3,
153                c1 * c2 * c3 - s1 * s2 * s3,
154            ),
155            RotationOrder::XZY => Quat::new(
156                s1 * c2 * c3 - c1 * s2 * s3,
157                c1 * s2 * c3 - s1 * c2 * s3,
158                c1 * c2 * s3 + s1 * s2 * c3,
159                c1 * c2 * c3 + s1 * s2 * s3,
160            ),
161        }
162        .normalize()
163    }
164}
165
166impl Default for Euler {
167    #[inline]
168    fn default() -> Self {
169        Self::new(0.0, 0.0, 0.0, RotationOrder::XYZ)
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use crate::{Vec3, assert_close};
177
178    #[test]
179    fn xyz_round_trips_through_quat() {
180        let euler = Euler::new(0.3, 0.4, 0.5, RotationOrder::XYZ);
181        let out = Euler::from_quat(euler.to_quat(), RotationOrder::XYZ);
182        assert_close(out.x, euler.x);
183        assert_close(out.y, euler.y);
184        assert_close(out.z, euler.z);
185    }
186
187    #[test]
188    fn euler_rotates_vector() {
189        let q = Euler::new(0.0, core::f32::consts::FRAC_PI_2, 0.0, RotationOrder::XYZ).to_quat();
190        let out = q.mul_vec3(Vec3::X);
191        assert_close(out.z, -1.0);
192    }
193}