Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions crates/lambda-rs/examples/reflective_room.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,8 @@ impl Component<ComponentResult, String> for ReflectiveRoomExample {
lambda::math::matrix::identity_matrix(4, 4),
[1.0, 0.0, 0.0],
-self.camera_pitch_turns,
);
)
.expect("rotation axis must be a unit axis vector");
let view = rot_x.multiply(&compute_view_matrix(camera.position));
let projection = compute_perspective_projection(
camera.field_of_view_in_turns,
Expand Down Expand Up @@ -360,7 +361,8 @@ impl Component<ComponentResult, String> for ReflectiveRoomExample {
model_floor,
[1.0, 0.0, 0.0],
self.floor_tilt_turns,
);
)
.expect("rotation axis must be a unit axis vector");
let mvp_floor = projection.multiply(&view).multiply(&model_floor);

let viewport = ViewportBuilder::new().build(self.width, self.height);
Expand Down
6 changes: 4 additions & 2 deletions crates/lambda-rs/examples/textured_cube.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,12 +411,14 @@ impl Component<ComponentResult, String> for TexturedCubeExample {
model,
[0.0, 1.0, 0.0],
angle_y_turns,
);
)
.expect("rotation axis must be a unit axis vector");
model = lambda::math::matrix::rotate_matrix(
model,
[1.0, 0.0, 0.0],
angle_x_turns,
);
)
.expect("rotation axis must be a unit axis vector");

let view = compute_view_matrix(camera.position);
let projection = compute_perspective_projection(
Expand Down
65 changes: 65 additions & 0 deletions crates/lambda-rs/src/math/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//! Math error types returned from fallible operations.

use std::fmt;

/// Errors returned by fallible math operations in `lambda-rs`.
#[derive(Debug, Clone, PartialEq)]
pub enum MathError {
/// Cross product requires exactly 3 dimensions.
CrossProductDimension { actual: usize },
/// Cross product requires both vectors to have the same dimension.
MismatchedVectorDimensions { left: usize, right: usize },
/// Rotation axis must be a unit axis vector (one of `[1,0,0]`, `[0,1,0]`,
/// `[0,0,1]`). A zero axis (`[0,0,0]`) is treated as "no rotation".
InvalidRotationAxis { axis: [f32; 3] },
/// Rotation requires a 4x4 matrix.
InvalidRotationMatrixSize { rows: usize, cols: usize },
/// Determinant requires a square matrix.
NonSquareMatrix { rows: usize, cols: usize },
/// Determinant cannot be computed for an empty matrix.
EmptyMatrix,
/// Cannot normalize a zero-length vector.
ZeroLengthVector,
}

impl fmt::Display for MathError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MathError::CrossProductDimension { actual } => {
return write!(f, "Cross product requires 3D vectors, got {}D", actual);
}
MathError::MismatchedVectorDimensions { left, right } => {
return write!(
f,
"Vectors must have matching dimensions (left {}D, right {}D)",
left, right
);
}
MathError::InvalidRotationAxis { axis } => {
return write!(f, "Rotation axis {:?} is not a unit axis vector", axis);
}
MathError::InvalidRotationMatrixSize { rows, cols } => {
return write!(
f,
"Rotation requires a 4x4 matrix, got {}x{}",
rows, cols
);
}
MathError::NonSquareMatrix { rows, cols } => {
return write!(
f,
"Determinant requires square matrix, got {}x{}",
rows, cols
);
}
MathError::EmptyMatrix => {
return write!(f, "Determinant requires a non-empty matrix");
}
MathError::ZeroLengthVector => {
return write!(f, "Cannot normalize a zero-length vector");
}
}
}
}

impl std::error::Error for MathError {}
167 changes: 89 additions & 78 deletions crates/lambda-rs/src/math/matrix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use super::{
turns_to_radians,
vector::Vector,
MathError,
};

