musileko/src/math/zalgebra/src/mat4.zig

652 lines
22 KiB
Zig

const std = @import("std");
const math = std.math;
const testing = std.testing;
const print = std.debug.print;
const root = @import("main.zig");
const vec4 = @import("vec4.zig");
const vec3 = @import("vec3.zig");
const quat = @import("quaternion.zig");
const Vec3 = vec3.Vec3;
const Vector3 = vec3.Vector3;
const Vector4 = vec4.Vector4;
const Quaternion = quat.Quaternion;
const Quat = quat.Quat;
pub const Mat4 = Mat4x4(f32);
pub const Mat4_f64 = Mat4x4(f64);
pub const perspective = Mat4.perspective;
pub const orthographic = Mat4.orthographic;
pub const lookAt = Mat4.lookAt;
/// A column-major 4x4 matrix
/// Note: Column-major means accessing data like m.data[COLUMN][ROW].
pub fn Mat4x4(comptime T: type) type {
if (@typeInfo(T) != .Float) {
@compileError("Mat4x4 not implemented for " ++ @typeName(T));
}
return extern struct {
data: [4][4]T,
const Self = @This();
pub fn identity() Self {
return .{
.data = .{
.{ 1, 0, 0, 0 },
.{ 0, 1, 0, 0 },
.{ 0, 0, 1, 0 },
.{ 0, 0, 0, 1 },
},
};
}
/// Construct new 4x4 matrix from given slice.
pub fn fromSlice(data: *const [16]T) Self {
return .{
.data = .{
data[0..4].*,
data[4..8].*,
data[8..12].*,
data[12..16].*,
},
};
}
/// Negate the given matrix.
pub fn negate(mat: Self) Self {
var result = mat;
var col: usize = 0;
while (col < 4) : (col += 1) {
var row: usize = 0;
while (row < 4) : (row += 1) {
result.data[col][row] = -mat.data[col][row];
}
}
return result;
}
/// Transpose the given matrix.
pub fn transpose(mat: Self) Self {
var result = mat;
var col: usize = 0;
while (col < 4) : (col += 1) {
var row: usize = col;
while (row < 4) : (row += 1) {
std.mem.swap(T, &result.data[col][row], &result.data[row][col]);
}
}
return result;
}
/// Return a pointer to the inner data of the matrix.
pub fn getData(mat: *const Self) *const T {
return @ptrCast(*const T, &mat.data);
}
pub fn eql(left: Self, right: Self) bool {
var col: usize = 0;
while (col < 4) : (col += 1) {
var row: usize = 0;
while (row < 4) : (row += 1) {
if (left.data[col][row] != right.data[col][row]) {
return false;
}
}
}
return true;
}
pub fn multByVec4(mat: Self, v: Vector4(T)) Vector4(T) {
var result: Vector4(T) = undefined;
result.x = (mat.data[0][0] * v.x) + (mat.data[1][0] * v.y) + (mat.data[2][0] * v.z) + (mat.data[3][0] * v.w);
result.y = (mat.data[0][1] * v.x) + (mat.data[1][1] * v.y) + (mat.data[2][1] * v.z) + (mat.data[3][1] * v.w);
result.z = (mat.data[0][2] * v.x) + (mat.data[1][2] * v.y) + (mat.data[2][2] * v.z) + (mat.data[3][2] * v.w);
result.w = (mat.data[0][3] * v.x) + (mat.data[1][3] * v.y) + (mat.data[2][3] * v.z) + (mat.data[3][3] * v.w);
return result;
}
/// Construct 4x4 translation matrix by multiplying identity matrix and
/// given translation vector.
pub fn fromTranslate(axis: Vector3(T)) Self {
var mat = Self.identity();
mat.data[3][0] = axis.x;
mat.data[3][1] = axis.y;
mat.data[3][2] = axis.z;
return mat;
}
/// Make a translation between the given matrix and the given axis.
pub fn translate(mat: Self, axis: Vector3(T)) Self {
const trans_mat = Self.fromTranslate(axis);
return Self.mult(trans_mat, mat);
}
/// Get translation Vec3 from current matrix.
pub fn extractTranslation(self: Self) Vector3(T) {
return Vector3(T).new(self.data[3][0], self.data[3][1], self.data[3][2]);
}
/// Construct a 4x4 matrix from given axis and angle (in degrees).
pub fn fromRotation(angle_in_degrees: T, axis: Vector3(T)) Self {
var mat = Self.identity();
const norm_axis = axis.norm();
const sin_theta = math.sin(root.toRadians(angle_in_degrees));
const cos_theta = math.cos(root.toRadians(angle_in_degrees));
const cos_value = 1.0 - cos_theta;
mat.data[0][0] = (norm_axis.x * norm_axis.x * cos_value) + cos_theta;
mat.data[0][1] = (norm_axis.x * norm_axis.y * cos_value) + (norm_axis.z * sin_theta);
mat.data[0][2] = (norm_axis.x * norm_axis.z * cos_value) - (norm_axis.y * sin_theta);
mat.data[1][0] = (norm_axis.y * norm_axis.x * cos_value) - (norm_axis.z * sin_theta);
mat.data[1][1] = (norm_axis.y * norm_axis.y * cos_value) + cos_theta;
mat.data[1][2] = (norm_axis.y * norm_axis.z * cos_value) + (norm_axis.x * sin_theta);
mat.data[2][0] = (norm_axis.z * norm_axis.x * cos_value) + (norm_axis.y * sin_theta);
mat.data[2][1] = (norm_axis.z * norm_axis.y * cos_value) - (norm_axis.x * sin_theta);
mat.data[2][2] = (norm_axis.z * norm_axis.z * cos_value) + cos_theta;
return mat;
}
pub fn rotate(mat: Self, angle_in_degrees: T, axis: Vector3(T)) Self {
const rotation_mat = Self.fromRotation(angle_in_degrees, axis);
return Self.mult(mat, rotation_mat);
}
/// Construct a rotation matrix from euler angles (X * Y * Z).
/// Order matters because matrix multiplication are NOT commutative.
pub fn fromEulerAngle(euler_angle: Vector3(T)) Self {
const x = Self.fromRotation(euler_angle.x, Vec3.new(1, 0, 0));
const y = Self.fromRotation(euler_angle.y, Vec3.new(0, 1, 0));
const z = Self.fromRotation(euler_angle.z, Vec3.new(0, 0, 1));
return z.mult(y.mult(x));
}
/// Ortho normalize given matrix.
pub fn orthoNormalize(mat: Self) Self {
const column_1 = Vec3.new(mat.data[0][0], mat.data[0][1], mat.data[0][2]).norm();
const column_2 = Vec3.new(mat.data[1][0], mat.data[1][1], mat.data[1][2]).norm();
const column_3 = Vec3.new(mat.data[2][0], mat.data[2][1], mat.data[2][2]).norm();
var result = mat;
result.data[0][0] = column_1.x;
result.data[0][1] = column_1.y;
result.data[0][2] = column_1.z;
result.data[1][0] = column_2.x;
result.data[1][1] = column_2.y;
result.data[1][2] = column_2.z;
result.data[2][0] = column_3.x;
result.data[2][1] = column_3.y;
result.data[2][2] = column_3.z;
return result;
}
/// Return the rotation as Euler angles in degrees.
/// Taken from Mike Day at Insomniac Games (and `glm` as the same function).
/// For more details: https://d3cw3dd2w32x2b.cloudfront.net/wp-content/uploads/2012/07/euler-angles1.pdf
pub fn extractRotation(self: Self) Vector3(T) {
const m = self.orthoNormalize();
const theta_x = math.atan2(T, m.data[1][2], m.data[2][2]);
const c2 = math.sqrt(math.pow(f32, m.data[0][0], 2) + math.pow(f32, m.data[0][1], 2));
const theta_y = math.atan2(T, -m.data[0][2], math.sqrt(c2));
const s1 = math.sin(theta_x);
const c1 = math.cos(theta_x);
const theta_z = math.atan2(T, s1 * m.data[2][0] - c1 * m.data[1][0], c1 * m.data[1][1] - s1 * m.data[2][1]);
return Vec3.new(root.toDegrees(theta_x), root.toDegrees(theta_y), root.toDegrees(theta_z));
}
pub fn fromScale(axis: Vector3(T)) Self {
var mat = Self.identity();
mat.data[0][0] = axis.x;
mat.data[1][1] = axis.y;
mat.data[2][2] = axis.z;
return mat;
}
pub fn scale(mat: Self, axis: Vector3(T)) Self {
const scale_mat = Self.fromScale(axis);
return Self.mult(scale_mat, mat);
}
pub fn extractScale(mat: Self) Vector3(T) {
const scale_x = Vec3.new(mat.data[0][0], mat.data[0][1], mat.data[0][2]).length();
const scale_y = Vec3.new(mat.data[1][0], mat.data[1][1], mat.data[1][2]).length();
const scale_z = Vec3.new(mat.data[2][0], mat.data[2][1], mat.data[2][2]).length();
return Vector3(T).new(scale_x, scale_y, scale_z);
}
/// Construct a perspective 4x4 matrix.
/// Note: Field of view is given in degrees.
/// Also for more details https://www.khronos.org/registry/OpenGL-Refpages/gl2.1/xhtml/gluPerspective.xml.
pub fn perspective(fovy_in_degrees: T, aspect_ratio: T, z_near: T, z_far: T) Self {
var mat: Self = Self.identity();
const f = 1.0 / math.tan(root.toRadians(fovy_in_degrees) * 0.5);
mat.data[0][0] = f / aspect_ratio;
mat.data[1][1] = f;
mat.data[2][2] = (z_near + z_far) / (z_near - z_far);
mat.data[2][3] = -1;
mat.data[3][2] = 2 * z_far * z_near / (z_near - z_far);
mat.data[3][3] = 0;
return mat;
}
/// Construct an orthographic 4x4 matrix.
pub fn orthographic(left: T, right: T, bottom: T, top: T, z_near: T, z_far: T) Self {
var mat: Self = undefined;
mat.data[0][0] = 2.0 / (right - left);
mat.data[1][1] = 2.0 / (top - bottom);
mat.data[2][2] = 2.0 / (z_near - z_far);
mat.data[3][3] = 1.0;
mat.data[3][0] = (left + right) / (left - right);
mat.data[3][1] = (bottom + top) / (bottom - top);
mat.data[3][2] = (z_far + z_near) / (z_near - z_far);
return mat;
}
/// Right-handed lookAt function.
pub fn lookAt(eye: Vector3(T), target: Vector3(T), up: Vector3(T)) Self {
const f = Vector3(T).norm(Vector3(T).sub(target, eye));
const s = Vector3(T).norm(Vector3(T).cross(f, up));
const u = Vector3(T).cross(s, f);
var mat: Self = undefined;
mat.data[0][0] = s.x;
mat.data[0][1] = u.x;
mat.data[0][2] = -f.x;
mat.data[0][3] = 0.0;
mat.data[1][0] = s.y;
mat.data[1][1] = u.y;
mat.data[1][2] = -f.y;
mat.data[1][3] = 0.0;
mat.data[2][0] = s.z;
mat.data[2][1] = u.z;
mat.data[2][2] = -f.z;
mat.data[2][3] = 0.0;
mat.data[3][0] = -Vector3(T).dot(s, eye);
mat.data[3][1] = -Vector3(T).dot(u, eye);
mat.data[3][2] = Vector3(T).dot(f, eye);
mat.data[3][3] = 1.0;
return mat;
}
/// Matrices multiplication.
/// Produce a new matrix from given two matrices.
pub fn mult(left: Self, right: Self) Self {
var mat = Self.identity();
var columns: usize = 0;
while (columns < 4) : (columns += 1) {
var rows: usize = 0;
while (rows < 4) : (rows += 1) {
var sum: T = 0.0;
var current_mat: usize = 0;
while (current_mat < 4) : (current_mat += 1) {
sum += left.data[current_mat][rows] * right.data[columns][current_mat];
}
mat.data[columns][rows] = sum;
}
}
return mat;
}
/// Construct inverse 4x4 from given matrix.
/// Note: This is not the most efficient way to do this.
/// TODO: Make it more efficient.
pub fn inv(mat: Self) Self {
var inv_mat: Self = undefined;
var s: [6]T = undefined;
var c: [6]T = undefined;
s[0] = mat.data[0][0] * mat.data[1][1] - mat.data[1][0] * mat.data[0][1];
s[1] = mat.data[0][0] * mat.data[1][2] - mat.data[1][0] * mat.data[0][2];
s[2] = mat.data[0][0] * mat.data[1][3] - mat.data[1][0] * mat.data[0][3];
s[3] = mat.data[0][1] * mat.data[1][2] - mat.data[1][1] * mat.data[0][2];
s[4] = mat.data[0][1] * mat.data[1][3] - mat.data[1][1] * mat.data[0][3];
s[5] = mat.data[0][2] * mat.data[1][3] - mat.data[1][2] * mat.data[0][3];
c[0] = mat.data[2][0] * mat.data[3][1] - mat.data[3][0] * mat.data[2][1];
c[1] = mat.data[2][0] * mat.data[3][2] - mat.data[3][0] * mat.data[2][2];
c[2] = mat.data[2][0] * mat.data[3][3] - mat.data[3][0] * mat.data[2][3];
c[3] = mat.data[2][1] * mat.data[3][2] - mat.data[3][1] * mat.data[2][2];
c[4] = mat.data[2][1] * mat.data[3][3] - mat.data[3][1] * mat.data[2][3];
c[5] = mat.data[2][2] * mat.data[3][3] - mat.data[3][2] * mat.data[2][3];
const determ = 1.0 / (s[0] * c[5] - s[1] * c[4] + s[2] * c[3] + s[3] * c[2] - s[4] * c[1] + s[5] * c[0]);
inv_mat.data[0][0] =
(mat.data[1][1] * c[5] - mat.data[1][2] * c[4] + mat.data[1][3] * c[3]) * determ;
inv_mat.data[0][1] =
(-mat.data[0][1] * c[5] + mat.data[0][2] * c[4] - mat.data[0][3] * c[3]) * determ;
inv_mat.data[0][2] =
(mat.data[3][1] * s[5] - mat.data[3][2] * s[4] + mat.data[3][3] * s[3]) * determ;
inv_mat.data[0][3] =
(-mat.data[2][1] * s[5] + mat.data[2][2] * s[4] - mat.data[2][3] * s[3]) * determ;
inv_mat.data[1][0] =
(-mat.data[1][0] * c[5] + mat.data[1][2] * c[2] - mat.data[1][3] * c[1]) * determ;
inv_mat.data[1][1] =
(mat.data[0][0] * c[5] - mat.data[0][2] * c[2] + mat.data[0][3] * c[1]) * determ;
inv_mat.data[1][2] =
(-mat.data[3][0] * s[5] + mat.data[3][2] * s[2] - mat.data[3][3] * s[1]) * determ;
inv_mat.data[1][3] =
(mat.data[2][0] * s[5] - mat.data[2][2] * s[2] + mat.data[2][3] * s[1]) * determ;
inv_mat.data[2][0] =
(mat.data[1][0] * c[4] - mat.data[1][1] * c[2] + mat.data[1][3] * c[0]) * determ;
inv_mat.data[2][1] =
(-mat.data[0][0] * c[4] + mat.data[0][1] * c[2] - mat.data[0][3] * c[0]) * determ;
inv_mat.data[2][2] =
(mat.data[3][0] * s[4] - mat.data[3][1] * s[2] + mat.data[3][3] * s[0]) * determ;
inv_mat.data[2][3] =
(-mat.data[2][0] * s[4] + mat.data[2][1] * s[2] - mat.data[2][3] * s[0]) * determ;
inv_mat.data[3][0] =
(-mat.data[1][0] * c[3] + mat.data[1][1] * c[1] - mat.data[1][2] * c[0]) * determ;
inv_mat.data[3][1] =
(mat.data[0][0] * c[3] - mat.data[0][1] * c[1] + mat.data[0][2] * c[0]) * determ;
inv_mat.data[3][2] =
(-mat.data[3][0] * s[3] + mat.data[3][1] * s[1] - mat.data[3][2] * s[0]) * determ;
inv_mat.data[3][3] =
(mat.data[2][0] * s[3] - mat.data[2][1] * s[1] + mat.data[2][2] * s[0]) * determ;
return inv_mat;
}
/// Return 4x4 matrix from given all transform components; `translation`, `rotation` and `sclale`.
/// The final order is T * R * S.
/// Note: `rotation` could be `Vec3` (Euler angles) or a `quat`.
pub fn recompose(translation: Vector3(T), rotation: anytype, scaler: Vector3(T)) Self {
const t = Self.fromTranslate(translation);
const s = Self.fromScale(scaler);
const r = switch (@TypeOf(rotation)) {
Quaternion(T) => Quaternion(T).toMat4(rotation),
Vector3(T) => Self.fromEulerAngle(rotation),
else => @compileError("Recompose not implemented for " ++ @typeName(@TypeOf(rotation))),
};
return t.mult(r.mult(s));
}
/// Return `translation`, `rotation` and `scale` components from given matrix.
/// For now, the rotation returned is a quaternion. If you want to get Euler angles
/// from it, just do: `returned_quat.extractRotation()`.
/// Note: We ortho nornalize the given matrix before extracting the rotation.
pub fn decompose(mat: Self) struct { t: Vector3(T), r: Quaternion(T), s: Vector3(T) } {
const t = mat.extractTranslation();
const s = mat.extractScale();
const r = Quat.fromMat4(mat.orthoNormalize());
return .{
.t = t,
.r = r,
.s = s,
};
}
/// Print the 4x4 to stderr.
pub fn debugPrint(self: Self) void {
const string =
\\ ({d}, {d}, {d}, {d})
\\ ({d}, {d}, {d}, {d})
\\ ({d}, {d}, {d}, {d})
\\ ({d}, {d}, {d}, {d})
\\
;
print(string, .{
self.data[0][0],
self.data[1][0],
self.data[2][0],
self.data[3][0],
self.data[0][1],
self.data[1][1],
self.data[2][1],
self.data[3][1],
self.data[0][2],
self.data[1][2],
self.data[2][2],
self.data[3][2],
self.data[0][3],
self.data[1][3],
self.data[2][3],
self.data[3][3],
});
}
};
}
test "zalgebra.Mat4.eql" {
const a = Mat4.identity();
const b = Mat4.identity();
const c = Mat4{
.data = .{
.{ 0, 0, 0, 0 },
.{ 0, 0, 0, 0 },
.{ 0, 0, 0, 0 },
.{ 0, 0, 0, 0 },
},
};
try testing.expectEqual(Mat4.eql(a, b), true);
try testing.expectEqual(Mat4.eql(a, c), false);
}
test "zalgebra.Mat4.negate" {
const a = Mat4{
.data = .{
.{ 1, 2, 3, 4 },
.{ 5, -6, 7, 8 },
.{ 9, 10, 11, -12 },
.{ 13, 14, 15, 16 },
},
};
const b = Mat4{
.data = .{
.{ -1, -2, -3, -4 },
.{ -5, 6, -7, -8 },
.{ -9, -10, -11, 12 },
.{ -13, -14, -15, -16 },
},
};
try testing.expectEqual(Mat4.eql(a.negate(), b), true);
}
test "zalgebra.Mat4.transpose" {
const a = Mat4{
.data = .{
.{ 1, 2, 3, 4 },
.{ 5, 6, 7, 8 },
.{ 9, 10, 11, 12 },
.{ 13, 14, 15, 16 },
},
};
const b = Mat4{
.data = .{
.{ 1, 5, 9, 13 },
.{ 2, 6, 10, 14 },
.{ 3, 7, 11, 15 },
.{ 4, 8, 12, 16 },
},
};
try testing.expectEqual(Mat4.eql(a.transpose(), b), true);
}
test "zalgebra.Mat4.fromSlice" {
const data = [_]f32{ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 };
const result = Mat4.fromSlice(&data);
try testing.expectEqual(Mat4.eql(result, Mat4.identity()), true);
}
test "zalgebra.Mat4.fromTranslate" {
const a = Mat4.fromTranslate(Vec3.new(2, 3, 4));
try testing.expectEqual(Mat4.eql(a, Mat4{
.data = .{
.{ 1, 0, 0, 0 },
.{ 0, 1, 0, 0 },
.{ 0, 0, 1, 0 },
.{ 2, 3, 4, 1 },
},
}), true);
}
test "zalgebra.Mat4.translate" {
const a = Mat4.fromTranslate(Vec3.new(2, 3, 2));
const result = Mat4.translate(a, Vec3.new(2, 3, 4));
try testing.expectEqual(Mat4.eql(result, Mat4{
.data = .{
.{ 1, 0, 0, 0 },
.{ 0, 1, 0, 0 },
.{ 0, 0, 1, 0 },
.{ 4, 6, 6, 1 },
},
}), true);
}
test "zalgebra.Mat4.fromScale" {
const a = Mat4.fromScale(Vec3.new(2, 3, 4));
try testing.expectEqual(Mat4.eql(a, Mat4{
.data = .{
.{ 2, 0, 0, 0 },
.{ 0, 3, 0, 0 },
.{ 0, 0, 4, 0 },
.{ 0, 0, 0, 1 },
},
}), true);
}
test "zalgebra.Mat4.scale" {
const a = Mat4.fromScale(Vec3.new(2, 3, 4));
const result = Mat4.scale(a, Vec3.new(2, 2, 2));
try testing.expectEqual(Mat4.eql(result, Mat4{
.data = .{
.{ 4, 0, 0, 0 },
.{ 0, 6, 0, 0 },
.{ 0, 0, 8, 0 },
.{ 0, 0, 0, 1 },
},
}), true);
}
test "zalgebra.Mat4.inv" {
const a: Mat4 = .{
.data = .{
.{ 2, 0, 0, 4 },
.{ 0, 2, 0, 0 },
.{ 0, 0, 2, 0 },
.{ 4, 0, 0, 2 },
},
};
try testing.expectEqual(Mat4.eql(a.inv(), Mat4{
.data = .{
.{ -0.1666666716337204, 0, 0, 0.3333333432674408 },
.{ 0, 0.5, 0, 0 },
.{ 0, 0, 0.5, 0 },
.{ 0.3333333432674408, 0, 0, -0.1666666716337204 },
},
}), true);
}
test "zalgebra.Mat4.extractTranslation" {
var a = Mat4.fromTranslate(Vec3.new(2, 3, 2));
a = a.translate(Vec3.new(2, 3, 2));
try testing.expectEqual(Vec3.eql(a.extractTranslation(), Vec3.new(4, 6, 4)), true);
}
test "zalgebra.Mat4.extractRotation" {
const a = Mat4.fromEulerAngle(Vec3.new(45, -5, 20));
try testing.expectEqual(Vec3.eql(
a.extractRotation(),
Vec3.new(45.000003814697266, -4.99052524, 19.999998092651367),
), true);
}
test "zalgebra.Mat4.extractScale" {
var a = Mat4.fromScale(Vec3.new(2, 4, 8));
a = a.scale(Vec3.new(2, 4, 8));
try testing.expectEqual(Vec3.eql(a.extractScale(), Vec3.new(4, 16, 64)), true);
}
test "zalgebra.Mat4.recompose" {
const result = Mat4.recompose(
Vec3.new(2, 2, 2),
Vec3.new(45, 5, 0),
Vec3.new(1, 1, 1),
);
try testing.expectEqual(Mat4.eql(result, Mat4{
.data = .{
.{ 0.9961947202682495, 0, -0.08715573698282242, 0 },
.{ 0.06162841618061066, 0.7071067690849304, 0.7044160962104797, 0 },
.{ 0.06162841245532036, -0.7071068286895752, 0.704416036605835, 0 },
.{ 2, 2, 2, 1 },
},
}), true);
}
test "zalgebra.Mat4.decompose" {
const a = Mat4.recompose(
Vec3.new(10, 5, 5),
Vec3.new(45, 5, 0),
Vec3.new(1, 1, 1),
);
const result = a.decompose();
try testing.expectEqual(result.t.eql(Vec3.new(10, 5, 5)), true);
try testing.expectEqual(result.s.eql(Vec3.new(1, 1, 1)), true);
try testing.expectEqual(result.r.extractRotation().eql(Vec3.new(45, 5, -0.00000010712935250012379)), true);
}