pub const IDENTITY_MAT4: [f64; 16] = [
1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, ];
pub fn mat4_mul(a: &[f64; 16], b: &[f64; 16]) -> [f64; 16] {
let mut out = [0.0f64; 16];
for r in 0..4 {
for c in 0..4 {
let mut v = 0.0;
for k in 0..4 {
v += a[r * 4 + k] * b[k * 4 + c];
}
out[r * 4 + c] = v;
}
}
out
}
pub fn mat4_transform_point(m: &[f64; 16], p: [f32; 3]) -> [f32; 3] {
let x = p[0] as f64;
let y = p[1] as f64;
let z = p[2] as f64;
let nx = x * m[0] + y * m[4] + z * m[8] + m[12];
let ny = x * m[1] + y * m[5] + z * m[9] + m[13];
let nz = x * m[2] + y * m[6] + z * m[10] + m[14];
[nx as f32, ny as f32, nz as f32]
}
pub fn mat4_transform_vec(m: &[f64; 16], v: [f32; 3]) -> [f32; 3] {
let x = v[0] as f64;
let y = v[1] as f64;
let z = v[2] as f64;
let nx = x * m[0] + y * m[4] + z * m[8];
let ny = x * m[1] + y * m[5] + z * m[9];
let nz = x * m[2] + y * m[6] + z * m[10];
[nx as f32, ny as f32, nz as f32]
}
pub const MAT4_SINGULAR_DETERMINANT: f64 = 1e-12;
pub fn mat4_inverse(m: &[f64; 16]) -> Option<[f64; 16]> {
let mut inv = [0.0f64; 16];
inv[0] =
m[5] * m[10] * m[15] - m[5] * m[11] * m[14] - m[9] * m[6] * m[15] + m[9] * m[7] * m[14] + m[13] * m[6] * m[11]
- m[13] * m[7] * m[10];
inv[4] =
-m[4] * m[10] * m[15] + m[4] * m[11] * m[14] + m[8] * m[6] * m[15] - m[8] * m[7] * m[14] - m[12] * m[6] * m[11]
+ m[12] * m[7] * m[10];
inv[8] =
m[4] * m[9] * m[15] - m[4] * m[11] * m[13] - m[8] * m[5] * m[15] + m[8] * m[7] * m[13] + m[12] * m[5] * m[11]
- m[12] * m[7] * m[9];
inv[12] =
-m[4] * m[9] * m[14] + m[4] * m[10] * m[13] + m[8] * m[5] * m[14] - m[8] * m[6] * m[13] - m[12] * m[5] * m[10]
+ m[12] * m[6] * m[9];
inv[1] =
-m[1] * m[10] * m[15] + m[1] * m[11] * m[14] + m[9] * m[2] * m[15] - m[9] * m[3] * m[14] - m[13] * m[2] * m[11]
+ m[13] * m[3] * m[10];
inv[5] =
m[0] * m[10] * m[15] - m[0] * m[11] * m[14] - m[8] * m[2] * m[15] + m[8] * m[3] * m[14] + m[12] * m[2] * m[11]
- m[12] * m[3] * m[10];
inv[9] =
-m[0] * m[9] * m[15] + m[0] * m[11] * m[13] + m[8] * m[1] * m[15] - m[8] * m[3] * m[13] - m[12] * m[1] * m[11]
+ m[12] * m[3] * m[9];
inv[13] =
m[0] * m[9] * m[14] - m[0] * m[10] * m[13] - m[8] * m[1] * m[14] + m[8] * m[2] * m[13] + m[12] * m[1] * m[10]
- m[12] * m[2] * m[9];
inv[2] =
m[1] * m[6] * m[15] - m[1] * m[7] * m[14] - m[5] * m[2] * m[15] + m[5] * m[3] * m[14] + m[13] * m[2] * m[7]
- m[13] * m[3] * m[6];
inv[6] =
-m[0] * m[6] * m[15] + m[0] * m[7] * m[14] + m[4] * m[2] * m[15] - m[4] * m[3] * m[14] - m[12] * m[2] * m[7]
+ m[12] * m[3] * m[6];
inv[10] =
m[0] * m[5] * m[15] - m[0] * m[7] * m[13] - m[4] * m[1] * m[15] + m[4] * m[3] * m[13] + m[12] * m[1] * m[7]
- m[12] * m[3] * m[5];
inv[14] =
-m[0] * m[5] * m[14] + m[0] * m[6] * m[13] + m[4] * m[1] * m[14] - m[4] * m[2] * m[13] - m[12] * m[1] * m[6]
+ m[12] * m[2] * m[5];
inv[3] =
-m[1] * m[6] * m[11] + m[1] * m[7] * m[10] + m[5] * m[2] * m[11] - m[5] * m[3] * m[10] - m[9] * m[2] * m[7]
+ m[9] * m[3] * m[6];
inv[7] = m[0] * m[6] * m[11] - m[0] * m[7] * m[10] - m[4] * m[2] * m[11] + m[4] * m[3] * m[10] + m[8] * m[2] * m[7]
- m[8] * m[3] * m[6];
inv[11] = -m[0] * m[5] * m[11] + m[0] * m[7] * m[9] + m[4] * m[1] * m[11] - m[4] * m[3] * m[9] - m[8] * m[1] * m[7]
+ m[8] * m[3] * m[5];
inv[15] = m[0] * m[5] * m[10] - m[0] * m[6] * m[9] - m[4] * m[1] * m[10] + m[4] * m[2] * m[9] + m[8] * m[1] * m[6]
- m[8] * m[2] * m[5];
let det = m[0] * inv[0] + m[1] * inv[4] + m[2] * inv[8] + m[3] * inv[12];
if det.abs() < MAT4_SINGULAR_DETERMINANT {
return None;
}
let inv_det = 1.0 / det;
for v in &mut inv {
*v *= inv_det;
}
Some(inv)
}
pub fn mat4_translation<T: Into<f64> + Copy>(t: [T; 3]) -> [f64; 16] {
let mut m = IDENTITY_MAT4;
m[12] = t[0].into();
m[13] = t[1].into();
m[14] = t[2].into();
m
}
pub fn mat4_scale<T: Into<f64> + Copy>(s: [T; 3]) -> [f64; 16] {
let mut m = IDENTITY_MAT4;
m[0] = s[0].into();
m[5] = s[1].into();
m[10] = s[2].into();
m
}
pub fn mat4_rotation_x(rad: f64) -> [f64; 16] {
let (s, c) = rad.sin_cos();
let mut m = IDENTITY_MAT4;
m[5] = c;
m[6] = s;
m[9] = -s;
m[10] = c;
m
}
pub fn mat4_rotation_y(rad: f64) -> [f64; 16] {
let (s, c) = rad.sin_cos();
let mut m = IDENTITY_MAT4;
m[0] = c;
m[2] = -s;
m[8] = s;
m[10] = c;
m
}
pub fn mat4_rotation_z(rad: f64) -> [f64; 16] {
let (s, c) = rad.sin_cos();
let mut m = IDENTITY_MAT4;
m[0] = c;
m[1] = s;
m[4] = -s;
m[5] = c;
m
}
pub fn mat4_from_quat<T: Into<f64> + Copy>(q: [T; 4]) -> [f64; 16] {
let w = q[0].into();
let x = q[1].into();
let y = q[2].into();
let z = q[3].into();
let xx = x * x;
let yy = y * y;
let zz = z * z;
let xy = x * y;
let xz = x * z;
let yz = y * z;
let wx = w * x;
let wy = w * y;
let wz = w * z;
[
1.0 - 2.0 * (yy + zz),
2.0 * (xy + wz),
2.0 * (xz - wy),
0.0,
2.0 * (xy - wz),
1.0 - 2.0 * (xx + zz),
2.0 * (yz + wx),
0.0,
2.0 * (xz + wy),
2.0 * (yz - wx),
1.0 - 2.0 * (xx + yy),
0.0,
0.0,
0.0,
0.0,
1.0,
]
}
#[cfg(test)]
mod tests {
use super::*;
fn translation(x: f64, y: f64, z: f64) -> [f64; 16] {
let mut m = IDENTITY_MAT4;
m[12] = x;
m[13] = y;
m[14] = z;
m
}
#[test]
fn mat4_mul_identity_is_noop() {
let m = translation(1.0, 2.0, 3.0);
assert_eq!(mat4_mul(&IDENTITY_MAT4, &m), m);
assert_eq!(mat4_mul(&m, &IDENTITY_MAT4), m);
}
#[test]
fn point_transform_applies_translation() {
let m = translation(10.0, 20.0, 30.0);
let p = mat4_transform_point(&m, [1.0, 2.0, 3.0]);
assert_eq!(p, [11.0, 22.0, 33.0]);
}
#[test]
fn vec_transform_ignores_translation() {
let m = translation(10.0, 20.0, 30.0);
let v = mat4_transform_vec(&m, [1.0, 2.0, 3.0]);
assert_eq!(v, [1.0, 2.0, 3.0]);
}
#[test]
fn inverse_round_trips() {
let m = translation(5.0, -2.0, 7.0);
let inv = mat4_inverse(&m).unwrap();
let id = mat4_mul(&m, &inv);
for i in 0..16 {
assert!((id[i] - IDENTITY_MAT4[i]).abs() < 1e-10, "row-col {i}: {id:?}");
}
}
#[test]
fn inverse_round_trips_with_rotation() {
let scale = {
let mut m = IDENTITY_MAT4;
m[0] = 2.0;
m[5] = 3.0;
m[10] = 0.5;
m
};
let rot_z = {
let c = 0.8660254037844387_f64;
let s = 0.5_f64;
let mut m = IDENTITY_MAT4;
m[0] = c;
m[1] = s;
m[4] = -s;
m[5] = c;
m
};
let trans = translation(7.0, -3.0, 11.0);
let m = mat4_mul(&mat4_mul(&scale, &rot_z), &trans);
let inv = mat4_inverse(&m).expect("non-singular");
let id1 = mat4_mul(&m, &inv);
let id2 = mat4_mul(&inv, &m);
for i in 0..16 {
assert!((id1[i] - IDENTITY_MAT4[i]).abs() < 1e-10, "M·inv off at {i}: {id1:?}");
assert!((id2[i] - IDENTITY_MAT4[i]).abs() < 1e-10, "inv·M off at {i}: {id2:?}");
}
let p = [4.0_f32, -2.5, 7.5];
let warped = mat4_transform_point(&m, p);
let back = mat4_transform_point(&inv, warped);
for k in 0..3 {
assert!((back[k] - p[k]).abs() < 1e-4, "point round-trip off: {back:?} vs {p:?}");
}
}
#[test]
fn inverse_rejects_near_singular() {
let mut near_singular = [0.0f64; 16];
for i in 0..4 {
near_singular[i * 4 + i] = 1e-4;
}
assert!(mat4_inverse(&near_singular).is_none(), "should reject near-singular");
let singular = [0.0f64; 16];
assert!(mat4_inverse(&singular).is_none(), "should reject zero matrix");
}
#[test]
fn translation_layout_matches_row_major() {
let m = mat4_translation([5.0, -2.0, 7.0]);
assert_eq!(&m[12..15], &[5.0, -2.0, 7.0]);
let p = mat4_transform_point(&m, [1.0, 1.0, 1.0]);
assert_eq!(p, [6.0, -1.0, 8.0]);
}
#[test]
fn scale_layout_matches_row_major() {
let m = mat4_scale([2.0, 3.0, 4.0]);
let p = mat4_transform_point(&m, [1.0, 1.0, 1.0]);
assert_eq!(p, [2.0, 3.0, 4.0]);
}
#[test]
fn rotation_x_takes_y_to_z() {
let m = mat4_rotation_x(std::f64::consts::FRAC_PI_2);
let p = mat4_transform_point(&m, [0.0, 1.0, 0.0]);
assert!(p[0].abs() < 1e-6);
assert!(p[1].abs() < 1e-6);
assert!((p[2] - 1.0).abs() < 1e-6, "z: {}", p[2]);
}
#[test]
fn rotation_y_takes_x_to_neg_z() {
let m = mat4_rotation_y(std::f64::consts::FRAC_PI_2);
let p = mat4_transform_point(&m, [1.0, 0.0, 0.0]);
assert!(p[0].abs() < 1e-6);
assert!(p[1].abs() < 1e-6);
assert!((p[2] + 1.0).abs() < 1e-6, "z: {}", p[2]);
}
#[test]
fn rotation_z_takes_x_to_y() {
let m = mat4_rotation_z(std::f64::consts::FRAC_PI_2);
let p = mat4_transform_point(&m, [1.0, 0.0, 0.0]);
assert!(p[0].abs() < 1e-6, "x: {}", p[0]);
assert!((p[1] - 1.0).abs() < 1e-6, "y: {}", p[1]);
assert!(p[2].abs() < 1e-6);
}
#[test]
fn identity_quat_to_matrix_is_identity() {
assert_eq!(mat4_from_quat([1.0, 0.0, 0.0, 0.0]), IDENTITY_MAT4);
}
#[test]
fn quat_90_about_z_takes_x_to_y() {
let h = std::f32::consts::FRAC_PI_4;
let m = mat4_from_quat([h.cos(), 0.0, 0.0, h.sin()]);
let p = mat4_transform_point(&m, [1.0, 0.0, 0.0]);
assert!(p[0].abs() < 1e-5);
assert!((p[1] - 1.0).abs() < 1e-5);
assert!(p[2].abs() < 1e-5);
}
}