use std::ops::Mul;
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct Vertex3D {
pub x: f32,
pub y: f32,
pub z: f32,
}
impl Vertex3D {
pub const fn new(x: f32, y: f32, z: f32) -> Self {
Self { x, y, z }
}
pub fn normalize(&mut self) {
let len = (self.x * self.x + self.y * self.y + self.z * self.z).sqrt();
if len > 0.0 {
let inv_len = 1.0 / len;
self.x *= inv_len;
self.y *= inv_len;
self.z *= inv_len;
}
}
pub fn normalized(mut self) -> Self {
self.normalize();
self
}
pub fn length(&self) -> f32 {
(self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Matrix3D {
pub m: [[f32; 4]; 4],
}
impl Default for Matrix3D {
fn default() -> Self {
Self::identity()
}
}
impl Matrix3D {
#[allow(clippy::too_many_arguments)]
pub const fn new(
m11: f32,
m12: f32,
m13: f32,
m14: f32,
m21: f32,
m22: f32,
m23: f32,
m24: f32,
m31: f32,
m32: f32,
m33: f32,
m34: f32,
m41: f32,
m42: f32,
m43: f32,
m44: f32,
) -> Self {
Self {
m: [
[m11, m12, m13, m14],
[m21, m22, m23, m24],
[m31, m32, m33, m34],
[m41, m42, m43, m44],
],
}
}
pub const fn identity() -> Self {
Self::new(
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 rotate_x(ang_rad: f32) -> Self {
let (sin, cos) = ang_rad.sin_cos();
Self::new(
1.0, 0.0, 0.0, 0.0, 0.0, cos, sin, 0.0, 0.0, -sin, cos, 0.0, 0.0, 0.0, 0.0, 1.0,
)
}
pub fn rotate_y(ang_rad: f32) -> Self {
let (sin, cos) = ang_rad.sin_cos();
Self::new(
cos, 0.0, -sin, 0.0, 0.0, 1.0, 0.0, 0.0, sin, 0.0, cos, 0.0, 0.0, 0.0, 0.0, 1.0,
)
}
pub fn rotate_z(ang_rad: f32) -> Self {
let (sin, cos) = ang_rad.sin_cos();
Self::new(
cos, sin, 0.0, 0.0, -sin, cos, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
)
}
pub const fn scale_uniform(scale: f32) -> Self {
Self::scale(scale, scale, scale)
}
pub const fn scale(sx: f32, sy: f32, sz: f32) -> Self {
Self::new(
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 const fn translate(x: f32, y: f32, z: f32) -> Self {
Self::new(
1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, x, y, z, 1.0,
)
}
pub fn transform_vertex(&self, v: Vertex3D) -> Vertex3D {
let xp = self.m[0][0] * v.x + self.m[1][0] * v.y + self.m[2][0] * v.z + self.m[3][0];
let yp = self.m[0][1] * v.x + self.m[1][1] * v.y + self.m[2][1] * v.z + self.m[3][1];
let zp = self.m[0][2] * v.x + self.m[1][2] * v.y + self.m[2][2] * v.z + self.m[3][2];
let wp = self.m[0][3] * v.x + self.m[1][3] * v.y + self.m[2][3] * v.z + self.m[3][3];
let inv_wp = 1.0 / wp;
Vertex3D::new(xp * inv_wp, yp * inv_wp, zp * inv_wp)
}
pub fn transform_vector(&self, v: Vertex3D) -> Vertex3D {
let xp = self.m[0][0] * v.x + self.m[1][0] * v.y + self.m[2][0] * v.z;
let yp = self.m[0][1] * v.x + self.m[1][1] * v.y + self.m[2][1] * v.z;
let zp = self.m[0][2] * v.x + self.m[1][2] * v.y + self.m[2][2] * v.z;
Vertex3D::new(xp, yp, zp)
}
pub fn transform_normal(&self, nx: f32, ny: f32, nz: f32) -> Vertex3D {
let xp = self.m[0][0] * nx + self.m[1][0] * ny + self.m[2][0] * nz;
let yp = self.m[0][1] * nx + self.m[1][1] * ny + self.m[2][1] * nz;
let zp = self.m[0][2] * nx + self.m[1][2] * ny + self.m[2][2] * nz;
Vertex3D::new(xp, yp, zp)
}
}
impl Mul for Matrix3D {
type Output = Matrix3D;
fn mul(self, mult: Matrix3D) -> Matrix3D {
let mut result = [[0.0f32; 4]; 4];
for (i, row) in result.iter_mut().enumerate() {
for (l, cell) in row.iter_mut().enumerate() {
*cell = mult.m[0][l] * self.m[i][0]
+ mult.m[1][l] * self.m[i][1]
+ mult.m[2][l] * self.m[i][2]
+ mult.m[3][l] * self.m[i][3];
}
}
Matrix3D { m: result }
}
}
impl Mul for &Matrix3D {
type Output = Matrix3D;
fn mul(self, mult: &Matrix3D) -> Matrix3D {
let mut result = [[0.0f32; 4]; 4];
for (i, row) in result.iter_mut().enumerate() {
for (l, cell) in row.iter_mut().enumerate() {
*cell = mult.m[0][l] * self.m[i][0]
+ mult.m[1][l] * self.m[i][1]
+ mult.m[2][l] * self.m[i][2]
+ mult.m[3][l] * self.m[i][3];
}
}
Matrix3D { m: result }
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPSILON: f32 = 1e-5;
fn approx_eq(a: f32, b: f32) -> bool {
(a - b).abs() < EPSILON
}
#[test]
fn test_identity() {
let m = Matrix3D::identity();
let v = Vertex3D::new(1.0, 2.0, 3.0);
let result = m.transform_vertex(v);
assert!(approx_eq(result.x, 1.0));
assert!(approx_eq(result.y, 2.0));
assert!(approx_eq(result.z, 3.0));
}
#[test]
fn test_translation() {
let m = Matrix3D::translate(10.0, 20.0, 30.0);
let v = Vertex3D::new(1.0, 2.0, 3.0);
let result = m.transform_vertex(v);
assert!(approx_eq(result.x, 11.0));
assert!(approx_eq(result.y, 22.0));
assert!(approx_eq(result.z, 33.0));
}
#[test]
fn test_scale() {
let m = Matrix3D::scale(2.0, 3.0, 4.0);
let v = Vertex3D::new(1.0, 2.0, 3.0);
let result = m.transform_vertex(v);
assert!(approx_eq(result.x, 2.0));
assert!(approx_eq(result.y, 6.0));
assert!(approx_eq(result.z, 12.0));
}
#[test]
fn test_rotate_z_90() {
let m = Matrix3D::rotate_z(std::f32::consts::FRAC_PI_2);
let v = Vertex3D::new(1.0, 0.0, 0.0);
let result = m.transform_vertex(v);
assert!(approx_eq(result.x, 0.0), "x: expected 0, got {}", result.x);
assert!(approx_eq(result.y, 1.0), "y: expected 1, got {}", result.y);
assert!(approx_eq(result.z, 0.0), "z: expected 0, got {}", result.z);
}
#[test]
fn test_rotate_x_90() {
let m = Matrix3D::rotate_x(std::f32::consts::FRAC_PI_2);
let v = Vertex3D::new(0.0, 1.0, 0.0);
let result = m.transform_vertex(v);
assert!(approx_eq(result.x, 0.0), "x: expected 0, got {}", result.x);
assert!(approx_eq(result.y, 0.0), "y: expected 0, got {}", result.y);
assert!(approx_eq(result.z, 1.0), "z: expected 1, got {}", result.z);
}
#[test]
fn test_rotate_y_90() {
let m = Matrix3D::rotate_y(std::f32::consts::FRAC_PI_2);
let v = Vertex3D::new(1.0, 0.0, 0.0);
let result = m.transform_vertex(v);
assert!(approx_eq(result.x, 0.0), "x: expected 0, got {}", result.x);
assert!(approx_eq(result.y, 0.0), "y: expected 0, got {}", result.y);
assert!(
approx_eq(result.z, -1.0),
"z: expected -1, got {}",
result.z
);
}
#[test]
fn test_matrix_multiplication_order() {
let scale = Matrix3D::scale(2.0, 1.0, 1.0);
let translate = Matrix3D::translate(10.0, 0.0, 0.0);
let combined = scale * translate;
let v = Vertex3D::new(1.0, 0.0, 0.0);
let result = combined.transform_vertex(v);
assert!(approx_eq(result.x, 12.0), "Expected 12.0, got {}", result.x);
}
#[test]
fn test_transform_vector_no_translation() {
let m = Matrix3D::translate(10.0, 20.0, 30.0);
let v = Vertex3D::new(1.0, 0.0, 0.0);
let result = m.transform_vector(v);
assert!(approx_eq(result.x, 1.0));
assert!(approx_eq(result.y, 0.0));
assert!(approx_eq(result.z, 0.0));
}
#[test]
fn test_combined_rotation() {
let rot_x = Matrix3D::rotate_x(90.0_f32.to_radians());
let rot_z = Matrix3D::rotate_z(90.0_f32.to_radians());
let combined = rot_x * rot_z;
let v = Vertex3D::new(0.0, 1.0, 0.0);
let result = combined.transform_vertex(v);
assert!(approx_eq(result.x, 0.0), "x: expected 0, got {}", result.x);
assert!(approx_eq(result.y, 0.0), "y: expected 0, got {}", result.y);
assert!(approx_eq(result.z, 1.0), "z: expected 1, got {}", result.z);
}
}