// -------------------------------- MATRIX -------------------------------------
Expand All @@ -17,7 +18,10 @@ pub trait Matrix<V: Vector> {
fn transpose(&self) -> Self;
fn inverse(&self) -> Self;
fn transform(&self, other: &V) -> V;
fn determinant(&self) -> f32;
/// Compute the determinant of the matrix.
///
/// Returns an error when the matrix is empty or not square.
fn determinant(&self) -> Result<f32, MathError>;
fn size(&self) -> (usize, usize);
fn row(&self, row: usize) -> &V;
fn at(&self, row: usize, column: usize) -> V::Scalar;
Expand Down Expand Up @@ -84,70 +88,67 @@ pub fn translation_matrix<
/// Rotates the input matrix by the given number of turns around the given axis.
/// The axis must be a unit vector and the turns must be in the range [0, 1).
/// The rotation is counter-clockwise when looking down the axis.
///
/// Returns an error when the matrix is not 4x4, or when `axis_to_rotate` is not
/// a unit axis vector (`[1,0,0]`, `[0,1,0]`, `[0,0,1]`). A zero axis (`[0,0,0]`)
/// is treated as "no rotation".
pub fn rotate_matrix<
InputVector: Vector<Scalar = f32>,
ResultingVector: Vector<Scalar = f32>,
OutputMatrix: Matrix<ResultingVector> + Default + Clone,
V: Vector<Scalar = f32>,
MatrixLike: Matrix<V> + Default + Clone,
>(
matrix_to_rotate: OutputMatrix,
axis_to_rotate: InputVector,
matrix_to_rotate: MatrixLike,
axis_to_rotate: [f32; 3],
angle_in_turns: f32,
) -> OutputMatrix {
) -> Result<MatrixLike, MathError> {
let (rows, columns) = matrix_to_rotate.size();
assert_eq!(rows, columns, "Matrix must be square");
assert_eq!(rows, 4, "Matrix must be 4x4");
assert_eq!(
axis_to_rotate.size(),
3,
"Axis vector must have 3 elements (x, y, z)"
);
if rows != columns {
return Err(MathError::NonSquareMatrix {
rows,
cols: columns,
});
}
if rows != 4 {
return Err(MathError::InvalidRotationMatrixSize {
rows,
cols: columns,
});
}

let angle_in_radians = turns_to_radians(angle_in_turns);
let cosine_of_angle = angle_in_radians.cos();
let sin_of_angle = angle_in_radians.sin();

let _t = 1.0 - cosine_of_angle;
let x = axis_to_rotate.at(0);
let y = axis_to_rotate.at(1);
let z = axis_to_rotate.at(2);

let mut rotation_matrix = OutputMatrix::default();

let rotation = match (x as u8, y as u8, z as u8) {
(0, 0, 0) => {
// No rotation
return matrix_to_rotate;
}
(0, 0, 1) => {
// Rotate around z-axis
[
[cosine_of_angle, sin_of_angle, 0.0, 0.0],
[-sin_of_angle, cosine_of_angle, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
]
}
(0, 1, 0) => {
// Rotate around y-axis
[
[cosine_of_angle, 0.0, -sin_of_angle, 0.0],
[0.0, 1.0, 0.0, 0.0],
[sin_of_angle, 0.0, cosine_of_angle, 0.0],
[0.0, 0.0, 0.0, 1.0],
]
}
(1, 0, 0) => {
// Rotate around x-axis
[
[1.0, 0.0, 0.0, 0.0],
[0.0, cosine_of_angle, sin_of_angle, 0.0],
[0.0, -sin_of_angle, cosine_of_angle, 0.0],
[0.0, 0.0, 0.0, 1.0],
]
}
_ => {
panic!("Axis must be a unit vector")
}
let mut rotation_matrix = MatrixLike::default();
let [x, y, z] = axis_to_rotate;

let rotation = if axis_to_rotate == [0.0, 0.0, 0.0] {
return Ok(matrix_to_rotate);
} else if axis_to_rotate == [0.0, 0.0, 1.0] {
// Rotate around z-axis
[
[cosine_of_angle, sin_of_angle, 0.0, 0.0],
[-sin_of_angle, cosine_of_angle, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
]
} else if axis_to_rotate == [0.0, 1.0, 0.0] {
// Rotate around y-axis
[
[cosine_of_angle, 0.0, -sin_of_angle, 0.0],
[0.0, 1.0, 0.0, 0.0],
[sin_of_angle, 0.0, cosine_of_angle, 0.0],
[0.0, 0.0, 0.0, 1.0],
]
} else if axis_to_rotate == [1.0, 0.0, 0.0] {
// Rotate around x-axis
[
[1.0, 0.0, 0.0, 0.0],
[0.0, cosine_of_angle, sin_of_angle, 0.0],
[0.0, -sin_of_angle, cosine_of_angle, 0.0],
[0.0, 0.0, 0.0, 1.0],
]
} else {
return Err(MathError::InvalidRotationAxis { axis: [x, y, z] });
};
Comment thread
vmarcella marked this conversation as resolved.

for (i, row) in rotation.iter().enumerate().take(rows) {
Expand All @@ -156,7 +157,7 @@ pub fn rotate_matrix<
}
}

return matrix_to_rotate.multiply(&rotation_matrix);
return Ok(matrix_to_rotate.multiply(&rotation_matrix));
}

/// Creates a 4x4 perspective matrix given the fov in turns (unit between
Expand Down Expand Up @@ -325,40 +326,47 @@ where
}

/// Computes the determinant of any square matrix using Laplace expansion.
fn determinant(&self) -> f32 {
let (width, height) =
(self.as_ref()[0].as_ref().len(), self.as_ref().len());
fn determinant(&self) -> Result<f32, MathError> {
let rows = self.as_ref().len();
if rows == 0 {
return Err(MathError::EmptyMatrix);
}

let cols = self.as_ref()[0].as_ref().len();
if cols == 0 {
return Err(MathError::EmptyMatrix);
}

if width != height {
panic!("Cannot compute determinant of non-square matrix");
if cols != rows {
return Err(MathError::NonSquareMatrix { rows, cols });
}

return match height {
1 => self.as_ref()[0].as_ref()[0],
return match rows {
1 => Ok(self.as_ref()[0].as_ref()[0]),
2 => {
let a = self.at(0, 0);
let b = self.at(0, 1);
let c = self.at(1, 0);
let d = self.at(1, 1);
a * d - b * c
return Ok(a * d - b * c);
}
Comment thread
vmarcella marked this conversation as resolved.
_ => {
let mut result = 0.0;
for i in 0..height {
let mut submatrix: Vec<Vec<f32>> = Vec::with_capacity(height - 1);
for j in 1..height {
for i in 0..rows {
let mut submatrix: Vec<Vec<f32>> = Vec::with_capacity(rows - 1);
for j in 1..rows {
let mut row = Vec::new();
for k in 0..height {
for k in 0..rows {
if k != i {
row.push(self.at(j, k));
}
}
submatrix.push(row);
}
result +=
self.at(0, i) * submatrix.determinant() * (-1.0_f32).powi(i as i32);
let sub_determinant = submatrix.determinant()?;
result += self.at(0, i) * sub_determinant * (-1.0_f32).powi(i as i32);
}
result
return Ok(result);
}
};
}
Expand Down Expand Up @@ -397,6 +405,7 @@ mod tests {
use crate::math::{
matrix::translation_matrix,
turns_to_radians,
MathError,
};

#[test]
Expand Down Expand Up @@ -438,17 +447,17 @@ mod tests {
#[test]
fn square_matrix_determinant() {
let m = [[3.0, 8.0], [4.0, 6.0]];
assert_eq!(m.determinant(), -14.0);
assert_eq!(m.determinant(), Ok(-14.0));

let m2 = [[6.0, 1.0, 1.0], [4.0, -2.0, 5.0], [2.0, 8.0, 7.0]];
assert_eq!(m2.determinant(), -306.0);
assert_eq!(m2.determinant(), Ok(-306.0));
}

#[test]
fn non_square_matrix_determinant() {
let m = [[3.0, 8.0], [4.0, 6.0], [0.0, 1.0]];
let result = std::panic::catch_unwind(|| m.determinant());
assert!(result.is_err());
let result = m.determinant();
assert_eq!(result, Err(MathError::NonSquareMatrix { rows: 3, cols: 2 }));
}

#[test]
Expand Down Expand Up @@ -503,7 +512,8 @@ mod tests {
fn rotate_matrices() {
// Test a zero turn rotation.
let matrix: [[f32; 4]; 4] = filled_matrix(4, 4, 1.0);
let rotated_matrix = rotate_matrix(matrix, [0.0, 0.0, 1.0], 0.0);
let rotated_matrix =
rotate_matrix(matrix, [0.0, 0.0, 1.0], 0.0).expect("valid axis");
assert_eq!(rotated_matrix, matrix);

// Test a 90 degree rotation.
Expand All @@ -513,7 +523,8 @@ mod tests {
[9.0, 10.0, 11.0, 12.0],
[13.0, 14.0, 15.0, 16.0],
];
let rotated = rotate_matrix(matrix, [0.0, 1.0, 0.0], 0.25);
let rotated =
rotate_matrix(matrix, [0.0, 1.0, 0.0], 0.25).expect("valid axis");
let expected = [
[3.0, 1.9999999, -1.0000001, 4.0],
[7.0, 5.9999995, -5.0000005, 8.0],
Expand Down
3 changes: 3 additions & 0 deletions crates/lambda-rs/src/math/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
//! Lambda Math Types and operations

pub mod error;
pub mod matrix;
pub mod vector;

pub use error::MathError;

/// Angle units used by conversion helpers and matrix transforms.
///
/// Prefer `Angle::Turns` for ergonomic quarter/half rotations when building
Expand Down
Loading
Loading