#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Vec3 {
pub x: f32,
pub y: f32,
pub z: f32,
}
impl Vec3 {
pub const ZERO: Vec3 = Vec3 {
x: 0.0,
y: 0.0,
z: 0.0,
};
pub const fn new(x: f32, y: f32, z: f32) -> Self {
Vec3 { x, y, z }
}
pub const fn from_array(a: [f32; 3]) -> Self {
Vec3 {
x: a[0],
y: a[1],
z: a[2],
}
}
pub const fn to_array(self) -> [f32; 3] {
[self.x, self.y, self.z]
}
pub fn dot(self, o: Vec3) -> f32 {
self.x * o.x + self.y * o.y + self.z * o.z
}
pub fn cross(self, o: Vec3) -> Vec3 {
Vec3::new(
self.y * o.z - self.z * o.y,
self.z * o.x - self.x * o.z,
self.x * o.y - self.y * o.x,
)
}
pub fn length(self) -> f32 {
self.dot(self).sqrt()
}
pub fn normalized(self) -> Vec3 {
let n = self.length();
if n == 0.0 { self } else { self * (1.0 / n) }
}
}
impl std::ops::Add for Vec3 {
type Output = Vec3;
fn add(self, o: Vec3) -> Vec3 {
Vec3::new(self.x + o.x, self.y + o.y, self.z + o.z)
}
}
impl std::ops::Sub for Vec3 {
type Output = Vec3;
fn sub(self, o: Vec3) -> Vec3 {
Vec3::new(self.x - o.x, self.y - o.y, self.z - o.z)
}
}
impl std::ops::Neg for Vec3 {
type Output = Vec3;
fn neg(self) -> Vec3 {
Vec3::new(-self.x, -self.y, -self.z)
}
}
impl std::ops::Mul<f32> for Vec3 {
type Output = Vec3;
fn mul(self, s: f32) -> Vec3 {
Vec3::new(self.x * s, self.y * s, self.z * s)
}
}
impl std::ops::AddAssign for Vec3 {
fn add_assign(&mut self, o: Vec3) {
self.x += o.x;
self.y += o.y;
self.z += o.z;
}
}
impl std::ops::SubAssign for Vec3 {
fn sub_assign(&mut self, o: Vec3) {
self.x -= o.x;
self.y -= o.y;
self.z -= o.z;
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Mat4 {
pub rows: [[f32; 4]; 4],
}
impl Mat4 {
pub const IDENTITY: Mat4 = Mat4 {
rows: [
[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 const fn from_rows(rows: [[f32; 4]; 4]) -> Self {
Mat4 { rows }
}
pub fn transform_point(&self, p: Vec3, perspective_divide: bool) -> Vec3 {
let v = [p.x, p.y, p.z, 1.0];
let mut r = [0.0f32; 4];
for (i, ri) in r.iter_mut().enumerate() {
*ri = (0..4).map(|j| self.rows[i][j] * v[j]).sum();
}
if perspective_divide && r[3] != 0.0 {
Vec3::new(r[0] / r[3], r[1] / r[3], r[2] / r[3])
} else {
Vec3::new(r[0], r[1], r[2])
}
}
pub fn transform_dir(&self, d: Vec3) -> Vec3 {
let v = [d.x, d.y, d.z];
let mut r = [0.0f32; 3];
for (i, ri) in r.iter_mut().enumerate() {
*ri = (0..3).map(|j| self.rows[i][j] * v[j]).sum();
}
Vec3::new(r[0], r[1], r[2])
}
pub fn to_gpu_cols(&self) -> [[f32; 4]; 4] {
let mut cols = [[0.0f32; 4]; 4];
for (c, col) in cols.iter_mut().enumerate() {
for (r, cell) in col.iter_mut().enumerate() {
*cell = self.rows[r][c];
}
}
cols
}
pub fn to_gpu_clip_cols(&self) -> [[f32; 4]; 4] {
let mut corrected = *self;
for c in 0..4 {
corrected.rows[2][c] = 0.5 * self.rows[2][c] + 0.5 * self.rows[3][c];
}
corrected.to_gpu_cols()
}
pub fn inverse(&self) -> Option<Mat4> {
let mut a = [[0.0f32; 8]; 4];
for (r, row) in a.iter_mut().enumerate() {
row[..4].copy_from_slice(&self.rows[r]);
row[4 + r] = 1.0;
}
for col in 0..4 {
let mut pivot = col;
for r in (col + 1)..4 {
if a[r][col].abs() > a[pivot][col].abs() {
pivot = r;
}
}
if a[pivot][col] == 0.0 {
return None; }
a.swap(col, pivot);
let inv_pivot = 1.0 / a[col][col];
for v in a[col].iter_mut() {
*v *= inv_pivot;
}
let pivot_row = a[col];
for (r, row) in a.iter_mut().enumerate() {
if r != col {
let factor = row[col];
if factor != 0.0 {
for (v, &p) in row.iter_mut().zip(pivot_row.iter()) {
*v -= factor * p;
}
}
}
}
}
let mut rows = [[0.0f32; 4]; 4];
for (r, row) in rows.iter_mut().enumerate() {
row.copy_from_slice(&a[r][4..8]);
}
Some(Mat4 { rows })
}
}
impl std::ops::Mul for Mat4 {
type Output = Mat4;
fn mul(self, other: Mat4) -> Mat4 {
let mut out = [[0.0f32; 4]; 4];
for (i, out_row) in out.iter_mut().enumerate() {
for (j, out_cell) in out_row.iter_mut().enumerate() {
let mut acc = 0.0;
for k in 0..4 {
acc += self.rows[i][k] * other.rows[k][j];
}
*out_cell = acc;
}
}
Mat4 { rows: out }
}
}
pub fn mat4_translate(tx: f32, ty: f32, tz: f32) -> Mat4 {
Mat4::from_rows([
[1.0, 0.0, 0.0, tx],
[0.0, 1.0, 0.0, ty],
[0.0, 0.0, 1.0, tz],
[0.0, 0.0, 0.0, 1.0],
])
}
pub fn mat4_scale(sx: f32, sy: f32, sz: f32) -> Mat4 {
Mat4::from_rows([
[sx, 0.0, 0.0, 0.0],
[0.0, sy, 0.0, 0.0],
[0.0, 0.0, sz, 0.0],
[0.0, 0.0, 0.0, 1.0],
])
}
pub fn mat4_rotate(angle: f32, x: f32, y: f32, z: f32) -> Mat4 {
let ca = angle.cos();
let sa = angle.sin();
let omca = 1.0 - ca;
Mat4::from_rows([
[
omca * x * x + ca,
omca * x * y - sa * z,
omca * x * z + sa * y,
0.0,
],
[
omca * x * y + sa * z,
omca * y * y + ca,
omca * y * z - sa * x,
0.0,
],
[
omca * x * z - sa * y,
omca * y * z + sa * x,
omca * z * z + ca,
0.0,
],
[0.0, 0.0, 0.0, 1.0],
])
}
pub fn mat4_look_at_dir(position: Vec3, direction: Vec3, up: Vec3) -> Mat4 {
let dirnorm = direction.length();
debug_assert!(dirnorm != 0.0, "look-at direction must be non-zero");
let direction = direction * (1.0 / dirnorm);
let side = direction.cross(up);
let sidenorm = side.length();
debug_assert!(sidenorm != 0.0, "look-at direction and up are parallel");
let up = (side * (1.0 / sidenorm)).cross(direction).normalized();
let rot = Mat4::from_rows([
[side.x, side.y, side.z, 0.0],
[up.x, up.y, up.z, 0.0],
[-direction.x, -direction.y, -direction.z, 0.0],
[0.0, 0.0, 0.0, 1.0],
]);
rot * mat4_translate(-position.x, -position.y, -position.z)
}
pub fn mat4_perspective(fovy: f32, width: f32, height: f32, near: f32, far: f32) -> Mat4 {
debug_assert!(fovy != 0.0 && width != 0.0 && height != 0.0);
debug_assert!(near > 0.0 && far > near);
let aspect = width / height;
let f = 1.0 / (fovy.to_radians() / 2.0).tan();
Mat4::from_rows([
[f / aspect, 0.0, 0.0, 0.0],
[0.0, f, 0.0, 0.0],
[
0.0,
0.0,
(far + near) / (near - far),
2.0 * far * near / (near - far),
],
[0.0, 0.0, -1.0, 0.0],
])
}
pub fn mat4_orthographic(
left: f32,
right: f32,
bottom: f32,
top: f32,
near: f32,
far: f32,
) -> Mat4 {
Mat4::from_rows([
[
2.0 / (right - left),
0.0,
0.0,
-(right + left) / (right - left),
],
[
0.0,
2.0 / (top - bottom),
0.0,
-(top + bottom) / (top - bottom),
],
[0.0, 0.0, -2.0 / (far - near), -(far + near) / (far - near)],
[0.0, 0.0, 0.0, 1.0],
])
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f32, b: f32) {
assert!((a - b).abs() < 1e-5, "{a} != {b}");
}
fn approx_vec(a: Vec3, b: Vec3) {
approx(a.x, b.x);
approx(a.y, b.y);
approx(a.z, b.z);
}
#[test]
fn cross_is_right_handed() {
approx_vec(
Vec3::new(1.0, 0.0, 0.0).cross(Vec3::new(0.0, 1.0, 0.0)),
Vec3::new(0.0, 0.0, 1.0),
);
}
#[test]
fn identity_is_multiplicative_unit() {
let m = mat4_translate(3.0, -2.0, 5.0);
assert_eq!(Mat4::IDENTITY * m, m);
assert_eq!(m * Mat4::IDENTITY, m);
}
#[test]
fn translate_moves_point() {
let m = mat4_translate(1.0, 2.0, 3.0);
approx_vec(
m.transform_point(Vec3::new(10.0, 20.0, 30.0), false),
Vec3::new(11.0, 22.0, 33.0),
);
}
#[test]
fn scale_scales_point() {
let m = mat4_scale(2.0, 3.0, 4.0);
approx_vec(
m.transform_point(Vec3::new(1.0, 1.0, 1.0), false),
Vec3::new(2.0, 3.0, 4.0),
);
}
#[test]
fn rotate_90_about_z_maps_x_to_y() {
let m = mat4_rotate(std::f32::consts::FRAC_PI_2, 0.0, 0.0, 1.0);
approx_vec(
m.transform_point(Vec3::new(1.0, 0.0, 0.0), false),
Vec3::new(0.0, 1.0, 0.0),
);
}
#[test]
fn rotate_direction_ignores_translation_column() {
let m = mat4_rotate(std::f32::consts::FRAC_PI_2, 0.0, 1.0, 0.0);
approx_vec(
m.transform_dir(Vec3::new(0.0, 0.0, 1.0)),
Vec3::new(1.0, 0.0, 0.0),
);
}
#[test]
fn look_at_dir_puts_camera_at_origin_of_view_space() {
let view = mat4_look_at_dir(
Vec3::new(0.0, 0.0, 5.0),
Vec3::new(0.0, 0.0, -1.0),
Vec3::new(0.0, 1.0, 0.0),
);
approx_vec(
view.transform_point(Vec3::ZERO, false),
Vec3::new(0.0, 0.0, -5.0),
);
approx_vec(
view.transform_point(Vec3::new(0.0, 0.0, 5.0), false),
Vec3::ZERO,
);
}
#[test]
fn orthographic_maps_box_to_ndc_cube() {
let m = mat4_orthographic(-2.0, 2.0, -2.0, 2.0, -2.0, 2.0);
approx_vec(
m.transform_point(Vec3::new(2.0, 2.0, 0.0), false),
Vec3::new(1.0, 1.0, 0.0),
);
approx_vec(
m.transform_point(Vec3::new(-2.0, -2.0, 0.0), false),
Vec3::new(-1.0, -1.0, 0.0),
);
}
#[test]
fn perspective_matches_silx_values() {
let m = mat4_perspective(30.0, 1.0, 1.0, 0.1, 10.0);
let f = 1.0 / (30.0f32.to_radians() / 2.0).tan();
approx(m.rows[0][0], f);
approx(m.rows[1][1], f);
approx(m.rows[2][2], (10.0 + 0.1) / (0.1 - 10.0));
approx(m.rows[2][3], 2.0 * 10.0 * 0.1 / (0.1 - 10.0));
approx(m.rows[3][2], -1.0);
approx(m.rows[3][3], 0.0);
}
#[test]
fn gpu_clip_correction_maps_minus_one_to_zero() {
let m = mat4_orthographic(-1.0, 1.0, -1.0, 1.0, 0.0, 10.0);
let near_ndc_z = m.transform_point(Vec3::new(0.0, 0.0, 0.0), false).z; approx(near_ndc_z, -1.0);
let cols = m.to_gpu_clip_cols();
approx(cols[3][2], 0.0);
let z_far = cols[2][2] * (-10.0) + cols[3][2];
approx(z_far, 1.0);
}
#[test]
fn inverse_round_trips_to_identity() {
fn approx_mat(a: Mat4, b: Mat4) {
for r in 0..4 {
for c in 0..4 {
approx(a.rows[r][c], b.rows[r][c]);
}
}
}
let affine = mat4_translate(2.0, -3.0, 1.0) * mat4_rotate(0.7, 0.0, 1.0, 0.0);
let inv = affine.inverse().expect("affine is invertible");
approx_mat(affine * inv, Mat4::IDENTITY);
approx_mat(inv * affine, Mat4::IDENTITY);
let persp = mat4_perspective(30.0, 4.0, 3.0, 0.1, 100.0);
let pinv = persp.inverse().expect("perspective is invertible");
approx_mat(persp * pinv, Mat4::IDENTITY);
let p = Vec3::new(0.4, -0.2, -5.0);
let clip = persp.transform_point(p, true);
approx_vec(pinv.transform_point(clip, true), p);
}
#[test]
fn inverse_of_singular_is_none() {
let singular = Mat4::from_rows([
[1.0, 2.0, 3.0, 4.0],
[0.0, 0.0, 0.0, 0.0],
[5.0, 6.0, 7.0, 8.0],
[0.0, 0.0, 0.0, 1.0],
]);
assert!(singular.inverse().is_none());
}
#[test]
fn to_gpu_cols_is_transpose() {
let m = mat4_translate(1.0, 2.0, 3.0);
let cols = m.to_gpu_cols();
approx(cols[3][0], 1.0);
approx(cols[3][1], 2.0);
approx(cols[3][2], 3.0);
}
}