use crate::math::{acos, sin, sin_cos, sqrt};
pub type Mat4 = [[f32; 4]; 4];
pub const IDENTITY: Mat4 = [
[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: Mat4, b: Mat4) -> Mat4 {
let mut out = [[0.0f32; 4]; 4];
for col in 0..4 {
for row in 0..4 {
for k in 0..4 {
out[col][row] += a[k][row] * b[col][k];
}
}
}
out
}
pub fn mat4_affine_inverse(m: Mat4) -> Mat4 {
let a = m;
let det = a[0][0] * (a[1][1] * a[2][2] - a[2][1] * a[1][2])
- a[1][0] * (a[0][1] * a[2][2] - a[2][1] * a[0][2])
+ a[2][0] * (a[0][1] * a[1][2] - a[1][1] * a[0][2]);
if det.abs() < 1e-12 {
return IDENTITY;
}
let inv_det = 1.0 / det;
let mut inv = [[0.0f32; 4]; 4];
inv[0][0] = (a[1][1] * a[2][2] - a[2][1] * a[1][2]) * inv_det;
inv[1][0] = -(a[1][0] * a[2][2] - a[2][0] * a[1][2]) * inv_det;
inv[2][0] = (a[1][0] * a[2][1] - a[2][0] * a[1][1]) * inv_det;
inv[0][1] = -(a[0][1] * a[2][2] - a[2][1] * a[0][2]) * inv_det;
inv[1][1] = (a[0][0] * a[2][2] - a[2][0] * a[0][2]) * inv_det;
inv[2][1] = -(a[0][0] * a[2][1] - a[2][0] * a[0][1]) * inv_det;
inv[0][2] = (a[0][1] * a[1][2] - a[1][1] * a[0][2]) * inv_det;
inv[1][2] = -(a[0][0] * a[1][2] - a[1][0] * a[0][2]) * inv_det;
inv[2][2] = (a[0][0] * a[1][1] - a[1][0] * a[0][1]) * inv_det;
let t = [m[3][0], m[3][1], m[3][2]];
inv[3][0] = -(inv[0][0] * t[0] + inv[1][0] * t[1] + inv[2][0] * t[2]);
inv[3][1] = -(inv[0][1] * t[0] + inv[1][1] * t[1] + inv[2][1] * t[2]);
inv[3][2] = -(inv[0][2] * t[0] + inv[1][2] * t[1] + inv[2][2] * t[2]);
inv[3][3] = 1.0;
inv
}
pub fn mat4_inverse(m: Mat4) -> Mat4 {
let a00 = m[0][0];
let a01 = m[1][0];
let a02 = m[2][0];
let a03 = m[3][0];
let a10 = m[0][1];
let a11 = m[1][1];
let a12 = m[2][1];
let a13 = m[3][1];
let a20 = m[0][2];
let a21 = m[1][2];
let a22 = m[2][2];
let a23 = m[3][2];
let a30 = m[0][3];
let a31 = m[1][3];
let a32 = m[2][3];
let a33 = m[3][3];
let b00 = a00 * a11 - a01 * a10;
let b01 = a00 * a12 - a02 * a10;
let b02 = a00 * a13 - a03 * a10;
let b03 = a01 * a12 - a02 * a11;
let b04 = a01 * a13 - a03 * a11;
let b05 = a02 * a13 - a03 * a12;
let b06 = a20 * a31 - a21 * a30;
let b07 = a20 * a32 - a22 * a30;
let b08 = a20 * a33 - a23 * a30;
let b09 = a21 * a32 - a22 * a31;
let b10 = a21 * a33 - a23 * a31;
let b11 = a22 * a33 - a23 * a32;
let det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
if det.abs() < 1e-20 || !det.is_finite() {
return IDENTITY;
}
let inv_det = 1.0 / det;
let i00 = (a11 * b11 - a12 * b10 + a13 * b09) * inv_det;
let i01 = (-a01 * b11 + a02 * b10 - a03 * b09) * inv_det;
let i02 = (a31 * b05 - a32 * b04 + a33 * b03) * inv_det;
let i03 = (-a21 * b05 + a22 * b04 - a23 * b03) * inv_det;
let i10 = (-a10 * b11 + a12 * b08 - a13 * b07) * inv_det;
let i11 = (a00 * b11 - a02 * b08 + a03 * b07) * inv_det;
let i12 = (-a30 * b05 + a32 * b02 - a33 * b01) * inv_det;
let i13 = (a20 * b05 - a22 * b02 + a23 * b01) * inv_det;
let i20 = (a10 * b10 - a11 * b08 + a13 * b06) * inv_det;
let i21 = (-a00 * b10 + a01 * b08 - a03 * b06) * inv_det;
let i22 = (a30 * b04 - a31 * b02 + a33 * b00) * inv_det;
let i23 = (-a20 * b04 + a21 * b02 - a23 * b00) * inv_det;
let i30 = (-a10 * b09 + a11 * b07 - a12 * b06) * inv_det;
let i31 = (a00 * b09 - a01 * b07 + a02 * b06) * inv_det;
let i32 = (-a30 * b03 + a31 * b01 - a32 * b00) * inv_det;
let i33 = (a20 * b03 - a21 * b01 + a22 * b00) * inv_det;
[
[i00, i10, i20, i30],
[i01, i11, i21, i31],
[i02, i12, i22, i32],
[i03, i13, i23, i33],
]
}
pub type Mat3 = [[f32; 3]; 3];
pub(crate) use crate::math::Quat;
pub(crate) fn rotation_mat3(rotation_deg: [f32; 3]) -> Mat3 {
let [pitch, yaw, roll] = rotation_deg;
let (sp, cp) = sin_cos(pitch.to_radians());
let (syw, cyw) = sin_cos(yaw.to_radians());
let (sr, cr) = sin_cos(roll.to_radians());
[
[cyw * cr + syw * sp * sr, cp * sr, -syw * cr + cyw * sp * sr],
[-cyw * sr + syw * sp * cr, cp * cr, syw * sr + cyw * sp * cr],
[syw * cp, -sp, cyw * cp],
]
}
pub fn compose(r: Mat3, scale: [f32; 3], t: [f32; 3]) -> Mat4 {
let [sx, sy, sz] = scale;
[
[r[0][0] * sx, r[0][1] * sx, r[0][2] * sx, 0.0],
[r[1][0] * sy, r[1][1] * sy, r[1][2] * sy, 0.0],
[r[2][0] * sz, r[2][1] * sz, r[2][2] * sz, 0.0],
[t[0], t[1], t[2], 1.0],
]
}
pub fn trs_matrix(position: [f32; 3], rotation_deg: [f32; 3], scale: [f32; 3]) -> Mat4 {
compose(rotation_mat3(rotation_deg), scale, position)
}
pub(crate) fn quat_from_mat3(m: Mat3) -> Quat {
let (m00, m11, m22) = (m[0][0], m[1][1], m[2][2]);
let trace = m00 + m11 + m22;
if trace > 0.0 {
let s = sqrt(trace + 1.0) * 2.0;
[
(m[1][2] - m[2][1]) / s,
(m[2][0] - m[0][2]) / s,
(m[0][1] - m[1][0]) / s,
0.25 * s,
]
} else if m00 > m11 && m00 > m22 {
let s = sqrt(1.0 + m00 - m11 - m22) * 2.0;
[
0.25 * s,
(m[1][0] + m[0][1]) / s,
(m[2][0] + m[0][2]) / s,
(m[1][2] - m[2][1]) / s,
]
} else if m11 > m22 {
let s = sqrt(1.0 + m11 - m00 - m22) * 2.0;
[
(m[1][0] + m[0][1]) / s,
0.25 * s,
(m[2][1] + m[1][2]) / s,
(m[2][0] - m[0][2]) / s,
]
} else {
let s = sqrt(1.0 + m22 - m00 - m11) * 2.0;
[
(m[2][0] + m[0][2]) / s,
(m[2][1] + m[1][2]) / s,
0.25 * s,
(m[0][1] - m[1][0]) / s,
]
}
}
pub(crate) fn quat_to_mat3(q: Quat) -> Mat3 {
let [x, y, z, w] = q;
[
[
1.0 - 2.0 * (y * y + z * z),
2.0 * (x * y + w * z),
2.0 * (x * z - w * y),
],
[
2.0 * (x * y - w * z),
1.0 - 2.0 * (x * x + z * z),
2.0 * (y * z + w * x),
],
[
2.0 * (x * z + w * y),
2.0 * (y * z - w * x),
1.0 - 2.0 * (x * x + y * y),
],
]
}
pub(crate) fn quat_slerp(a: Quat, mut b: Quat, f: f32) -> Quat {
let f = f.clamp(0.0, 1.0);
let mut dot = a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
if dot < 0.0 {
b = [-b[0], -b[1], -b[2], -b[3]];
dot = -dot;
}
if dot > 0.9995 {
return crate::math::quat_normalize([
a[0] + (b[0] - a[0]) * f,
a[1] + (b[1] - a[1]) * f,
a[2] + (b[2] - a[2]) * f,
a[3] + (b[3] - a[3]) * f,
]);
}
let theta_0 = acos(dot.clamp(-1.0, 1.0));
let sin_0 = sin(theta_0);
let s_a = sin((1.0 - f) * theta_0) / sin_0;
let s_b = sin(f * theta_0) / sin_0;
[
a[0] * s_a + b[0] * s_b,
a[1] * s_a + b[1] * s_b,
a[2] * s_a + b[2] * s_b,
a[3] * s_a + b[3] * s_b,
]
}
pub fn decompose(m: Mat4) -> ([f32; 3], Quat, [f32; 3]) {
let t = [m[3][0], m[3][1], m[3][2]];
let col_len = |c: usize| sqrt(m[c][0] * m[c][0] + m[c][1] * m[c][1] + m[c][2] * m[c][2]);
let scale = [col_len(0), col_len(1), col_len(2)];
let norm = |c: usize| {
let s = scale[c];
if s < 1e-12 {
[0.0, 0.0, 0.0]
} else {
[m[c][0] / s, m[c][1] / s, m[c][2] / s]
}
};
let r: Mat3 = [norm(0), norm(1), norm(2)];
(t, crate::math::quat_normalize(quat_from_mat3(r)), scale)
}
pub fn blend_matrices(a: Mat4, b: Mat4, f: f32) -> Mat4 {
let f = f.clamp(0.0, 1.0);
let (ta, qa, sa) = decompose(a);
let (tb, qb, sb) = decompose(b);
let mix = |x: [f32; 3], y: [f32; 3]| {
[
x[0] + (y[0] - x[0]) * f,
x[1] + (y[1] - x[1]) * f,
x[2] + (y[2] - x[2]) * f,
]
};
compose(
quat_to_mat3(quat_slerp(qa, qb, f)),
mix(sa, sb),
mix(ta, tb),
)
}
pub fn euler_yxz_from_quat(q: Quat) -> [f32; 3] {
crate::math::euler_yxz_deg_from_quat(q)
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f32, b: f32) -> bool {
(a - b).abs() < 1e-4
}
#[test]
fn affine_inverse_round_trips() {
let m = trs_matrix([3.0, -2.0, 5.0], [0.0, 30.0, 0.0], [2.0, 2.0, 2.0]);
let id = mat4_mul(m, mat4_affine_inverse(m));
for col in 0..4 {
for row in 0..4 {
assert!(approx(id[col][row], IDENTITY[col][row]));
}
}
}
#[test]
fn affine_inverse_of_a_degenerate_matrix_falls_back_to_identity() {
let m = trs_matrix([1.0, 2.0, 3.0], [10.0, 20.0, 30.0], [1.0, 0.0, 1.0]);
assert_eq!(mat4_affine_inverse(m), IDENTITY);
}
#[test]
fn general_inverse_round_trips_a_projection_and_a_view() {
let proj = crate::gfx::projection::perspective_rh(75.0f32.to_radians(), 1.6, 0.1, 500.0);
let view: Mat4 = [
[0.92388, 0.0, -0.38268, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.38268, 0.0, 0.92388, 0.0],
[-1.5, -0.7, 4.0, 1.0],
];
for m in [proj, view, mat4_mul(proj, view)] {
for id in [mat4_mul(m, mat4_inverse(m)), mat4_mul(mat4_inverse(m), m)] {
for col in 0..4 {
for row in 0..4 {
assert!(
(id[col][row] - IDENTITY[col][row]).abs() < 1e-3,
"[{col}][{row}]: {}",
id[col][row]
);
}
}
}
}
}
#[test]
fn general_inverse_falls_back_on_a_singular_or_non_finite_matrix() {
let mut singular = IDENTITY;
singular[1] = [0.0; 4];
assert_eq!(mat4_inverse(singular), IDENTITY);
let mut nan = IDENTITY;
nan[0][0] = f32::NAN;
assert_eq!(mat4_inverse(nan), IDENTITY);
let mut inf = IDENTITY;
inf[2][2] = f32::INFINITY;
assert_eq!(mat4_inverse(inf), IDENTITY);
}
#[test]
fn quat_mat3_round_trips() {
for e in [
[0.0, 0.0, 0.0],
[30.0, 50.0, 20.0],
[-80.0, 140.0, -25.0],
[90.0, 0.0, 0.0],
[0.0, 180.0, 0.0],
] {
let r = rotation_mat3(e);
let r2 = quat_to_mat3(quat_from_mat3(r));
for c in 0..3 {
for row in 0..3 {
assert!(
approx(r[c][row], r2[c][row]),
"e={:?} [{}][{}]: {} vs {}",
e,
c,
row,
r[c][row],
r2[c][row]
);
}
}
}
}
#[test]
fn quat_normalize_falls_back_for_a_zero_quaternion() {
assert_eq!(crate::math::quat_normalize([0.0; 4]), [0.0, 0.0, 0.0, 1.0]);
let n = crate::math::quat_normalize([0.0, 0.0, 0.0, 4.0]);
assert!(approx(n[3], 1.0));
}
#[test]
fn slerp_midpoint_splits_the_arc_equally() {
let qa = quat_from_mat3(rotation_mat3([10.0, 20.0, 30.0]));
let qb = quat_from_mat3(rotation_mat3([70.0, -40.0, 80.0]));
let qm = quat_slerp(qa, qb, 0.5);
let angle = |x: Quat, y: Quat| {
let d = (x[0] * y[0] + x[1] * y[1] + x[2] * y[2] + x[3] * y[3])
.abs()
.min(1.0);
2.0 * acos(d)
};
assert!(
approx(angle(qa, qm), angle(qm, qb)),
"arcs {} vs {}",
angle(qa, qm),
angle(qm, qb)
);
}
#[test]
fn decompose_round_trips_a_composed_matrix() {
let m = trs_matrix([3.0, -2.0, 5.0], [25.0, -60.0, 40.0], [1.5, 0.5, 2.0]);
let (t, q, s) = decompose(m);
let rebuilt = compose(quat_to_mat3(q), s, t);
for c in 0..4 {
for row in 0..4 {
assert!(
approx(rebuilt[c][row], m[c][row]),
"[{}][{}]: {} vs {}",
c,
row,
rebuilt[c][row],
m[c][row]
);
}
}
}
#[test]
fn blend_matrices_endpoints_are_exact_and_the_middle_slerps() {
let a = trs_matrix([1.0, 2.0, 3.0], [10.0, 20.0, 30.0], [1.0, 1.5, 2.0]);
let b = trs_matrix([-4.0, 0.0, 5.0], [70.0, -40.0, 15.0], [2.0, 1.0, 0.5]);
for (f, want) in [(0.0, a), (1.0, b)] {
let got = blend_matrices(a, b, f);
for c in 0..4 {
for row in 0..4 {
assert!(approx(got[c][row], want[c][row]), "f={f} [{c}][{row}]");
}
}
}
let mid = blend_matrices(a, b, 0.5);
let (_, _, scale) = decompose(mid);
assert!(approx(scale[0], 1.5) && approx(scale[1], 1.25) && approx(scale[2], 1.25));
assert!(approx(mid[3][0], -1.5) && approx(mid[3][1], 1.0) && approx(mid[3][2], 4.0));
assert_eq!(blend_matrices(a, b, -1.0), blend_matrices(a, b, 0.0));
assert_eq!(blend_matrices(a, b, 2.0), blend_matrices(a, b, 1.0));
}
#[test]
fn euler_from_quat_round_trips_through_the_rotation_matrix() {
for e in [
[0.0, 0.0, 0.0],
[25.0, -60.0, 40.0],
[-80.0, 140.0, -25.0],
[10.0, 200.0, -170.0],
] {
let r = rotation_mat3(e);
let q = quat_from_mat3(r);
let e2 = euler_yxz_from_quat(q);
let r2 = rotation_mat3(e2);
for c in 0..3 {
for row in 0..3 {
assert!(
approx(r[c][row], r2[c][row]),
"e={:?} [{}][{}]: {} vs {}",
e,
c,
row,
r[c][row],
r2[c][row]
);
}
}
}
}
#[test]
fn euler_from_quat_handles_gimbal_lock() {
for e in [[90.0, 35.0, 0.0], [-90.0, -110.0, 0.0]] {
let r = rotation_mat3(e);
let e2 = euler_yxz_from_quat(quat_from_mat3(r));
assert!(e2.iter().all(|v| v.is_finite()), "non-finite for {:?}", e);
let r2 = rotation_mat3(e2);
for c in 0..3 {
for row in 0..3 {
assert!(approx(r[c][row], r2[c][row]), "e={:?} [{}][{}]", e, c, row);
}
}
}
}
#[test]
fn decomposing_a_collapsed_axis_yields_a_zero_scale() {
let mut m = IDENTITY;
m[1] = [0.0, 0.0, 0.0, 0.0];
m[3] = [1.0, 2.0, 3.0, 1.0];
let (translation, rotation, scale) = decompose(m);
assert_eq!(translation, [1.0, 2.0, 3.0]);
assert_eq!(scale[1], 0.0, "the collapsed axis has no length");
assert_eq!((scale[0], scale[2]), (1.0, 1.0), "the others are intact");
assert!(
rotation.iter().all(|v| v.is_finite()),
"{rotation:?} is not a usable rotation"
);
}
}