Skip to main content

glamx/
eigen3.rs

1//! Eigendecomposition for symmetric 3x3 matrices.
2//!
3//! The eigensolver is a Rust adaptation, with modifications, of the pseudocode and approach described in
4//! "A Robust Eigensolver for 3 x 3 Symmetric Matrices" by David Eberly, Geometric Tools, Redmond WA 98052.
5//! <https://www.geometrictools.com/Documentation/RobustEigenSymmetric3x3.pdf>
6//!
7//! Adapted from <https://github.com/Jondolf/barry/tree/main/src/math/eigen>
8
9#![allow(clippy::manual_swap)] // Needed for spirv compatibility.
10
11use glam::Vec3Swizzles;
12use num_traits::float::FloatConst;
13
14#[cfg(not(feature = "std"))]
15use simba::scalar::ComplexField;
16
17/// Macro to generate SymmetricEigen3 for a specific scalar type.
18macro_rules! impl_symmetric_eigen3 {
19    ($SymmetricEigen3:ident, $Mat3:ty, $Vec3:ty, $Real:ty $(, #[$attr:meta])*) => {
20        #[doc = concat!("The eigen decomposition of a symmetric 3x3 matrix (", stringify!($Real), " precision).")]
21        #[derive(Clone, Copy, Debug, PartialEq)]
22        #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23        $(#[$attr])*
24        pub struct $SymmetricEigen3 {
25            /// The eigenvalues of the symmetric 3x3 matrix.
26            pub eigenvalues: $Vec3,
27            /// The three eigenvectors of the symmetric 3x3 matrix (as columns).
28            pub eigenvectors: $Mat3,
29        }
30
31        impl $SymmetricEigen3 {
32            /// Computes the eigen decomposition of the given symmetric 3x3 matrix.
33            ///
34            /// The eigenvalues are returned in ascending order `eigen1 < eigen2 < eigen3`.
35            /// This can be reversed with the [`reverse`](Self::reverse) method.
36            pub fn new(mat: $Mat3) -> Self {
37                let eigenvalues = Self::eigenvalues(mat);
38                let eigenvector1 = Self::eigenvector1(mat, eigenvalues.x);
39                let eigenvector2 = Self::eigenvector2(mat, eigenvector1, eigenvalues.y);
40                let eigenvector3 = Self::eigenvector3(eigenvector1, eigenvector2);
41
42                Self {
43                    eigenvalues,
44                    eigenvectors: <$Mat3>::from_cols(eigenvector1, eigenvector2, eigenvector3),
45                }
46            }
47
48            /// Reverses the order of the eigenvalues and their corresponding eigenvectors.
49            pub fn reverse(&self) -> Self {
50                Self {
51                    eigenvalues: self.eigenvalues.zyx(),
52                    eigenvectors: <$Mat3>::from_cols(
53                        self.eigenvectors.z_axis,
54                        self.eigenvectors.y_axis,
55                        self.eigenvectors.x_axis,
56                    ),
57                }
58            }
59
60            /// Computes the eigenvalues of a symmetric 3x3 matrix.
61            ///
62            /// Reference: <https://en.wikipedia.org/wiki/Eigenvalue_algorithm#3%C3%973_matrices>
63            pub fn eigenvalues(mat: $Mat3) -> $Vec3 {
64                let p1 = mat.y_axis.x.powi(2) + mat.z_axis.x.powi(2) + mat.z_axis.y.powi(2);
65                if p1 == 0.0 {
66                    // The matrix is diagonal.
67                    let mut eigenvalues = [mat.x_axis.x, mat.y_axis.y, mat.z_axis.z];
68                    // Simple 3-element sorting network (no alloc needed).
69                    // NOTE: manual swaps instead of .swap() to avoid ptr::swap which
70                    // generates memcpy unsupported in SPIR-V.
71                    if eigenvalues[0] > eigenvalues[1] { let t = eigenvalues[0]; eigenvalues[0] = eigenvalues[1]; eigenvalues[1] = t; }
72                    if eigenvalues[1] > eigenvalues[2] { let t = eigenvalues[1]; eigenvalues[1] = eigenvalues[2]; eigenvalues[2] = t; }
73                    if eigenvalues[0] > eigenvalues[1] { let t = eigenvalues[0]; eigenvalues[0] = eigenvalues[1]; eigenvalues[1] = t; }
74                    <$Vec3>::from_array(eigenvalues)
75                } else {
76                    let q = (mat.x_axis.x + mat.y_axis.y + mat.z_axis.z) / 3.0;
77                    let p2 = (mat.x_axis.x - q).powi(2)
78                        + (mat.y_axis.y - q).powi(2)
79                        + (mat.z_axis.z - q).powi(2)
80                        + 2.0 * p1;
81
82                    let p = (p2 / 6.0).sqrt();
83
84                    // NOTE: `p` shouldn’t be zero, but in some platforms (nvidia gpus apparently),
85                    //       denormals can be flushed resulting in a zero.
86                    let r = if p != 0.0 {
87                        let mat_b = 1.0 / p * (mat - q * <$Mat3>::IDENTITY);
88                        mat_b.determinant() / 2.0
89                    } else {
90                        1.0 // Will result in phy == 0.0
91                    };
92
93                    // r should be in the [-1, 1] range for a symmetric matrix,
94                    // but computation error can leave it slightly outside this range.
95                    let pi: $Real = <$Real as FloatConst>::PI();
96                    let phi = if r <= -1.0 {
97                        pi / 3.0
98                    } else if r >= 1.0 {
99                        0.0
100                    } else {
101                        r.acos() / 3.0
102                    };
103
104                    // The eigenvalues satisfy eigen3 <= eigen2 <= eigen1
105                    let eigen1 = q + 2.0 * p * phi.cos();
106                    let eigen3 = q + 2.0 * p * (phi + 2.0 * pi / 3.0).cos();
107                    let eigen2 = 3.0 * q - eigen1 - eigen3; // trace(mat) = eigen1 + eigen2 + eigen3
108                    <$Vec3>::new(eigen3, eigen2, eigen1)
109                }
110            }
111
112            /// Computes the unit-length eigenvector corresponding to the `eigenvalue1` of `mat` that was
113            /// computed from the root of a cubic polynomial with a multiplicity of 1.
114            ///
115            /// If the other two eigenvalues are well separated, this method can be used for computing
116            /// all three eigenvectors. However, to avoid numerical issues when eigenvalues are close to
117            /// each other, it's recommended to use the `eigenvector2` method for the second eigenvector.
118            ///
119            /// The third eigenvector can be computed as the cross product of the first two.
120            pub fn eigenvector1(mat: $Mat3, eigenvalue1: $Real) -> $Vec3 {
121                let cols = mat - <$Mat3>::from_diagonal(<$Vec3>::splat(eigenvalue1).into());
122                let c0xc1 = cols.x_axis.cross(cols.y_axis);
123                let c0xc2 = cols.x_axis.cross(cols.z_axis);
124                let c1xc2 = cols.y_axis.cross(cols.z_axis);
125                let d0 = c0xc1.length_squared();
126                let d1 = c0xc2.length_squared();
127                let d2 = c1xc2.length_squared();
128
129                let mut d_max = d0;
130                let mut i_max = 0;
131
132                if d1 > d_max {
133                    d_max = d1;
134                    i_max = 1;
135                }
136                if d2 > d_max {
137                    d_max = d2;
138                    i_max = 2;
139                }
140
141                // When all eigenvalues are equal (multiplicity 3), mat - eigenvalue * I is the
142                // zero matrix, so all cross products are zero. Any unit vector is a valid
143                // eigenvector in this case.
144                if d_max < 1e-20 {
145                    return <$Vec3>::X;
146                }
147
148                if i_max == 0 {
149                    c0xc1 / d0.sqrt()
150                } else if i_max == 1 {
151                    c0xc2 / d1.sqrt()
152                } else {
153                    c1xc2 / d2.sqrt()
154                }
155            }
156
157            /// Computes the unit-length eigenvector corresponding to the `eigenvalue2` of `mat` that was
158            /// computed from the root of a cubic polynomial with a potential multiplicity of 2.
159            ///
160            /// The third eigenvector can be computed as the cross product of the first two.
161            pub fn eigenvector2(mat: $Mat3, eigenvector1: $Vec3, eigenvalue2: $Real) -> $Vec3 {
162                // Compute right-handed orthonormal set { U, V, W }, where W is eigenvector1.
163                let (u, v) = Self::any_orthonormal_pair(eigenvector1);
164
165                // The unit-length eigenvector is E = x0 * U + x1 * V. We need to compute x0 and x1.
166                //
167                // Define the symmetric 2x2 matrix M = J^T * (mat - eigenvalue2 * I), where J = [U V]
168                // and I is a 3x3 identity matrix. This means that E = J * X, where X is a column vector
169                // with rows x0 and x1. The 3x3 linear system (mat - eigenvalue2 * I) * E = 0 reduces to
170                // the 2x2 linear system M * X = 0.
171                //
172                // When eigenvalue2 != eigenvalue3, M has rank 1 and is not the zero matrix.
173                // Otherwise, it has rank 0, and it is the zero matrix.
174
175                let au = mat * u;
176                let av = mat * v;
177
178                let mut m00 = u.dot(au) - eigenvalue2;
179                let mut m01 = u.dot(av);
180                let mut m11 = v.dot(av) - eigenvalue2;
181                let (abs_m00, abs_m01, abs_m11) = (m00.abs(), m01.abs(), m11.abs());
182
183                if abs_m00 >= abs_m11 {
184                    let max_abs_component = abs_m00.max(abs_m01);
185                    if max_abs_component > 0.0 {
186                        if abs_m00 >= abs_m01 {
187                            // m00 is the largest component of the row.
188                            // Factor it out for normalization and discard to avoid underflow or overflow.
189                            m01 /= m00;
190                            m00 = 1.0 / (1.0 + m01 * m01).sqrt();
191                            m01 *= m00;
192                        } else {
193                            // m01 is the largest component of the row.
194                            // Factor it out for normalization and discard to avoid underflow or overflow.
195                            m00 /= m01;
196                            m01 = 1.0 / (1.0 + m00 * m00).sqrt();
197                            m00 *= m01;
198                        }
199                        return m01 * u - m00 * v;
200                    }
201                } else {
202                    let max_abs_component = abs_m11.max(abs_m01);
203                    if max_abs_component > 0.0 {
204                        if abs_m11 >= abs_m01 {
205                            // m11 is the largest component of the row.
206                            // Factor it out for normalization and discard to avoid underflow or overflow.
207                            m01 /= m11;
208                            m11 = 1.0 / (1.0 + m01 * m01).sqrt();
209                            m01 *= m11;
210                        } else {
211                            // m01 is the largest component of the row.
212                            // Factor it out for normalization and discard to avoid underflow or overflow.
213                            m11 /= m01;
214                            m01 = 1.0 / (1.0 + m11 * m11).sqrt();
215                            m11 *= m01;
216                        }
217                        return m11 * u - m01 * v;
218                    }
219                }
220
221                // M is the zero matrix, any unit-length solution suffices.
222                u
223            }
224
225            /// Computes the third eigenvector as the cross product of the first two.
226            /// If the given eigenvectors are valid, the returned vector should be unit length.
227            pub fn eigenvector3(eigenvector1: $Vec3, eigenvector2: $Vec3) -> $Vec3 {
228                eigenvector1.cross(eigenvector2)
229            }
230
231            /// Compute any orthonormal pair of vectors perpendicular to `v`.
232            fn any_orthonormal_pair(v: $Vec3) -> ($Vec3, $Vec3) {
233                let sign = if v.z >= 0.0 { 1.0 } else { -1.0 };
234                let a = -1.0 / (sign + v.z);
235                let b = v.x * v.y * a;
236                let u = <$Vec3>::new(1.0 + sign * v.x * v.x * a, sign * b, -sign * v.x);
237                let w = <$Vec3>::new(b, sign + v.y * v.y * a, -v.y);
238                (u, w)
239            }
240        }
241    };
242}
243
244impl_symmetric_eigen3!(SymmetricEigen3, glam::Mat3, glam::Vec3, f32);
245impl_symmetric_eigen3!(SymmetricEigen3A, glam::Mat3A, glam::Vec3A, f32);
246impl_symmetric_eigen3!(DSymmetricEigen3, glam::DMat3, glam::DVec3, f64);
247
248// f32 <-> f64 conversions
249impl From<SymmetricEigen3> for DSymmetricEigen3 {
250    #[inline]
251    fn from(e: SymmetricEigen3) -> Self {
252        Self {
253            eigenvalues: e.eigenvalues.as_dvec3(),
254            eigenvectors: e.eigenvectors.as_dmat3(),
255        }
256    }
257}
258
259impl From<DSymmetricEigen3> for SymmetricEigen3 {
260    #[inline]
261    fn from(e: DSymmetricEigen3) -> Self {
262        Self {
263            eigenvalues: e.eigenvalues.as_vec3(),
264            eigenvectors: e.eigenvectors.as_mat3(),
265        }
266    }
267}
268
269impl From<SymmetricEigen3A> for DSymmetricEigen3 {
270    #[inline]
271    fn from(e: SymmetricEigen3A) -> Self {
272        Self {
273            eigenvalues: e.eigenvalues.as_dvec3(),
274            eigenvectors: e.eigenvectors.as_dmat3(),
275        }
276    }
277}
278
279impl From<DSymmetricEigen3> for SymmetricEigen3A {
280    #[inline]
281    fn from(e: DSymmetricEigen3) -> Self {
282        Self {
283            eigenvalues: e.eigenvalues.as_vec3a(),
284            eigenvectors: e.eigenvectors.as_mat3().into(),
285        }
286    }
287}
288
289// SymmetricEigen3 <-> SymmetricEigen3A conversions
290impl From<SymmetricEigen3> for SymmetricEigen3A {
291    #[inline]
292    fn from(e: SymmetricEigen3) -> Self {
293        Self {
294            eigenvalues: e.eigenvalues.into(),
295            eigenvectors: glam::Mat3A::from(e.eigenvectors),
296        }
297    }
298}
299
300impl From<SymmetricEigen3A> for SymmetricEigen3 {
301    #[inline]
302    fn from(e: SymmetricEigen3A) -> Self {
303        Self {
304            eigenvalues: e.eigenvalues.into(),
305            eigenvectors: glam::Mat3::from(e.eigenvectors),
306        }
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313    use approx::assert_relative_eq;
314
315    #[test]
316    fn eigen_3x3() {
317        let mat =
318            glam::Mat3::from_cols_array_2d(&[[2.0, 7.0, 8.0], [7.0, 6.0, 3.0], [8.0, 3.0, 0.0]]);
319        let eigen = SymmetricEigen3::new(mat);
320
321        assert_relative_eq!(
322            eigen.eigenvalues,
323            glam::Vec3::new(-7.6051016780, 0.5774961930, 15.0276054850),
324            epsilon = 1.0e-6
325        );
326        assert_relative_eq!(
327            glam::Mat3::from_cols(
328                eigen.eigenvectors.x_axis,
329                eigen.eigenvectors.y_axis,
330                eigen.eigenvectors.z_axis
331            ),
332            glam::Mat3::from_cols(
333                glam::Vec3::new(1.0754474819, -0.3328260589, -1.0).normalize(),
334                glam::Vec3::new(-0.5420669703, 1.2530131898, -1.0).normalize(),
335                glam::Vec3::new(1.3587443369, 1.3858835966, 1.0).normalize()
336            ),
337            epsilon = 1.0e-6
338        );
339    }
340
341    #[test]
342    fn eigen_3x3_diagonal() {
343        let mat =
344            glam::Mat3::from_cols_array_2d(&[[2.0, 0.0, 0.0], [0.0, 5.0, 0.0], [0.0, 0.0, 3.0]]);
345        let eigen = SymmetricEigen3::new(mat);
346
347        assert_eq!(eigen.eigenvalues, glam::Vec3::new(2.0, 3.0, 5.0));
348        assert_eq!(
349            glam::Mat3::from_cols(
350                eigen.eigenvectors.x_axis.normalize().abs(),
351                eigen.eigenvectors.y_axis.normalize().abs(),
352                eigen.eigenvectors.z_axis.normalize().abs()
353            ),
354            glam::Mat3::from_cols_array_2d(&[[1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 1.0, 0.0]])
355        );
356    }
357
358    #[test]
359    fn eigen_3x3_f64() {
360        let mat =
361            glam::DMat3::from_cols_array_2d(&[[2.0, 7.0, 8.0], [7.0, 6.0, 3.0], [8.0, 3.0, 0.0]]);
362        let eigen = DSymmetricEigen3::new(mat);
363
364        assert_relative_eq!(
365            eigen.eigenvalues,
366            glam::DVec3::new(-7.6051016780, 0.5774961930, 15.0276054850),
367            epsilon = 1.0e-8
368        );
369    }
370
371    #[test]
372    fn eigen_3x3_identity() {
373        // Identity matrix has all eigenvalues equal to 1 (multiplicity 3)
374        // Any orthonormal basis is a valid set of eigenvectors
375        let mat = glam::Mat3::IDENTITY;
376        let eigen = SymmetricEigen3::new(mat);
377
378        assert_eq!(eigen.eigenvalues, glam::Vec3::ONE);
379
380        // Verify eigenvectors are orthonormal (any orthonormal basis is valid)
381        assert_relative_eq!(
382            eigen.eigenvectors * eigen.eigenvectors.transpose(),
383            glam::Mat3::IDENTITY,
384            epsilon = 1e-6
385        );
386    }
387
388    #[test]
389    fn eigen_3x3_uniform_scaling() {
390        // Uniform scaling matrix: 5*I has all eigenvalues equal to 5
391        let mat = glam::Mat3::from_diagonal(glam::Vec3::splat(5.0));
392        let eigen = SymmetricEigen3::new(mat);
393
394        assert_eq!(eigen.eigenvalues, glam::Vec3::splat(5.0));
395
396        // Verify eigenvectors are orthonormal
397        assert_relative_eq!(
398            eigen.eigenvectors * eigen.eigenvectors.transpose(),
399            glam::Mat3::IDENTITY,
400            epsilon = 1e-6
401        );
402    }
403}