Skip to main content

khora_core/math/
matrix.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Defines the `Mat3` and `Mat4` types and associated operations.
16
17use super::{Quaternion, Vec2, Vec3, Vec4, EPSILON};
18use bincode::{Decode, Encode};
19use serde::{Deserialize, Serialize};
20use std::ops::{Index, IndexMut, Mul};
21
22// --- Mat3 ---
23
24/// A 3x3 column-major matrix, typically used for 2D affine transformations (scale, rotation).
25///
26/// While it can represent any 3x3 matrix, its primary role in a 3D engine is often
27/// as the upper-left rotation and scale part of a `Mat4`.
28#[derive(Debug, Clone, Copy, PartialEq)]
29#[repr(C)]
30pub struct Mat3 {
31    /// The columns of the matrix. `cols[0]` is the first column, and so on.
32    pub cols: [Vec3; 3],
33}
34
35impl Mat3 {
36    /// The 3x3 identity matrix.
37    pub const IDENTITY: Self = Self {
38        cols: [Vec3::X, Vec3::Y, Vec3::Z],
39    };
40
41    /// A 3x3 matrix with all elements set to 0.
42    pub const ZERO: Self = Self {
43        cols: [Vec3::ZERO; 3],
44    };
45
46    /// Creates a new matrix from three column vectors.
47    #[inline]
48    pub fn from_cols(c0: Vec3, c1: Vec3, c2: Vec3) -> Self {
49        Self { cols: [c0, c1, c2] }
50    }
51
52    /// Returns a row of the matrix as a `Vec3`.
53    #[allow(dead_code)]
54    #[inline]
55    fn get_row(&self, index: usize) -> Vec3 {
56        Vec3 {
57            x: self.cols[0].get(index),
58            y: self.cols[1].get(index),
59            z: self.cols[2].get(index),
60        }
61    }
62
63    /// Creates a 2D scaling matrix.
64    ///
65    /// The Z-axis scale is set to 1.0, making it a no-op in that dimension.
66    #[inline]
67    pub fn from_scale_vec2(scale: Vec2) -> Self {
68        Self::from_scale(Vec3::new(scale.x, scale.y, 1.0))
69    }
70
71    /// Creates a 3D scaling matrix.
72    #[inline]
73    pub fn from_scale(scale: Vec3) -> Self {
74        Self {
75            cols: [
76                Vec3::new(scale.x, 0.0, 0.0),
77                Vec3::new(0.0, scale.y, 0.0),
78                Vec3::new(0.0, 0.0, scale.z),
79            ],
80        }
81    }
82
83    /// Creates a matrix for a rotation around the X-axis.
84    ///
85    /// # Arguments
86    ///
87    /// * `angle_radians`: The angle of rotation in radians.
88    #[inline]
89    pub fn from_rotation_x(angle_radians: f32) -> Self {
90        let (s, c) = angle_radians.sin_cos();
91        Self {
92            cols: [
93                Vec3::new(1.0, 0.0, 0.0),
94                Vec3::new(0.0, c, s),
95                Vec3::new(0.0, -s, c),
96            ],
97        }
98    }
99
100    /// Creates a matrix for a right-handed rotation around the Y-axis.
101    ///
102    /// # Arguments
103    ///
104    /// * `angle_radians`: The angle of rotation in radians.
105    #[inline]
106    pub fn from_rotation_y(angle_radians: f32) -> Self {
107        let (s, c) = angle_radians.sin_cos();
108        Self {
109            cols: [
110                Vec3::new(c, 0.0, -s),
111                Vec3::new(0.0, 1.0, 0.0),
112                Vec3::new(s, 0.0, c),
113            ],
114        }
115    }
116
117    /// Creates a matrix for a rotation around the Z-axis.
118    ///
119    /// # Arguments
120    ///
121    /// * `angle_radians`: The angle of rotation in radians.
122    #[inline]
123    pub fn from_rotation_z(angle_radians: f32) -> Self {
124        let (s, c) = angle_radians.sin_cos();
125        Self {
126            cols: [
127                Vec3::new(c, s, 0.0),
128                Vec3::new(-s, c, 0.0),
129                Vec3::new(0.0, 0.0, 1.0),
130            ],
131        }
132    }
133
134    /// Creates a rotation matrix from a normalized axis and an angle.
135    ///
136    /// # Arguments
137    ///
138    /// * `axis`: The axis of rotation. Must be a unit vector.
139    /// * `angle_radians`: The angle of rotation in radians.
140    #[inline]
141    pub fn from_axis_angle(axis: Vec3, angle_radians: f32) -> Self {
142        let (s, c) = angle_radians.sin_cos();
143        let t = 1.0 - c;
144        let x = axis.x;
145        let y = axis.y;
146        let z = axis.z;
147        Self {
148            cols: [
149                Vec3::new(t * x * x + c, t * x * y + s * z, t * x * z - s * y),
150                Vec3::new(t * y * x - s * z, t * y * y + c, t * y * z + s * x),
151                Vec3::new(t * z * x + s * y, t * z * y - s * x, t * z * z + c),
152            ],
153        }
154    }
155
156    /// Creates a rotation matrix from a quaternion.
157    /// The quaternion is normalized before conversion to ensure a valid rotation matrix.
158    #[inline]
159    pub fn from_quat(q: Quaternion) -> Self {
160        let q = q.normalize();
161        let x = q.x;
162        let y = q.y;
163        let z = q.z;
164        let w = q.w;
165        let x2 = x + x;
166        let y2 = y + y;
167        let z2 = z + z;
168        let xx = x * x2;
169        let xy = x * y2;
170        let xz = x * z2;
171        let yy = y * y2;
172        let yz = y * z2;
173        let zz = z * z2;
174        let wx = w * x2;
175        let wy = w * y2;
176        let wz = w * z2;
177
178        Self::from_cols(
179            Vec3::new(1.0 - (yy + zz), xy + wz, xz - wy),
180            Vec3::new(xy - wz, 1.0 - (xx + zz), yz + wx),
181            Vec3::new(xz + wy, yz - wx, 1.0 - (xx + yy)),
182        )
183    }
184
185    /// Creates a `Mat3` from the upper-left 3x3 corner of a [`Mat4`].
186    /// This effectively extracts the rotation and scale components, discarding translation.
187    #[inline]
188    pub fn from_mat4(m4: &Mat4) -> Self {
189        Self::from_cols(
190            m4.cols[0].truncate(),
191            m4.cols[1].truncate(),
192            m4.cols[2].truncate(),
193        )
194    }
195
196    /// Computes the determinant of the matrix.
197    ///
198    /// The determinant is a scalar value indicating the volume scaling factor of the
199    /// linear transformation. A determinant of 0 means the matrix is not invertible.
200    #[inline]
201    pub fn determinant(&self) -> f32 {
202        let c0 = self.cols[0];
203        let c1 = self.cols[1];
204        let c2 = self.cols[2];
205        c0.x * (c1.y * c2.z - c2.y * c1.z) - c1.x * (c0.y * c2.z - c2.y * c0.z)
206            + c2.x * (c0.y * c1.z - c1.y * c0.z)
207    }
208
209    /// Returns the transpose of the matrix, where rows and columns are swapped.
210    #[inline]
211    pub fn transpose(&self) -> Self {
212        Self::from_cols(
213            Vec3::new(self.cols[0].x, self.cols[1].x, self.cols[2].x),
214            Vec3::new(self.cols[0].y, self.cols[1].y, self.cols[2].y),
215            Vec3::new(self.cols[0].z, self.cols[1].z, self.cols[2].z),
216        )
217    }
218
219    /// Computes the inverse of the matrix.
220    ///
221    /// If the matrix is not invertible (i.e., its determinant is close to zero),
222    /// this method returns `None`.
223    pub fn inverse(&self) -> Option<Self> {
224        let c0 = self.cols[0];
225        let c1 = self.cols[1];
226        let c2 = self.cols[2];
227        let m00 = c1.y * c2.z - c2.y * c1.z;
228        let m10 = c2.y * c0.z - c0.y * c2.z;
229        let m20 = c0.y * c1.z - c1.y * c0.z;
230        let det = c0.x * m00 + c1.x * m10 + c2.x * m20;
231
232        if det.abs() < EPSILON {
233            return None;
234        }
235
236        let inv_det = 1.0 / det;
237        let m01 = c2.x * c1.z - c1.x * c2.z;
238        let m11 = c0.x * c2.z - c2.x * c0.z;
239        let m21 = c1.x * c0.z - c0.x * c1.z;
240        let m02 = c1.x * c2.y - c2.x * c1.y;
241        let m12 = c2.x * c0.y - c0.x * c2.y;
242        let m22 = c0.x * c1.y - c1.x * c0.y;
243
244        Some(Self::from_cols(
245            Vec3::new(m00, m10, m20) * inv_det,
246            Vec3::new(m01, m11, m21) * inv_det,
247            Vec3::new(m02, m12, m22) * inv_det,
248        ))
249    }
250
251    /// Converts this `Mat3` into a [`Mat4`], preserving its values in the upper-left corner.
252    /// The new fourth column and row are set to `(0, 0, 0, 1)`.
253    #[inline]
254    pub fn to_mat4(&self) -> Mat4 {
255        Mat4::from_cols(
256            Vec4::from_vec3(self.cols[0], 0.0),
257            Vec4::from_vec3(self.cols[1], 0.0),
258            Vec4::from_vec3(self.cols[2], 0.0),
259            Vec4::W,
260        )
261    }
262}
263
264// --- Operator Overloads ---
265
266impl Default for Mat3 {
267    /// Returns the 3x3 identity matrix.
268    #[inline]
269    fn default() -> Self {
270        Self::IDENTITY
271    }
272}
273
274impl Mul<Mat3> for Mat3 {
275    type Output = Self;
276    /// Multiplies this matrix by another `Mat3`.
277    #[inline]
278    fn mul(self, rhs: Mat3) -> Self::Output {
279        Self::from_cols(self * rhs.cols[0], self * rhs.cols[1], self * rhs.cols[2])
280    }
281}
282
283impl Mul<Vec3> for Mat3 {
284    type Output = Vec3;
285    /// Transforms a `Vec3` by this matrix.
286    #[inline]
287    fn mul(self, v: Vec3) -> Self::Output {
288        self.cols[0] * v.x + self.cols[1] * v.y + self.cols[2] * v.z
289    }
290}
291
292impl Index<usize> for Mat3 {
293    type Output = Vec3;
294    /// Allows accessing a matrix column by index.
295    #[inline]
296    fn index(&self, index: usize) -> &Self::Output {
297        &self.cols[index]
298    }
299}
300
301impl IndexMut<usize> for Mat3 {
302    /// Allows mutably accessing a matrix column by index.
303    #[inline]
304    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
305        &mut self.cols[index]
306    }
307}
308
309// --- Mat4 ---
310
311/// A 4x4 column-major matrix, used for 3D affine transformations.
312///
313/// This is the primary type for representing transformations (translation, rotation,
314/// scale) in 3D space. It is also used for camera view and projection matrices.
315/// The memory layout is column-major, which is compatible with modern graphics APIs
316/// like Vulkan, Metal, and DirectX.
317///
318/// # Examples
319///
320/// ```rust
321/// use khora_core::math::{Mat4, Quaternion, Vec3};
322/// use std::f32::consts::FRAC_PI_2;
323///
324/// // The identity matrix leaves a point unchanged.
325/// assert_eq!(Mat4::IDENTITY.transform_point(Vec3::ONE), Vec3::ONE);
326///
327/// // Compose translation ∘ rotation ∘ scale by multiplying matrices
328/// // (applied right-to-left).
329/// let model = Mat4::from_translation(Vec3::new(0.0, 1.0, 0.0))
330///     * Mat4::from_quat(Quaternion::from_axis_angle(Vec3::Y, FRAC_PI_2))
331///     * Mat4::from_scale(Vec3::ONE * 2.0);
332/// let moved = model.transform_point(Vec3::ZERO);
333/// assert!((moved - Vec3::new(0.0, 1.0, 0.0)).length() < 1e-5);
334///
335/// // A perspective projection for a 16:9 viewport.
336/// let proj = Mat4::perspective_rh_zo(FRAC_PI_2, 16.0 / 9.0, 0.1, 1000.0);
337/// assert!(proj != Mat4::IDENTITY);
338/// ```
339#[derive(
340    Debug,
341    Clone,
342    Copy,
343    PartialEq,
344    bytemuck::Pod,
345    bytemuck::Zeroable,
346    Serialize,
347    Deserialize,
348    Encode,
349    Decode,
350)]
351#[repr(C)]
352pub struct Mat4 {
353    /// The columns of the matrix. `cols[0]` is the first column, and so on.
354    pub cols: [Vec4; 4],
355}
356
357impl Mat4 {
358    /// The 4x4 identity matrix.
359    pub const IDENTITY: Self = Self {
360        cols: [Vec4::X, Vec4::Y, Vec4::Z, Vec4::W],
361    };
362
363    /// A 4x4 matrix with all elements set to 0.
364    pub const ZERO: Self = Self {
365        cols: [Vec4::ZERO; 4],
366    };
367
368    /// Creates a new matrix from four column vectors.
369    #[inline]
370    pub fn from_cols(c0: Vec4, c1: Vec4, c2: Vec4, c3: Vec4) -> Self {
371        Self {
372            cols: [c0, c1, c2, c3],
373        }
374    }
375
376    /// Returns a row of the matrix as a `Vec4`.
377    #[inline]
378    pub fn get_row(&self, index: usize) -> Vec4 {
379        Vec4 {
380            x: self.cols[0].get(index),
381            y: self.cols[1].get(index),
382            z: self.cols[2].get(index),
383            w: self.cols[3].get(index),
384        }
385    }
386
387    /// Returns the matrix as a 2D array of columns: `[[x, y, z, w]; 4]`.
388    #[inline]
389    pub const fn to_cols_array_2d(&self) -> [[f32; 4]; 4] {
390        [
391            [
392                self.cols[0].x,
393                self.cols[0].y,
394                self.cols[0].z,
395                self.cols[0].w,
396            ],
397            [
398                self.cols[1].x,
399                self.cols[1].y,
400                self.cols[1].z,
401                self.cols[1].w,
402            ],
403            [
404                self.cols[2].x,
405                self.cols[2].y,
406                self.cols[2].z,
407                self.cols[2].w,
408            ],
409            [
410                self.cols[3].x,
411                self.cols[3].y,
412                self.cols[3].z,
413                self.cols[3].w,
414            ],
415        ]
416    }
417
418    /// Creates a translation matrix.
419    ///
420    /// # Arguments
421    ///
422    /// * `v`: The translation vector to apply.
423    #[inline]
424    pub fn from_translation(v: Vec3) -> Self {
425        Self {
426            cols: [
427                Vec4::new(1.0, 0.0, 0.0, 0.0),
428                Vec4::new(0.0, 1.0, 0.0, 0.0),
429                Vec4::new(0.0, 0.0, 1.0, 0.0),
430                Vec4::new(v.x, v.y, v.z, 1.0),
431            ],
432        }
433    }
434
435    /// Creates a non-uniform scaling matrix.
436    #[inline]
437    pub fn from_scale(scale: Vec3) -> Self {
438        Self {
439            cols: [
440                Vec4::new(scale.x, 0.0, 0.0, 0.0),
441                Vec4::new(0.0, scale.y, 0.0, 0.0),
442                Vec4::new(0.0, 0.0, scale.z, 0.0),
443                Vec4::new(0.0, 0.0, 0.0, 1.0),
444            ],
445        }
446    }
447
448    /// Creates a matrix for a rotation around the X-axis.
449    ///
450    /// # Arguments
451    ///
452    /// * `angle`: The angle of rotation in radians.
453    #[inline]
454    pub fn from_rotation_x(angle: f32) -> Self {
455        let c = angle.cos();
456        let s = angle.sin();
457        Self {
458            cols: [
459                Vec4::new(1.0, 0.0, 0.0, 0.0),
460                Vec4::new(0.0, c, s, 0.0),
461                Vec4::new(0.0, -s, c, 0.0),
462                Vec4::new(0.0, 0.0, 0.0, 1.0),
463            ],
464        }
465    }
466
467    /// Creates a matrix for a right-handed rotation around the Y-axis.
468    ///
469    /// # Arguments
470    ///
471    /// * `angle`: The angle of rotation in radians.
472    #[inline]
473    pub fn from_rotation_y(angle: f32) -> Self {
474        let c = angle.cos();
475        let s = angle.sin();
476        Self {
477            cols: [
478                Vec4::new(c, 0.0, -s, 0.0),
479                Vec4::new(0.0, 1.0, 0.0, 0.0),
480                Vec4::new(s, 0.0, c, 0.0),
481                Vec4::new(0.0, 0.0, 0.0, 1.0),
482            ],
483        }
484    }
485
486    /// Creates a matrix for a rotation around the Z-axis.
487    ///
488    /// # Arguments
489    ///
490    /// * `angle`: The angle of rotation in radians.
491    #[inline]
492    pub fn from_rotation_z(angle: f32) -> Self {
493        let c = angle.cos();
494        let s = angle.sin();
495        Self {
496            cols: [
497                Vec4::new(c, s, 0.0, 0.0),
498                Vec4::new(-s, c, 0.0, 0.0),
499                Vec4::new(0.0, 0.0, 1.0, 0.0),
500                Vec4::new(0.0, 0.0, 0.0, 1.0),
501            ],
502        }
503    }
504
505    /// Creates a rotation matrix from a normalized axis and an angle.
506    ///
507    /// # Arguments
508    ///
509    /// * `axis`: The axis of rotation. Must be a unit vector.
510    /// * `angle`: The angle of rotation in radians.
511    #[inline]
512    pub fn from_axis_angle(axis: Vec3, angle: f32) -> Self {
513        let c = angle.cos();
514        let s = angle.sin();
515        let t = 1.0 - c;
516        let x = axis.x;
517        let y = axis.y;
518        let z = axis.z;
519
520        Self {
521            cols: [
522                Vec4::new(t * x * x + c, t * x * y - s * z, t * x * z + s * y, 0.0),
523                Vec4::new(t * y * x + s * z, t * y * y + c, t * y * z - s * x, 0.0),
524                Vec4::new(t * z * x - s * y, t * z * y + s * x, t * z * z + c, 0.0),
525                Vec4::new(0.0, 0.0, 0.0, 1.0),
526            ],
527        }
528    }
529
530    /// Creates a rotation matrix from a quaternion.
531    #[inline]
532    pub fn from_quat(q: Quaternion) -> Self {
533        let x = q.x;
534        let y = q.y;
535        let z = q.z;
536        let w = q.w;
537        let x2 = x + x;
538        let y2 = y + y;
539        let z2 = z + z;
540        let xx = x * x2;
541        let xy = x * y2;
542        let xz = x * z2;
543        let yy = y * y2;
544        let yz = y * z2;
545        let zz = z * z2;
546        let wx = w * x2;
547        let wy = w * y2;
548        let wz = w * z2;
549
550        Self::from_cols(
551            Vec4::new(1.0 - (yy + zz), xy + wz, xz - wy, 0.0),
552            Vec4::new(xy - wz, 1.0 - (xx + zz), yz + wx, 0.0),
553            Vec4::new(xz + wy, yz - wx, 1.0 - (xx + yy), 0.0),
554            Vec4::W,
555        )
556    }
557
558    /// Creates a right-handed perspective projection matrix with a [0, 1] depth range (ZO).
559    ///
560    /// # Arguments
561    ///
562    /// * `fov_y_radians`: Vertical field of view in radians.
563    /// * `aspect_ratio`: Width divided by height of the viewport.
564    /// * `z_near`: Distance to the near clipping plane (must be positive).
565    /// * `z_far`: Distance to the far clipping plane (must be positive and > `z_near`).
566    #[inline]
567    pub fn perspective_rh_zo(
568        fov_y_radians: f32,
569        aspect_ratio: f32,
570        z_near: f32,
571        z_far: f32,
572    ) -> Self {
573        assert!(z_near > 0.0 && z_far > z_near);
574        let tan_half_fovy = (fov_y_radians / 2.0).tan();
575        let f = 1.0 / tan_half_fovy;
576        let aa = f / aspect_ratio;
577        let bb = f;
578        let cc = z_far / (z_near - z_far);
579        let dd = (z_near * z_far) / (z_near - z_far);
580
581        Self::from_cols(
582            Vec4::new(aa, 0.0, 0.0, 0.0),
583            Vec4::new(0.0, bb, 0.0, 0.0),
584            Vec4::new(0.0, 0.0, cc, -1.0),
585            Vec4::new(0.0, 0.0, dd, 0.0),
586        )
587    }
588
589    /// Creates a right-handed orthographic projection matrix with a [0, 1] depth range (ZO).
590    #[inline]
591    pub fn orthographic_rh_zo(
592        left: f32,
593        right: f32,
594        bottom: f32,
595        top: f32,
596        z_near: f32,
597        z_far: f32,
598    ) -> Self {
599        let rml = right - left;
600        let rpl = right + left;
601        let tmb = top - bottom;
602        let tpb = top + bottom;
603        let fmn = z_far - z_near;
604        let aa = 2.0 / rml;
605        let bb = 2.0 / tmb;
606        let cc = -1.0 / fmn;
607        let dd = -rpl / rml;
608        let ee = -tpb / tmb;
609        let ff = -z_near / fmn;
610
611        Self::from_cols(
612            Vec4::new(aa, 0.0, 0.0, 0.0),
613            Vec4::new(0.0, bb, 0.0, 0.0),
614            Vec4::new(0.0, 0.0, cc, 0.0),
615            Vec4::new(dd, ee, ff, 1.0),
616        )
617    }
618
619    /// Creates a right-handed view matrix for a camera looking from `eye` towards `target`.
620    ///
621    /// # Arguments
622    ///
623    /// * `eye`: The position of the camera in world space.
624    /// * `target`: The point in world space that the camera is looking at.
625    /// * `up`: A vector indicating the "up" direction of the world (commonly `Vec3::Y`).
626    ///
627    /// # Returns
628    ///
629    /// Returns `Some(Mat4)` if a valid view matrix can be constructed, or `None` if
630    /// `eye` and `target` are too close, or if `up` is parallel to the view direction.
631    #[inline]
632    pub fn look_at_rh(eye: Vec3, target: Vec3, up: Vec3) -> Option<Self> {
633        let forward = target - eye;
634        if forward.length_squared() < crate::math::EPSILON * crate::math::EPSILON {
635            return None;
636        }
637        let f = forward.normalize();
638        let s = f.cross(up);
639        if s.length_squared() < crate::math::EPSILON * crate::math::EPSILON {
640            return None;
641        }
642        let s = s.normalize();
643        let u = s.cross(f);
644
645        Some(Self::from_cols(
646            Vec4::new(s.x, u.x, -f.x, 0.0),
647            Vec4::new(s.y, u.y, -f.y, 0.0),
648            Vec4::new(s.z, u.z, -f.z, 0.0),
649            Vec4::new(-eye.dot(s), -eye.dot(u), eye.dot(f), 1.0),
650        ))
651    }
652
653    /// View-projection matrix for one face of an omnidirectional cubemap
654    /// rendered from `position`.
655    ///
656    /// Builds a 90°-FOV perspective with `[NEAR, far]` depth range, then a
657    /// look-at matrix using [`CubeFace::forward`](crate::math::CubeFace::forward)
658    /// and [`CubeFace::up`](crate::math::CubeFace::up) in the wgpu cubemap
659    /// convention. The near plane is fixed at `0.1` — the matching
660    /// shader-side depth recompute (used in omnidirectional shadow sampling)
661    /// assumes the same constant.
662    ///
663    /// `far` should be the light's effective range in world units.
664    pub fn cube_face_view_proj(
665        position: crate::math::Vec3,
666        face: crate::math::CubeFace,
667        far: f32,
668    ) -> Mat4 {
669        const NEAR: f32 = 0.1;
670        let proj = Mat4::perspective_rh_zo(crate::math::FRAC_PI_2, 1.0, NEAR, far);
671        let view = Mat4::look_at_rh(position, position + face.forward(), face.up())
672            .unwrap_or(Mat4::IDENTITY);
673        proj * view
674    }
675
676    /// Six view-projection matrices, one per cube face, in
677    /// [`CubeFace::ALL`](crate::math::CubeFace::ALL) order
678    /// (`[+X, -X, +Y, -Y, +Z, -Z]`).
679    ///
680    /// Equivalent to:
681    /// ```ignore
682    /// CubeFace::ALL.map(|f| Mat4::cube_face_view_proj(position, f, far))
683    /// ```
684    pub fn cube_face_view_projs(position: crate::math::Vec3, far: f32) -> [Mat4; 6] {
685        crate::math::CubeFace::ALL.map(|f| Mat4::cube_face_view_proj(position, f, far))
686    }
687
688    /// Returns the transpose of the matrix, where rows and columns are swapped.
689    #[inline]
690    pub fn transpose(&self) -> Self {
691        Self::from_cols(
692            Vec4::new(
693                self.cols[0].x,
694                self.cols[1].x,
695                self.cols[2].x,
696                self.cols[3].x,
697            ),
698            Vec4::new(
699                self.cols[0].y,
700                self.cols[1].y,
701                self.cols[2].y,
702                self.cols[3].y,
703            ),
704            Vec4::new(
705                self.cols[0].z,
706                self.cols[1].z,
707                self.cols[2].z,
708                self.cols[3].z,
709            ),
710            Vec4::new(
711                self.cols[0].w,
712                self.cols[1].w,
713                self.cols[2].w,
714                self.cols[3].w,
715            ),
716        )
717    }
718
719    /// Computes the determinant of the matrix.
720    pub fn determinant(&self) -> f32 {
721        let c0 = self.cols[0];
722        let c1 = self.cols[1];
723        let c2 = self.cols[2];
724        let c3 = self.cols[3];
725
726        let m00 = c1.y * (c2.z * c3.w - c3.z * c2.w) - c2.y * (c1.z * c3.w - c3.z * c1.w)
727            + c3.y * (c1.z * c2.w - c2.z * c1.w);
728        let m01 = c0.y * (c2.z * c3.w - c3.z * c2.w) - c2.y * (c0.z * c3.w - c3.z * c0.w)
729            + c3.y * (c0.z * c2.w - c2.z * c0.w);
730        let m02 = c0.y * (c1.z * c3.w - c3.z * c1.w) - c1.y * (c0.z * c3.w - c3.z * c0.w)
731            + c3.y * (c0.z * c1.w - c1.z * c0.w);
732        let m03 = c0.y * (c1.z * c2.w - c2.z * c1.w) - c1.y * (c0.z * c2.w - c2.z * c0.w)
733            + c2.y * (c0.z * c1.w - c1.z * c0.w);
734
735        c0.x * m00 - c1.x * m01 + c2.x * m02 - c3.x * m03
736    }
737
738    /// Computes the inverse of the matrix.
739    /// Returns `None` if the matrix is not invertible.
740    pub fn inverse(&self) -> Option<Self> {
741        let c0 = self.cols[0];
742        let c1 = self.cols[1];
743        let c2 = self.cols[2];
744        let c3 = self.cols[3];
745
746        let a00 = c1.y * (c2.z * c3.w - c3.z * c2.w) - c2.y * (c1.z * c3.w - c3.z * c1.w)
747            + c3.y * (c1.z * c2.w - c2.z * c1.w);
748        let a01 = -(c1.x * (c2.z * c3.w - c3.z * c2.w) - c2.x * (c1.z * c3.w - c3.z * c1.w)
749            + c3.x * (c1.z * c2.w - c2.z * c1.w));
750        let a02 = c1.x * (c2.y * c3.w - c3.y * c2.w) - c2.x * (c1.y * c3.w - c3.y * c1.w)
751            + c3.x * (c1.y * c2.w - c2.y * c1.w);
752        let a03 = -(c1.x * (c2.y * c3.z - c3.y * c2.z) - c2.x * (c1.y * c3.z - c3.y * c1.z)
753            + c3.x * (c1.y * c2.z - c2.y * c1.z));
754
755        let a10 = -(c0.y * (c2.z * c3.w - c3.z * c2.w) - c2.y * (c0.z * c3.w - c3.z * c0.w)
756            + c3.y * (c0.z * c2.w - c2.z * c0.w));
757        let a11 = c0.x * (c2.z * c3.w - c3.z * c2.w) - c2.x * (c0.z * c3.w - c3.z * c0.w)
758            + c3.x * (c0.z * c2.w - c2.z * c0.w);
759        let a12 = -(c0.x * (c2.y * c3.w - c3.y * c2.w) - c2.x * (c0.y * c3.w - c3.y * c0.w)
760            + c3.x * (c0.y * c2.w - c2.y * c0.w));
761        let a13 = c0.x * (c2.y * c3.z - c3.y * c2.z) - c2.x * (c0.y * c3.z - c3.y * c0.z)
762            + c3.x * (c0.y * c2.z - c2.y * c0.z);
763
764        let a20 = c0.y * (c1.z * c3.w - c3.z * c1.w) - c1.y * (c0.z * c3.w - c3.z * c0.w)
765            + c3.y * (c0.z * c1.w - c1.z * c0.w);
766        let a21 = -(c0.x * (c1.z * c3.w - c3.z * c1.w) - c1.x * (c0.z * c3.w - c3.z * c0.w)
767            + c3.x * (c0.z * c1.w - c1.z * c0.w));
768        let a22 = c0.x * (c1.y * c3.w - c3.y * c1.w) - c1.x * (c0.y * c3.w - c3.y * c0.w)
769            + c3.x * (c0.y * c1.w - c1.y * c0.w);
770        let a23 = -(c0.x * (c1.y * c3.z - c3.y * c1.z) - c1.x * (c0.y * c3.z - c3.y * c0.z)
771            + c3.x * (c0.y * c1.z - c1.y * c0.z));
772
773        let a30 = -(c0.y * (c1.z * c2.w - c2.z * c1.w) - c1.y * (c0.z * c2.w - c2.z * c0.w)
774            + c2.y * (c0.z * c1.w - c1.z * c0.w));
775        let a31 = c0.x * (c1.z * c2.w - c2.z * c1.w) - c1.x * (c0.z * c2.w - c2.z * c0.w)
776            + c2.x * (c0.z * c1.w - c1.z * c0.w);
777        let a32 = -(c0.x * (c1.y * c2.w - c2.y * c1.w) - c1.x * (c0.y * c2.w - c2.y * c0.w)
778            + c2.x * (c0.y * c1.w - c1.y * c0.w));
779        let a33 = c0.x * (c1.y * c2.z - c2.y * c1.z) - c1.x * (c0.y * c2.z - c2.y * c0.z)
780            + c2.x * (c0.y * c1.z - c1.y * c0.z);
781
782        let det = c0.x * a00 + c1.x * a10 + c2.x * a20 + c3.x * a30;
783        if det.abs() < crate::math::EPSILON {
784            return None;
785        }
786        let inv_det = 1.0 / det;
787
788        Some(Self::from_cols(
789            Vec4::new(a00 * inv_det, a10 * inv_det, a20 * inv_det, a30 * inv_det),
790            Vec4::new(a01 * inv_det, a11 * inv_det, a21 * inv_det, a31 * inv_det),
791            Vec4::new(a02 * inv_det, a12 * inv_det, a22 * inv_det, a32 * inv_det),
792            Vec4::new(a03 * inv_det, a13 * inv_det, a23 * inv_det, a33 * inv_det),
793        ))
794    }
795
796    /// Computes the inverse of an affine transformation matrix more efficiently
797    /// and with better numerical stability than the general `inverse` method.
798    ///
799    /// An affine matrix is one composed of only translation, rotation, and scale.
800    ///
801    /// # Returns
802    ///
803    /// `None` if the matrix is not affine or is not invertible.
804    #[inline]
805    pub fn affine_inverse(&self) -> Option<Self> {
806        let c0 = self.cols[0].truncate();
807        let c1 = self.cols[1].truncate();
808        let c2 = self.cols[2].truncate();
809        let translation = self.cols[3].truncate();
810        let det3x3 = c0.x * (c1.y * c2.z - c2.y * c1.z) - c1.x * (c0.y * c2.z - c2.y * c0.z)
811            + c2.x * (c0.y * c1.z - c1.y * c0.z);
812
813        if det3x3.abs() < crate::math::EPSILON {
814            return None;
815        }
816
817        let inv_det3x3 = 1.0 / det3x3;
818        let inv00 = (c1.y * c2.z - c2.y * c1.z) * inv_det3x3;
819        let inv10 = -(c2.y * c0.z - c0.y * c2.z) * inv_det3x3;
820        let inv20 = (c0.y * c1.z - c1.y * c0.z) * inv_det3x3;
821        let inv01 = -(c2.x * c1.z - c1.x * c2.z) * inv_det3x3;
822        let inv11 = (c0.x * c2.z - c2.x * c0.z) * inv_det3x3;
823        let inv21 = -(c1.x * c0.z - c0.x * c1.z) * inv_det3x3;
824        let inv02 = (c1.x * c2.y - c2.x * c1.y) * inv_det3x3;
825        let inv12 = -(c2.x * c0.y - c0.x * c2.y) * inv_det3x3;
826        let inv22 = (c0.x * c1.y - c1.x * c0.y) * inv_det3x3;
827        let inv_tx = -(inv00 * translation.x + inv01 * translation.y + inv02 * translation.z);
828        let inv_ty = -(inv10 * translation.x + inv11 * translation.y + inv12 * translation.z);
829        let inv_tz = -(inv20 * translation.x + inv21 * translation.y + inv22 * translation.z);
830
831        Some(Self::from_cols(
832            Vec4::new(inv00, inv10, inv20, 0.0),
833            Vec4::new(inv01, inv11, inv21, 0.0),
834            Vec4::new(inv02, inv12, inv22, 0.0),
835            Vec4::new(inv_tx, inv_ty, inv_tz, 1.0),
836        ))
837    }
838
839    /// Transforms a 3D point by this matrix (treating it as a homogeneous point with w=1).
840    ///
841    /// This applies the full affine transformation including translation, rotation, and scale.
842    #[inline]
843    pub fn transform_point(&self, p: Vec3) -> Vec3 {
844        (*self * Vec4::from_vec3(p, 1.0)).truncate()
845    }
846
847    /// Transforms a 3D direction vector by this matrix (treating it as w=0).
848    ///
849    /// This applies rotation and scale but **not** translation. Use this for
850    /// normals, axes, and other direction vectors.
851    #[inline]
852    pub fn transform_vector(&self, v: Vec3) -> Vec3 {
853        (*self * Vec4::from_vec3(v, 0.0)).truncate()
854    }
855}
856
857// --- Operators Overloading ---
858
859impl Default for Mat4 {
860    /// Returns the 4x4 identity matrix.
861    #[inline]
862    fn default() -> Self {
863        Self::IDENTITY
864    }
865}
866
867impl Mul<Mat4> for Mat4 {
868    type Output = Self;
869    /// Multiplies this matrix by another `Mat4`. Note that matrix multiplication is not commutative.
870    #[inline]
871    fn mul(self, rhs: Mat4) -> Self::Output {
872        let mut result_cols = [Vec4 {
873            x: 0.0,
874            y: 0.0,
875            z: 0.0,
876            w: 0.0,
877        }; 4];
878        for (c_idx, target_col_ref_mut) in result_cols.iter_mut().enumerate().take(4) {
879            let col_from_rhs = rhs.cols[c_idx];
880            *target_col_ref_mut = Vec4 {
881                x: self.get_row(0).dot(col_from_rhs),
882                y: self.get_row(1).dot(col_from_rhs),
883                z: self.get_row(2).dot(col_from_rhs),
884                w: self.get_row(3).dot(col_from_rhs),
885            };
886        }
887        Mat4 { cols: result_cols }
888    }
889}
890
891impl Mul<Vec4> for Mat4 {
892    type Output = Vec4;
893    /// Transforms a `Vec4` by this matrix.
894    #[inline]
895    fn mul(self, rhs: Vec4) -> Self::Output {
896        self.cols[0] * rhs.x + self.cols[1] * rhs.y + self.cols[2] * rhs.z + self.cols[3] * rhs.w
897    }
898}
899
900// --- Tests ---
901
902#[cfg(test)]
903mod tests {
904    use super::*;
905    use crate::math::{approx_eq, matrix::Mat4, quaternion::Quaternion, vector::Vec3, PI};
906
907    fn vec3_approx_eq(a: Vec3, b: Vec3) -> bool {
908        approx_eq(a.x, b.x) && approx_eq(a.y, b.y) && approx_eq(a.z, b.z)
909    }
910
911    fn mat3_approx_eq(a: Mat3, b: Mat3) -> bool {
912        vec3_approx_eq(a.cols[0], b.cols[0])
913            && vec3_approx_eq(a.cols[1], b.cols[1])
914            && vec3_approx_eq(a.cols[2], b.cols[2])
915    }
916
917    fn vec4_approx_eq(a: Vec4, b: Vec4) -> bool {
918        approx_eq(a.x, b.x) && approx_eq(a.y, b.y) && approx_eq(a.z, b.z) && approx_eq(a.w, b.w)
919    }
920
921    fn mat4_approx_eq(a: Mat4, b: Mat4) -> bool {
922        vec4_approx_eq(a.cols[0], b.cols[0])
923            && vec4_approx_eq(a.cols[1], b.cols[1])
924            && vec4_approx_eq(a.cols[2], b.cols[2])
925            && vec4_approx_eq(a.cols[3], b.cols[3])
926    }
927
928    // --- Tests for Mat3 ---
929
930    #[test]
931    fn test_mat3_identity_default() {
932        assert_eq!(Mat3::default(), Mat3::IDENTITY);
933
934        let m = Mat3::from_scale(Vec3::new(1.0, 2.0, 3.0));
935        assert!(mat3_approx_eq(m * Mat3::IDENTITY, m));
936        assert!(mat3_approx_eq(Mat3::IDENTITY * m, m));
937    }
938
939    #[test]
940    fn test_mat3_from_scale() {
941        let s = Vec3::new(2.0, -3.0, 0.5);
942        let m = Mat3::from_scale(s);
943        let v = Vec3::new(1.0, 1.0, 1.0);
944        assert!(vec3_approx_eq(m * v, s)); // Scaling (1,1,1) should yield the scale vector
945    }
946
947    #[test]
948    fn test_mat3_rotations() {
949        let angle = PI / 6.0; // 30 degrees
950        let mx = Mat3::from_rotation_x(angle);
951        let my = Mat3::from_rotation_y(angle);
952        let mz = Mat3::from_rotation_z(angle);
953
954        let p = Vec3::Y; // Point on Y axis
955        let expected_px = Vec3::new(0.0, angle.cos(), angle.sin());
956        assert!(vec3_approx_eq(mx * p, expected_px));
957
958        let p = Vec3::X; // Point on X axis
959        let expected_py = Vec3::new(angle.cos(), 0.0, -angle.sin()); // RH
960        assert!(vec3_approx_eq(my * p, expected_py));
961
962        let p = Vec3::X; // Point on X axis
963        let expected_pz = Vec3::new(angle.cos(), angle.sin(), 0.0);
964        assert!(vec3_approx_eq(mz * p, expected_pz));
965    }
966
967    #[test]
968    fn test_mat3_from_axis_angle() {
969        let axis = Vec3::new(1.0, 1.0, 1.0).normalize();
970        let angle = 1.2 * PI;
971        let m = Mat3::from_axis_angle(axis, angle);
972
973        // Rotation around (1,1,1) axis should permute basis vectors
974        let v = Vec3::X;
975        let v_rotated = m * v;
976
977        // Check if length is preserved
978        assert!(approx_eq(v_rotated.length(), v.length()));
979
980        // Specific check is hard without known values, but ensure it's not identity or zero
981        assert!(v_rotated.distance_squared(v) > EPSILON);
982    }
983
984    #[test]
985    fn test_mat3_from_quat() {
986        let axis = Vec3::new(1.0, -2.0, 3.0).normalize();
987        let angle = PI / 7.0;
988        let q = Quaternion::from_axis_angle(axis, angle);
989        let m_from_q = Mat3::from_quat(q);
990
991        let v = Vec3::new(0.5, 1.0, -0.2);
992        let v_rotated_q = q * v;
993        let v_rotated_m = m_from_q * v;
994
995        assert!(vec3_approx_eq(v_rotated_q, v_rotated_m));
996    }
997
998    #[test]
999    fn test_mat3_determinant() {
1000        assert!(approx_eq(Mat3::IDENTITY.determinant(), 1.0));
1001        assert!(approx_eq(Mat3::ZERO.determinant(), 0.0));
1002
1003        let m_scale = Mat3::from_scale(Vec3::new(2.0, 3.0, 4.0));
1004        assert!(approx_eq(m_scale.determinant(), 24.0));
1005
1006        let m_rot = Mat3::from_rotation_y(PI / 5.0);
1007        assert!(approx_eq(m_rot.determinant(), 1.0)); // Rotations preserve volume
1008    }
1009
1010    #[test]
1011    fn test_mat3_transpose() {
1012        let m = Mat3::from_cols(
1013            Vec3::new(1.0, 2.0, 3.0),
1014            Vec3::new(4.0, 5.0, 6.0),
1015            Vec3::new(7.0, 8.0, 9.0),
1016        );
1017        let mt = m.transpose();
1018        let expected_mt = Mat3::from_cols(
1019            Vec3::new(1.0, 4.0, 7.0),
1020            Vec3::new(2.0, 5.0, 8.0),
1021            Vec3::new(3.0, 6.0, 9.0),
1022        );
1023
1024        assert!(mat3_approx_eq(mt, expected_mt));
1025        assert!(mat3_approx_eq(m.transpose().transpose(), m)); // Double transpose
1026    }
1027
1028    #[test]
1029    fn test_mat3_inverse() {
1030        let m = Mat3::from_rotation_z(PI / 3.0) * Mat3::from_scale(Vec3::new(1.0, 2.0, 0.5));
1031        let inv_m = m.inverse().expect("Matrix should be invertible");
1032        let identity = m * inv_m;
1033        assert!(
1034            mat3_approx_eq(identity, Mat3::IDENTITY),
1035            "M * inv(M) should be Identity"
1036        );
1037
1038        let singular = Mat3::from_scale(Vec3::new(1.0, 0.0, 1.0));
1039        assert!(
1040            singular.inverse().is_none(),
1041            "Singular matrix inverse should be None"
1042        );
1043    }
1044
1045    #[test]
1046    fn test_mat3_mul_vec3() {
1047        let m = Mat3::from_rotation_z(PI / 2.0); // Rotate 90 deg around Z
1048        let v = Vec3::X; // (1, 0, 0)
1049        let expected_v = Vec3::Y; // (0, 1, 0)
1050        assert!(vec3_approx_eq(m * v, expected_v));
1051    }
1052
1053    #[test]
1054    fn test_mat3_mul_mat3() {
1055        let rot90z = Mat3::from_rotation_z(PI / 2.0);
1056        let rot180z = rot90z * rot90z;
1057        let expected_rot180z = Mat3::from_rotation_z(PI);
1058        assert!(mat3_approx_eq(rot180z, expected_rot180z));
1059    }
1060
1061    #[test]
1062    fn test_mat3_conversions() {
1063        let m4 = Mat4::from_translation(Vec3::new(10., 20., 30.)) * Mat4::from_rotation_x(PI / 4.0);
1064        let m3 = Mat3::from_mat4(&m4);
1065        let m4_again = m3.to_mat4();
1066
1067        // Check if rotation part was extracted correctly
1068        let v = Vec3::Y;
1069        let v_rot_m3 = m3 * v;
1070        let v_rot_m4 = Mat4::from_rotation_x(PI / 4.0) * Vec4::from_vec3(v, 0.0); // Rotate as vector
1071        assert!(vec3_approx_eq(v_rot_m3, v_rot_m4.truncate()));
1072
1073        // Check if embedding back into Mat4 worked (translation should be zero)
1074        let origin = Vec4::new(0.0, 0.0, 0.0, 1.0);
1075        let transformed_origin = m4_again * origin;
1076        assert!(approx_eq(transformed_origin.x, 0.0));
1077        assert!(approx_eq(transformed_origin.y, 0.0));
1078        assert!(approx_eq(transformed_origin.z, 0.0));
1079        assert!(approx_eq(transformed_origin.w, 1.0));
1080    }
1081
1082    #[test]
1083    fn test_mat3_index() {
1084        let mut m = Mat3::from_cols(Vec3::X, Vec3::Y, Vec3::Z);
1085        assert_eq!(m[0], Vec3::X);
1086        assert_eq!(m[1], Vec3::Y);
1087        assert_eq!(m[2], Vec3::Z);
1088        m[0] = Vec3::ONE;
1089        assert_eq!(m.cols[0], Vec3::ONE);
1090    }
1091
1092    #[test]
1093    #[should_panic]
1094    fn test_mat3_index_out_of_bounds() {
1095        let m = Mat3::IDENTITY;
1096        let _ = m[3]; // Should panic
1097    }
1098
1099    // --- End of Mat3 Tests ---
1100
1101    // --- Tests for Mat4 ---
1102
1103    #[test]
1104    fn test_identity() {
1105        assert_eq!(Mat4::default(), Mat4::IDENTITY);
1106        let m = Mat4::from_translation(Vec3::new(1.0, 2.0, 3.0));
1107        assert!(mat4_approx_eq(m * Mat4::IDENTITY, m));
1108        assert!(mat4_approx_eq(Mat4::IDENTITY * m, m));
1109    }
1110
1111    #[test]
1112    fn test_from_quat() {
1113        let axis = Vec3::new(1.0, 2.0, 3.0).normalize();
1114        let angle = PI / 5.0;
1115        let q = Quaternion::from_axis_angle(axis, angle);
1116        let m_from_q = Mat4::from_quat(q);
1117
1118        let v = Vec3::new(5.0, -1.0, 2.0);
1119
1120        let v_rotated_q = q * v; // Rotate using quaternion directly
1121                                 // Rotate using matrix: convert v to Vec4(point), multiply, convert back
1122        let v4 = Vec4::from_vec3(v, 1.0);
1123        let v_rotated_m4 = m_from_q * v4;
1124        let v_rotated_m = v_rotated_m4.truncate();
1125
1126        // Compare results
1127        assert!(approx_eq(v_rotated_q.x, v_rotated_m.x));
1128        assert!(approx_eq(v_rotated_q.y, v_rotated_m.y));
1129        assert!(approx_eq(v_rotated_q.z, v_rotated_m.z));
1130    }
1131
1132    #[test]
1133    fn test_translation() {
1134        let t = Vec3::new(1.0, 2.0, 3.0);
1135        let m = Mat4::from_translation(t);
1136        let p = Vec4::new(1.0, 1.0, 1.0, 1.0);
1137        let expected_p = Vec4::new(2.0, 3.0, 4.0, 1.0);
1138
1139        assert!(vec4_approx_eq(m * p, expected_p));
1140    }
1141
1142    #[test]
1143    fn test_scale() {
1144        let s = Vec3::new(2.0, 3.0, 4.0);
1145        let m = Mat4::from_scale(s);
1146        let p = Vec4::new(1.0, 1.0, 1.0, 1.0);
1147        let expected_p = Vec4::new(2.0, 3.0, 4.0, 1.0);
1148        assert!(vec4_approx_eq(m * p, expected_p));
1149    }
1150
1151    #[test]
1152    fn test_rotation_x() {
1153        let angle = PI / 2.0; // 90 degrees
1154        let m = Mat4::from_rotation_x(angle);
1155        let p = Vec4::new(0.0, 1.0, 0.0, 1.0); // Point on Y axis
1156        let expected_p = Vec4::new(0.0, 0.0, 1.0, 1.0); // Should rotate to Z axis
1157        assert!(vec4_approx_eq(m * p, expected_p));
1158    }
1159
1160    #[test]
1161    fn test_rotation_y() {
1162        let angle = PI / 2.0; // 90 degrees
1163        let m = Mat4::from_rotation_y(angle);
1164        let p = Vec4::new(1.0, 0.0, 0.0, 1.0); // Point on X axis
1165        let expected_p = Vec4::new(0.0, 0.0, -1.0, 1.0); // Should rotate to -Z axis
1166
1167        assert!(vec4_approx_eq(m * p, expected_p));
1168    }
1169
1170    #[test]
1171    fn test_rotation_z() {
1172        let angle = PI / 2.0; // 90 degrees
1173        let m = Mat4::from_rotation_z(angle);
1174        let p = Vec4::new(1.0, 0.0, 0.0, 1.0); // Point on X axis
1175        let expected_p = Vec4::new(0.0, 1.0, 0.0, 1.0); // Should rotate to Y axis
1176        assert!(vec4_approx_eq(m * p, expected_p));
1177    }
1178
1179    #[test]
1180    fn test_transpose() {
1181        let m = Mat4::from_cols(
1182            Vec4::new(1., 2., 3., 4.),
1183            Vec4::new(5., 6., 7., 8.),
1184            Vec4::new(9., 10., 11., 12.),
1185            Vec4::new(13., 14., 15., 16.),
1186        );
1187        let mt = m.transpose();
1188        let expected_mt = Mat4::from_cols(
1189            Vec4::new(1., 5., 9., 13.),
1190            Vec4::new(2., 6., 10., 14.),
1191            Vec4::new(3., 7., 11., 15.),
1192            Vec4::new(4., 8., 12., 16.),
1193        );
1194        assert_eq!(mt.cols[0], expected_mt.cols[0]); // Compare columns after transpose
1195        assert_eq!(mt.cols[1], expected_mt.cols[1]);
1196        assert_eq!(mt.cols[2], expected_mt.cols[2]);
1197        assert_eq!(mt.cols[3], expected_mt.cols[3]);
1198
1199        // Test double transpose
1200        assert!(mat4_approx_eq(m.transpose().transpose(), m));
1201    }
1202
1203    #[test]
1204    fn test_mul_mat4() {
1205        let t = Mat4::from_translation(Vec3::new(1.0, 0.0, 0.0));
1206        let r = Mat4::from_rotation_z(PI / 2.0);
1207
1208        // Order matters: Translate then Rotate
1209        let tr = r * t;
1210        let p = Vec4::new(1.0, 0.0, 0.0, 1.0); // Point at (1,0,0)
1211                                               // 1. Translate: p becomes (2, 0, 0, 1)
1212                                               // 2. Rotate Z 90: (2, 0, 0) becomes (0, 2, 0)
1213        let expected_tr = Vec4::new(0.0, 2.0, 0.0, 1.0);
1214        assert!(vec4_approx_eq(tr * p, expected_tr));
1215
1216        // Order matters: Rotate then Translate
1217        let rt = t * r;
1218        // 1. Rotate Z 90: p becomes (0, 1, 0, 1)
1219        // 2. Translate: (0, 1, 0) becomes (1, 1, 0)
1220        let expected_rt = Vec4::new(1.0, 1.0, 0.0, 1.0);
1221        assert!(vec4_approx_eq(rt * p, expected_rt));
1222    }
1223
1224    #[test]
1225    fn test_inverse() {
1226        let m = Mat4::from_translation(Vec3::new(1., 2., 3.))
1227            * Mat4::from_rotation_y(PI / 4.0)
1228            * Mat4::from_scale(Vec3::new(1., 2., 1.));
1229
1230        let inv_m = m.inverse().expect("Matrix should be invertible");
1231        let identity = m * inv_m;
1232
1233        // Check if M * M^-1 is close to identity
1234        assert!(
1235            mat4_approx_eq(identity, Mat4::IDENTITY),
1236            "M * inv(M) should be Identity"
1237        );
1238
1239        // Check singular matrix (e.g., scale with zero)
1240        let singular = Mat4::from_scale(Vec3::new(1.0, 0.0, 1.0));
1241        assert!(
1242            singular.inverse().is_none(),
1243            "Singular matrix inverse should be None"
1244        );
1245    }
1246
1247    #[test]
1248    fn test_affine_inverse() {
1249        let t = Mat4::from_translation(Vec3::new(1., 2., 3.));
1250        let r = Mat4::from_rotation_y(PI / 3.0);
1251        let s = Mat4::from_scale(Vec3::new(1., 2., 0.5));
1252        let m = t * r * s; // Combined affine transform
1253
1254        let inv_m = m.inverse().expect("Matrix should be invertible");
1255        let affine_inv_m = m
1256            .affine_inverse()
1257            .expect("Matrix should be affine invertible");
1258
1259        // Check if affine inverse matches general inverse for this case
1260        assert!(
1261            mat4_approx_eq(inv_m, affine_inv_m),
1262            "Affine inverse should match general inverse"
1263        );
1264
1265        // Check M * inv(M) == Identity using affine inverse
1266        let identity = m * affine_inv_m;
1267        assert!(
1268            mat4_approx_eq(identity, Mat4::IDENTITY),
1269            "M * affine_inv(M) should be Identity"
1270        );
1271
1272        // Test singular affine matrix
1273        let singular_s = Mat4::from_scale(Vec3::new(1.0, 0.0, 1.0));
1274        let singular_m = t * singular_s;
1275        assert!(
1276            singular_m.affine_inverse().is_none(),
1277            "Singular affine matrix inverse should be None"
1278        );
1279    }
1280
1281    #[test]
1282    fn test_perspective_rh_zo() {
1283        let fov = PI / 4.0; // 45 degrees
1284        let aspect = 16.0 / 9.0;
1285        let near = 0.1;
1286        let far = 100.0;
1287
1288        let m = Mat4::perspective_rh_zo(fov, aspect, near, far);
1289        assert!(approx_eq(m.cols[0].x, 1.0 / (aspect * (fov / 2.0).tan())));
1290        assert!(approx_eq(m.cols[1].y, 1.0 / ((fov / 2.0).tan())));
1291        assert!(approx_eq(m.cols[2].z, -far / (far - near)));
1292        assert!(approx_eq(m.cols[3].z, -(far * near) / (far - near)));
1293    }
1294
1295    #[test]
1296    fn test_orthographic_rh_zo() {
1297        let left = -1.0;
1298        let right = 1.0;
1299        let bottom = -1.0;
1300        let top = 1.0;
1301        let near = 0.1;
1302        let far = 100.0;
1303        let m = Mat4::orthographic_rh_zo(left, right, bottom, top, near, far);
1304
1305        // Check scale factors
1306        assert!(approx_eq(m.cols[0].x, 2.0 / (right - left)));
1307        assert!(approx_eq(m.cols[1].y, 2.0 / (top - bottom)));
1308        assert!(approx_eq(m.cols[2].z, -1.0 / (far - near)));
1309
1310        // Check translation factors
1311        assert!(approx_eq(m.cols[3].x, -(right + left) / (right - left)));
1312        assert!(approx_eq(m.cols[3].y, -(top + bottom) / (top - bottom)));
1313        assert!(approx_eq(m.cols[3].z, -near / (far - near))); // -near and not -(far + near)
1314    }
1315
1316    #[test]
1317    fn test_look_at_rh() {
1318        let eye = Vec3::new(0.0, 0.0, 5.0);
1319        let target = Vec3::new(0.0, 0.0, 0.0);
1320        let up = Vec3::new(0.0, 1.0, 0.0);
1321
1322        let m = Mat4::look_at_rh(eye, target, up).expect("look_at_rh should return Some(Mat4)");
1323
1324        // Forward direction (third column, third row): should be +1.0 for a right-handed system
1325        assert!(approx_eq(m.cols[2].z, 1.0));
1326
1327        // Translation part (fourth column, third row): should be -eye · forward = -5.0
1328        assert!(approx_eq(m.cols[3].z, -5.0));
1329    }
1330
1331    #[test]
1332    fn test_look_at_rh_invalid() {
1333        let eye = Vec3::new(0.0, 0.0, 5.0);
1334        let target = Vec3::new(0.0, 0.0, 5.0); // Same as eye
1335        let up = Vec3::new(0.0, 1.0, 0.0);
1336
1337        // This should panic or return None (depending on implementation)
1338        assert!(Mat4::look_at_rh(eye, target, up).is_none());
1339    }
1340
1341    // --- Cubemap helpers -----------------------------------------------
1342
1343    /// Helper: clip-space depth (z/w) of a world-space point through a
1344    /// view-projection matrix.
1345    fn clip_depth(view_proj: &Mat4, world: Vec3) -> f32 {
1346        let clip = *view_proj * crate::math::Vec4::new(world.x, world.y, world.z, 1.0);
1347        clip.z / clip.w
1348    }
1349
1350    #[test]
1351    fn cube_face_view_projs_returns_six_in_wgpu_order() {
1352        let m = Mat4::cube_face_view_projs(Vec3::ZERO, 100.0);
1353        assert_eq!(m.len(), 6);
1354        // The six matrices are pairwise distinct (different views).
1355        for i in 0..6 {
1356            for j in (i + 1)..6 {
1357                let same = (0..16).all(|k| {
1358                    let row = k / 4;
1359                    let col = k % 4;
1360                    let a = m[i].cols[col][row];
1361                    let b = m[j].cols[col][row];
1362                    crate::math::approx_eq(a, b)
1363                });
1364                assert!(!same, "faces {i} and {j} produced identical matrices");
1365            }
1366        }
1367    }
1368
1369    #[test]
1370    fn cube_face_matrices_project_axis_aligned_points_into_correct_face() {
1371        // For each face, a point one unit away along that face's forward
1372        // direction must land *in front of* the camera (z/w in [0, 1] under
1373        // RH-ZO) and roughly at the centre of the screen (x/w ≈ y/w ≈ 0).
1374        let pos = Vec3::ZERO;
1375        let far = 100.0;
1376        for face in crate::math::CubeFace::ALL {
1377            let vp = Mat4::cube_face_view_proj(pos, face, far);
1378            let target = pos + face.forward() * 5.0; // safely between near (0.1) and far (100)
1379            let clip = vp * crate::math::Vec4::new(target.x, target.y, target.z, 1.0);
1380            assert!(
1381                clip.w > 0.0,
1382                "face {face:?}: target should be in front (w>0), got w = {}",
1383                clip.w
1384            );
1385            let z_norm = clip.z / clip.w;
1386            assert!(
1387                (0.0..=1.0).contains(&z_norm),
1388                "face {face:?}: target z/w {z_norm} not in [0, 1]"
1389            );
1390            let x_norm = clip.x / clip.w;
1391            let y_norm = clip.y / clip.w;
1392            assert!(
1393                x_norm.abs() < 1e-3 && y_norm.abs() < 1e-3,
1394                "face {face:?}: axis-aligned point should be near screen centre, got ({x_norm}, {y_norm})"
1395            );
1396        }
1397    }
1398
1399    #[test]
1400    fn cube_far_plane_maps_to_one() {
1401        let pos = Vec3::ZERO;
1402        let far = 50.0;
1403        for face in crate::math::CubeFace::ALL {
1404            let vp = Mat4::cube_face_view_proj(pos, face, far);
1405            let z = clip_depth(&vp, pos + face.forward() * far);
1406            assert!(
1407                (z - 1.0).abs() < 1e-4,
1408                "face {face:?}: z at far plane should be ≈ 1.0, got {z}"
1409            );
1410        }
1411    }
1412
1413    #[test]
1414    fn cube_near_plane_maps_to_zero() {
1415        let pos = Vec3::ZERO;
1416        let far = 50.0;
1417        // The near constant inside `cube_face_view_proj` is 0.1.
1418        for face in crate::math::CubeFace::ALL {
1419            let vp = Mat4::cube_face_view_proj(pos, face, far);
1420            let z = clip_depth(&vp, pos + face.forward() * 0.1);
1421            assert!(
1422                z.abs() < 1e-4,
1423                "face {face:?}: z at near plane should be ≈ 0.0, got {z}"
1424            );
1425        }
1426    }
1427
1428    #[test]
1429    fn cube_face_view_proj_idempotent_with_explicit_lookat() {
1430        // Regression: ensure the helper produces the same matrix as the
1431        // unfolded (proj * look_at_rh) sequence.
1432        let pos = Vec3::new(1.0, 2.0, 3.0);
1433        let far = 80.0;
1434        for face in crate::math::CubeFace::ALL {
1435            let helper = Mat4::cube_face_view_proj(pos, face, far);
1436            let proj = Mat4::perspective_rh_zo(crate::math::FRAC_PI_2, 1.0, 0.1, far);
1437            let view = Mat4::look_at_rh(pos, pos + face.forward(), face.up())
1438                .expect("non-degenerate basis");
1439            let expected = proj * view;
1440            for col in 0..4 {
1441                for row in 0..4 {
1442                    assert!(
1443                        crate::math::approx_eq_eps(
1444                            helper.cols[col][row],
1445                            expected.cols[col][row],
1446                            1e-5,
1447                        ),
1448                        "face {face:?}: mismatch at col {col} row {row}"
1449                    );
1450                }
1451            }
1452        }
1453    }
1454
1455    // --- End of Tests For Mat4 ---
1456}