diff --git a/src/main.zig b/src/main.zig index db30173f..59cf52c9 100644 --- a/src/main.zig +++ b/src/main.zig @@ -13,7 +13,7 @@ pub const gfx = @import("gfx/util.zig"); pub const gfx2d = struct { pub const Sprite2D = @import("gfx2d/Sprite2D.zig"); }; -pub const math = @import("math.zig"); +pub const math = @import("math/main.zig"); pub const testing = @import("testing.zig"); pub const Atlas = @import("atlas/Atlas.zig"); diff --git a/src/math.zig b/src/math.zig deleted file mode 100644 index cc8b4853..00000000 --- a/src/math.zig +++ /dev/null @@ -1,1191 +0,0 @@ -//! # mach/math is opinionated -//! -//! Math is hard enough as-is, without you having to question ground truths while problem solving. -//! As a result, mach/math takes a more opinionated approach than some other math libraries: we try -//! to encourage you through API design to use what we believe to be the best choices. For example, -//! other math libraries provide both LH and RH (left-handed and right-handed) variants for each -//! operation, and they sit on equal footing for you to choose from; mach/math may also provide both -//! variants as needed for conversions, but unlike other libraries will bless one choice with e.g. -//! a shorter function name to nudge you in the right direction and towards _consistency_. -//! -//! ## Matrices -//! -//! * Column-major matrix storage -//! * Column-vectors (i.e. right-associative multiplication, matrix * vector = vector) -//! -//! The benefit of using this "OpenGL-style" matrix is that it matches the conventions accepted by -//! the scientific community, it's what you'll find in linear algebra textbooks. It also matches -//! WebGPU, Vulkan, Unity3D, etc. It does NOT match DirectX-style which e.g. Unreal Engine uses. -//! -//! Note: many people will say "row major" or "column major" and implicitly mean three or more -//! different concepts; to avoid confusion we'll go over this in more depth below. -//! -//! ## Coordinate system (+Y up, left-handed) -//! -//! * Normalized Device coordinates: +Y up; (-1, -1) is at the bottom-left corner. -//! * Framebuffer coordinates: +Y down; (0, 0) is at the top-left corner. -//! * Texture coordinates: +Y down; (0, 0) is at the top-left corner. -//! -//! This coordinate system is consistent with WebGPU, Metal, DirectX, and Unity (NDC only.) -//! -//! Note that since +Y is up (not +Z), developers can seamlessly transition from 2D applications -//! to 3D applications by adding the Z component. This is in contrast to e.g. Z-up coordinate -//! systems, where 2D and 3D must differ. -//! -//! ## Additional reading -//! -//! * [Coordinate system explainer](https://machengine.org/engine/math/coordinate-system/) -//! * [Matrix storage explainer](https://machengine.org/engine/math/matrix-storage/) -//! - -const std = @import("std"); -const expect = std.testing.expect; -const expectEqual = std.testing.expectEqual; -const expectApproxEqAbs = std.testing.expectApproxEqAbs; - -pub const float = struct { - pub const equals = std.math.approxEqAbs; -}; - -pub const Vec2 = @Vector(2, f32); -pub const Vec3 = @Vector(3, f32); -pub const Vec4 = @Vector(4, f32); -pub const Mat3x3 = [3]@Vector(4, f32); -pub const Mat4x4 = [4]@Vector(4, f32); - -/// Vector operations -pub const vec = struct { - /// Returns the vector dimension size of the given type - /// - /// ``` - /// vec.size(Vec3) == 3 - /// ``` - pub inline fn size(comptime T: type) comptime_int { - switch (@typeInfo(T)) { - .Vector => |info| return info.len, - else => @compileError("Expected vector, found '" ++ @typeName(T) ++ "'"), - } - } - - /// Returns a vector with all components set to the `scalar` value: - /// - /// ``` - /// var v = vec.splat(Vec3, 1337.0); - /// // v.x == 1337, v.y == 1337, v.z == 1337 - /// ``` - pub inline fn splat(comptime V: type, scalar: f32) V { - return @splat(scalar); - } - - /// Computes the squared length of the vector. Faster than `len()` - pub inline fn len2(v: anytype) f32 { - switch (@TypeOf(v)) { - Vec2 => return (v[0] * v[0]) + (v[1] * v[1]), - Vec3 => return (v[0] * v[0]) + (v[1] * v[1]) + (v[2] * v[2]), - Vec4 => return (v[0] * v[0]) + (v[1] * v[1]) + (v[2] * v[2]) + (v[3] * v[3]), - else => @compileError("Expected vector, found '" ++ @typeName(@TypeOf(v)) ++ "'"), - } - } - - /// Computes the length of the vector. - pub inline fn len(v: anytype) f32 { - return std.math.sqrt(len2(v)); - } - - /// Normalizes a vector, such that all components end up in the range [0.0, 1.0]. - /// - /// d0 is added to the divisor, which means that e.g. if you provide a near-zero value, then in - /// situations where you would otherwise get NaN back you will instead get a near-zero vector. - /// - /// ``` - /// var v = normalize(v, 0.00000001); - /// ``` - pub inline fn normalize(v: anytype, d0: f32) @TypeOf(v) { - return v / (splat(@TypeOf(v), len(v) + d0)); - } - - /// Returns the normalized direction vector from points a and b. - /// - /// d0 is added to the divisor, which means that e.g. if you provide a near-zero value, then in - /// situations where you would otherwise get NaN back you will instead get a near-zero vector. - /// - /// ``` - /// var v = dir(a_point, b_point); - /// ``` - pub inline fn dir(a: anytype, b: @TypeOf(a), d0: f32) @TypeOf(a) { - return normalize(b - a, d0); - } - - /// Calculates the squared distance between points a and b. Faster than `dist()`. - pub inline fn dist2(a: anytype, b: @TypeOf(a)) f32 { - return len2(b - a); - } - - /// Calculates the distance between points a and b. - pub inline fn dist(a: anytype, b: @TypeOf(a)) f32 { - return std.math.sqrt(dist2(a, b)); - } - - /// Performs linear interpolation between a and b by some amount. - /// - /// ``` - /// lerp(a, b, 0.0) == a - /// lerp(a, b, 1.0) == b - /// ``` - pub inline fn lerp(a: anytype, b: @TypeOf(a), amount: f32) @TypeOf(a) { - return (a * splat(@TypeOf(a), 1.0 - amount)) + (b * splat(@TypeOf(a), amount)); - } - - /// Calculates the cross product between vector a and b. This can be done only in 3D - /// and required inputs are Vec3. - pub inline fn cross(a: Vec3, b: Vec3) Vec3 { - // I am using build in operators * and - of @Vector that work with SIMD if possible - // So I will first compute vector {y1*z2, z1*x2, x1*y2} - // And then compute vector {z1*y2, x1*z2, y1*x2} and then subtract them - // Equation is taken from: https://gamemath.com/book/vectors.html#cross_product - - const v1 = Vec3{ a[1], a[2], a[0] }; - const v2 = Vec3{ b[2], b[0], b[1] }; - const sub1 = v1 * v2; - - const _v1 = Vec3{ a[2], a[0], a[1] }; - const _v2 = Vec3{ b[1], b[2], b[0] }; - const sub2 = _v1 * _v2; - - return sub1 - sub2; - } - - /// Calculates the dot product between vector a and b and returns scalar. - pub inline fn dot(a: anytype, b: @TypeOf(a)) f32 { - switch (@TypeOf(a)) { - Vec2, Vec3, Vec4 => return @reduce(.Add, a * b), - else => @compileError("Expected vector, found '" ++ @typeName(@TypeOf(a)) ++ "'"), - } - } -}; - -test "vec.size" { - try expect(vec.size(Vec2) == 2); - try expect(vec.size(Vec3) == 3); - try expect(vec.size(Vec4) == 4); -} - -test "vec.splat" { - const v2 = vec.splat(Vec2, 1337.0); - try expect(v2[0] == 1337 and v2[1] == 1337); - - const v3 = vec.splat(Vec3, 1337.0); - try expect(v3[0] == 1337 and v3[1] == 1337 and v3[2] == 1337); - - const v4 = vec.splat(Vec4, 1337.0); - try expect(v4[0] == 1337 and v4[1] == 1337 and v4[2] == 1337 and v4[3] == 1337); -} - -test "vec.len2" { - { - const v = Vec2{ 1, 1 }; - try expect(vec.len2(v) == 2); - } - - { - const v = Vec3{ 2, 3, -4 }; - try expect(vec.len2(v) == 29); - } - - { - const v = Vec4{ 1.5, 2.25, 3.33, 4.44 }; - try expectApproxEqAbs(vec.len2(v), 38.115, 0.0001); - } - - { - const v = Vec4{ 0, 0, 0, 0 }; - try expect(vec.len2(v) == 0); - } -} - -test "vec.len" { - { - const v = Vec2{ 3, 4 }; - try expect(vec.len(v) == 5); - } - - { - const v = Vec3{ 4, 4, 2 }; - try expect(vec.len(v) == 6); - } - - { - const tolerance = 1e-8; - const v = Vec4{ 1.5, 2.25, 3.33, 4.44 }; - try expectApproxEqAbs(vec.len(v), 6.17373468817700328621, tolerance); - } - - { - const v = Vec4{ 0, 0, 0, 0 }; - try expect(vec.len(v) == 0); - } -} - -test "vec.normalize" { - const near_zero_value = 1e-8; - - { - const v = Vec2{ 1, 1 }; - const normalized = vec.normalize(v, 0); - const norm_val = std.math.sqrt1_2; // 1 / sqrt(2) - try expect(normalized[0] == norm_val and normalized[1] == norm_val); - } - - { - const v = Vec4{ 10, 0.5, -3, -0.2 }; - const normalized = vec.normalize(v, near_zero_value); - const result = Vec4{ 0.9565546486012808204, 0.04782773243006404102, -0.28696639458038424612, -0.01913109297202561641 }; - try expectApproxEqAbs(normalized[0], result[0], 1e-7); - try expectApproxEqAbs(normalized[1], result[1], 1e-7); - try expectApproxEqAbs(normalized[2], result[2], 1e-7); - try expectApproxEqAbs(normalized[3], result[3], 1e-7); - } - - // This test ensures that zero vector is also normalized to zero vector with help of divisor - { - const v = Vec2{ 0, 0 }; - const normalized = vec.normalize(v, near_zero_value); - try expect(normalized[0] == 0 and normalized[1] == 0); - } - - // TODO: This test should work but I am getting error: - // 'test.vec.normalize' failed: expected -nan, found -nan - // { - // const v = Vec2{ 0, 0 }; - // const normalized = vec.normalize(v, 0); - // const NaN = std.math.nan(f32); - // try std.testing.expectEqual(-NaN, normalized[0]); - // try std.testing.expectEqual(-NaN, normalized[1]); - - // // try std.testing.expectApproxEqAbs(normalized[0], -NaN, 0.00000001); - // // try std.testing.expectApproxEqAbs(normalized[1], -NaN, 0.00000001); - // } - - // These two tests show how for small values if we add divisor we get different normalized vectors - { - const v = Vec2{ near_zero_value, near_zero_value }; - const normalized = vec.normalize(v, near_zero_value); - const norm_val = 0.4142135623730950488; // 0.00000001 / (sqrt((0.00000001×0.00000001) + (0.00000001×0.00000001)) + 0.00000001) - try expect(normalized[0] == norm_val and normalized[1] == norm_val); - } - - { - const v = Vec2{ near_zero_value, near_zero_value }; - const normalized = vec.normalize(v, 0); - const norm_val = std.math.sqrt1_2; // 1 / sqrt(2) - try expect(normalized[0] == norm_val and normalized[1] == norm_val); - } -} - -test "vec.dir" { - const near_zero_value = 1e-8; - - { - const a = Vec2{ 0, 0 }; - const b = Vec2{ 0, 0 }; - const d = vec.dir(a, b, near_zero_value); - try expect(d[0] == 0 and d[1] == 0); - } - - { - const a = Vec2{ 1, 2 }; - const b = Vec2{ 1, 2 }; - const d = vec.dir(a, b, near_zero_value); - try expect(d[0] == 0 and d[1] == 0); - } - - { - const a = Vec2{ 1, 2 }; - const b = Vec2{ 3, 4 }; - const d = vec.dir(a, b, 0); - const result = std.math.sqrt1_2; // 1 / sqrt(2) - try expect(d[0] == result and d[1] == result); - } - - { - const a = Vec2{ 1, 2 }; - const b = Vec2{ -1, -2 }; - const d = vec.dir(a, b, 0); - const result = -0.44721359549995793928; // 1 / sqrt(5) - try expectApproxEqAbs(d[0], result, near_zero_value); - try expectApproxEqAbs(d[1], 2 * result, near_zero_value); - } - - { - const a = Vec3{ 1, -1, 0 }; - const b = Vec3{ 0, 1, 1 }; - const d = vec.dir(a, b, 0); - - const result_3 = 0.40824829046386301637; // 1 / sqrt(6) - const result_1 = -result_3; // -1 / sqrt(6) - const result_2 = 0.81649658092772603273; // sqrt(2/3) - try expectApproxEqAbs(d[0], result_1, 1e-7); - try expectApproxEqAbs(d[1], result_2, 1e-7); - try expectApproxEqAbs(d[2], result_3, 1e-7); - } -} - -test "vec.dist2" { - { - const a = Vec4{ 0, 0, 0, 0 }; - const b = Vec4{ 0, 0, 0, 0 }; - try expect(vec.dist2(a, b) == 0); - } - - { - const a = Vec2{ 1, 1 }; - try expect(vec.dist2(a, a) == 0); - } - - { - const a = Vec2{ 1, 2 }; - const b = Vec2{ 3, 4 }; - try expect(vec.dist2(a, b) == 8); - } - - { - const a = Vec3{ -1, -2, -3 }; - const b = Vec3{ 3, 2, 1 }; - try expect(vec.dist2(a, b) == 48); - } - - { - const a = Vec4{ 1.5, 2.25, 3.33, 4.44 }; - const b = Vec4{ 1.44, -9.33, 7.25, -0.5 }; - try expectApproxEqAbs(vec.dist2(a, b), 173.87, 1e-8); - } -} - -test "vec.dist" { - { - const a = Vec4{ 0, 0, 0, 0 }; - const b = Vec4{ 0, 0, 0, 0 }; - try expect(vec.dist(a, b) == 0); - } - - { - const a = Vec2{ 1, 1 }; - try expect(vec.dist(a, a) == 0); - } - - { - const a = Vec2{ 1, 2 }; - const b = Vec2{ 4, 6 }; - try expectEqual(vec.dist(a, b), 5); - } - - { - const a = Vec3{ -1, -2, -3 }; - const b = Vec3{ 3, 2, -1 }; - try expect(vec.dist(a, b) == 6); - } - - { - const a = Vec4{ 1.5, 2.25, 3.33, 4.44 }; - const b = Vec4{ 1.44, -9.33, 7.25, -0.5 }; - try expectApproxEqAbs(vec.dist(a, b), 13.18597740025364975978, 1e-8); - } -} - -test "vec.lerp" { - { - const a = Vec4{ 1, 1, 1, 1 }; - const b = Vec4{ 0, 0, 0, 0 }; - const lerp_to_a = vec.lerp(a, b, 0.0); - try expectEqual(lerp_to_a[0], a[0]); - try expectEqual(lerp_to_a[1], a[1]); - try expectEqual(lerp_to_a[2], a[2]); - try expectEqual(lerp_to_a[3], a[3]); - - const lerp_to_b = vec.lerp(a, b, 1.0); - try expectEqual(lerp_to_b[0], b[0]); - try expectEqual(lerp_to_b[1], b[1]); - try expectEqual(lerp_to_b[2], b[2]); - try expectEqual(lerp_to_b[3], b[3]); - - const lerp_to_mid = vec.lerp(a, b, 0.5); - try expectEqual(lerp_to_mid[0], 0.5); - try expectEqual(lerp_to_mid[1], 0.5); - try expectEqual(lerp_to_mid[2], 0.5); - try expectEqual(lerp_to_mid[3], 0.5); - } -} - -test "vec.cross" { - { - const a = Vec3{ 1, 3, 4 }; - const b = Vec3{ 2, -5, 8 }; - const cross = vec.cross(a, b); - try expectEqual(cross[0], 44); - try expectEqual(cross[1], 0); - try expectEqual(cross[2], -11); - } - { - const a = Vec3{ 1.0, 0.0, 0.0 }; - const b = Vec3{ 0.0, 1.0, 0.0 }; - const cross = vec.cross(a, b); - try expectEqual(cross[0], 0.0); - try expectEqual(cross[1], 0.0); - try expectEqual(cross[2], 1.0); - } - { - const a = Vec3{ 1.0, 0.0, 0.0 }; - const b = Vec3{ 0.0, -1.0, 0.0 }; - const cross = vec.cross(a, b); - try expectEqual(cross[0], 0.0); - try expectEqual(cross[1], 0.0); - try expectEqual(cross[2], -1.0); - } - { - const a = Vec3{ -3.0, 0.0, -2.0 }; - const b = Vec3{ 5.0, -1.0, 2.0 }; - const cross = vec.cross(a, b); - try expectEqual(cross[0], -2.0); - try expectEqual(cross[1], -4.0); - try expectEqual(cross[2], 3.0); - } -} - -test "vec.dot" { - { - const a = Vec2{ -1, 2 }; - const b = Vec2{ 4, 5 }; - const dot = vec.dot(a, b); - try expectEqual(dot, 6); - } - { - const a = Vec3{ -1.0, 2.0, 3.0 }; - const b = Vec3{ 4.0, 5.0, 6.0 }; - const dot = vec.dot(a, b); - try expectEqual(dot, 24.0); - } - { - const a = Vec4{ -1.0, 2.0, 3.0, -2.0 }; - const b = Vec4{ 4.0, 5.0, 6.0, 2.0 }; - const dot = vec.dot(a, b); - try expectEqual(dot, 20.0); - } - - { - const a = Vec4{ 0, 0, 0, 0 }; - const b = Vec4{ 0, 0, 0, 0 }; - const dot = vec.dot(a, b); - try expectEqual(dot, 0.0); - } -} - -/// Matrix operations -pub const mat = struct { - pub inline fn init(comptime T: type, v: anytype) T { - return if (T == Mat3x3) .{ - .{ v[0], v[1], v[2], v[3] }, - .{ v[4], v[5], v[6], v[7] }, - .{ v[8], v[9], v[10], v[11] }, - } else if (T == Mat4x4) .{ - .{ v[0], v[1], v[2], v[3] }, - .{ v[4], v[5], v[6], v[7] }, - .{ v[8], v[9], v[10], v[11] }, - .{ v[12], v[13], v[14], v[15] }, - } else @compileError("Expected matrix, found '" ++ @typeName(T) ++ "'"); - } - - pub inline fn index(a: anytype, i: u8) f32 { - const T = @TypeOf(a); - const columns = vec.size(if (T == Mat3x3) Vec4 else if (T == Mat4x4) Vec4 else @compileError("Expected matrix, found '" ++ @typeName(T) ++ "'")); - return a[(i / columns)][(i % columns)]; - } - - /// Constructs an identity matrix of type T. - pub inline fn identity(comptime T: type) T { - return if (T == Mat3x3) init(Mat3x3, .{ - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - }) else if (T == Mat4x4) init(Mat4x4, .{ - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1, - }) else @compileError("Expected matrix, found '" ++ @typeName(T) ++ "'"); - } - - /// Constructs an orthographic projection matrix; an orthogonal transformation matrix which - /// transforms from the given left, right, bottom, and top dimensions into -1 +1 in x and y, - /// and 0 to +1 in z. - /// - /// The near/far parameters denotes the depth (z coordinate) of the near/far clipping plane. - /// - /// Returns an orthographic projection matrix. - pub inline fn ortho( - /// The sides of the near clipping plane viewport - left: f32, - right: f32, - bottom: f32, - top: f32, - /// The depth (z coordinate) of the near/far clipping plane. - near: f32, - far: f32, - ) Mat4x4 { - const xx = 2 / (right - left); - const yy = 2 / (top - bottom); - const zz = 1 / (near - far); - const tx = (right + left) / (left - right); - const ty = (top + bottom) / (bottom - top); - const tz = near / (near - far); - return init(Mat4x4, .{ - xx, 0, 0, 0, - 0, yy, 0, 0, - 0, 0, zz, 0, - tx, ty, tz, 1, - }); - } - - /// Constructs a 2D matrix which translates coordinates by v. - pub inline fn translate2d(v: Vec2) Mat3x3 { - return init(Mat3x3, .{ - 1, 0, 0, 0, - 0, 1, 0, 0, - v[0], v[1], 1, 0, - }); - } - - /// Constructs a 3D matrix which translates coordinates by v. - pub inline fn translate3d(v: Vec3) Mat4x4 { - return init(Mat4x4, .{ - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - v[0], v[1], v[2], 1, - }); - } - - /// Returns the translation component of the 2D matrix. - pub inline fn translation2d(v: Mat3x3) Vec2 { - return .{ mat.index(v, 8), mat.index(v, 9) }; - } - - /// Returns the translation component of the 3D matrix. - pub inline fn translation3d(v: Mat4x4) Vec3 { - return .{ mat.index(v, 12), mat.index(v, 13), mat.index(v, 14) }; - } - - /// Constructs a 3D matrix which scales each dimension by the given vector. - pub inline fn scale3d(v: Vec3) Mat4x4 { - return init(Mat4x4, .{ - v[0], 0, 0, 0, - 0, v[1], 0, 0, - 0, 0, v[2], 0, - 0, 0, 0, 1, - }); - } - - /// Constructs a 3D matrix which scales each dimension by the given vector. - pub inline fn scale2d(v: Vec2) Mat3x3 { - return init(Mat3x3, .{ - v[0], 0, 0, 0, - 0, v[1], 0, 0, - 0, 0, 1, 0, - }); - } - - // Multiplies matrices a * b - pub inline fn mul(a: anytype, b: @TypeOf(a)) @TypeOf(a) { - return if (@TypeOf(a) == Mat3x3) { - const a00 = a[0][0]; - const a01 = a[0][1]; - const a02 = a[0][2]; - const a10 = a[1][0]; - const a11 = a[1][1]; - const a12 = a[1][2]; - const a20 = a[2][0]; - const a21 = a[2][1]; - const a22 = a[2][2]; - const b00 = b[0][0]; - const b01 = b[0][1]; - const b02 = b[0][2]; - const b10 = b[1][0]; - const b11 = b[1][1]; - const b12 = b[1][2]; - const b20 = b[2][0]; - const b21 = b[2][1]; - const b22 = b[2][2]; - return init(Mat3x3, .{ - a00 * b00 + a10 * b01 + a20 * b02, - a01 * b00 + a11 * b01 + a21 * b02, - a02 * b00 + a12 * b01 + a22 * b02, - a00 * b10 + a10 * b11 + a20 * b12, - a01 * b10 + a11 * b11 + a21 * b12, - a02 * b10 + a12 * b11 + a22 * b12, - a00 * b20 + a10 * b21 + a20 * b22, - a01 * b20 + a11 * b21 + a21 * b22, - a02 * b20 + a12 * b21 + a22 * b22, - }); - } else if (@TypeOf(a) == Mat4x4) { - const a00 = a[0][0]; - const a01 = a[0][1]; - const a02 = a[0][2]; - const a03 = a[0][3]; - const a10 = a[1][0]; - const a11 = a[1][1]; - const a12 = a[1][2]; - const a13 = a[1][3]; - const a20 = a[2][0]; - const a21 = a[2][1]; - const a22 = a[2][2]; - const a23 = a[2][3]; - const a30 = a[3][0]; - const a31 = a[3][1]; - const a32 = a[3][2]; - const a33 = a[3][3]; - const b00 = b[0][0]; - const b01 = b[0][1]; - const b02 = b[0][2]; - const b03 = b[0][3]; - const b10 = b[1][0]; - const b11 = b[1][1]; - const b12 = b[1][2]; - const b13 = b[1][3]; - const b20 = b[2][0]; - const b21 = b[2][1]; - const b22 = b[2][2]; - const b23 = b[2][3]; - const b30 = b[3][0]; - const b31 = b[3][1]; - const b32 = b[3][2]; - const b33 = b[3][3]; - return init(Mat4x4, .{ - a00 * b00 + a10 * b01 + a20 * b02 + a30 * b03, - a01 * b00 + a11 * b01 + a21 * b02 + a31 * b03, - a02 * b00 + a12 * b01 + a22 * b02 + a32 * b03, - a03 * b00 + a13 * b01 + a23 * b02 + a33 * b03, - a00 * b10 + a10 * b11 + a20 * b12 + a30 * b13, - a01 * b10 + a11 * b11 + a21 * b12 + a31 * b13, - a02 * b10 + a12 * b11 + a22 * b12 + a32 * b13, - a03 * b10 + a13 * b11 + a23 * b12 + a33 * b13, - a00 * b20 + a10 * b21 + a20 * b22 + a30 * b23, - a01 * b20 + a11 * b21 + a21 * b22 + a31 * b23, - a02 * b20 + a12 * b21 + a22 * b22 + a32 * b23, - a03 * b20 + a13 * b21 + a23 * b22 + a33 * b23, - a00 * b30 + a10 * b31 + a20 * b32 + a30 * b33, - a01 * b30 + a11 * b31 + a21 * b32 + a31 * b33, - a02 * b30 + a12 * b31 + a22 * b32 + a32 * b33, - a03 * b30 + a13 * b31 + a23 * b32 + a33 * b33, - }); - } else @compileError("Expected matrix, found '" ++ @typeName(@TypeOf(a)) ++ "'"); - } - - /// Check if two matrices are approximate equal. Returns true if the absolute difference between - /// each element in matrix them is less or equal than the specified tolerance. - pub inline fn equals(a: anytype, b: @TypeOf(a), tolerance: f32) bool { - // TODO: leverage a vec.equals function - return if (@TypeOf(a) == Mat3x3) { - return float.equals(f32, a[0][0], b[0][0], tolerance) and - float.equals(f32, a[0][1], b[0][1], tolerance) and - float.equals(f32, a[0][2], b[0][2], tolerance) and - float.equals(f32, a[0][3], b[0][3], tolerance) and - float.equals(f32, a[1][0], b[1][0], tolerance) and - float.equals(f32, a[1][1], b[1][1], tolerance) and - float.equals(f32, a[1][2], b[1][2], tolerance) and - float.equals(f32, a[1][3], b[1][3], tolerance) and - float.equals(f32, a[2][0], b[2][0], tolerance) and - float.equals(f32, a[2][1], b[2][1], tolerance) and - float.equals(f32, a[2][2], b[2][2], tolerance) and - float.equals(f32, a[2][3], b[2][3], tolerance); - } else if (@TypeOf(a) == Mat4x4) { - return float.equals(f32, a[0][0], b[0][0], tolerance) and - float.equals(f32, a[0][1], b[0][1], tolerance) and - float.equals(f32, a[0][2], b[0][2], tolerance) and - float.equals(f32, a[0][3], b[0][3], tolerance) and - float.equals(f32, a[1][0], b[1][0], tolerance) and - float.equals(f32, a[1][1], b[1][1], tolerance) and - float.equals(f32, a[1][2], b[1][2], tolerance) and - float.equals(f32, a[1][3], b[1][3], tolerance) and - float.equals(f32, a[2][0], b[2][0], tolerance) and - float.equals(f32, a[2][1], b[2][1], tolerance) and - float.equals(f32, a[2][2], b[2][2], tolerance) and - float.equals(f32, a[2][3], b[2][3], tolerance) and - float.equals(f32, a[3][0], b[3][0], tolerance) and - float.equals(f32, a[3][1], b[3][1], tolerance) and - float.equals(f32, a[3][2], b[3][2], tolerance) and - float.equals(f32, a[3][3], b[3][3], tolerance); - } else @compileError("Expected matrix, found '" ++ @typeName(@TypeOf(a)) ++ "'"); - } - - /// Constructs a 3D matrix which rotates around the X axis by `angle_radians`. - pub inline fn rotateX(angle_radians: f32) Mat4x4 { - const c = std.math.cos(angle_radians); - const s = std.math.sin(angle_radians); - - return init(Mat4x4, .{ - 1, 0, 0, 0, - 0, c, s, 0, - 0, -s, c, 0, - 0, 0, 0, 1, - }); - } - - /// Constructs a 3D matrix which rotates around the X axis by `angle_radians`. - pub inline fn rotateY(angle_radians: f32) Mat4x4 { - const c = std.math.cos(angle_radians); - const s = std.math.sin(angle_radians); - - return init(Mat4x4, .{ - c, 0, -s, 0, - 0, 1, 0, 0, - s, 0, c, 0, - 0, 0, 0, 1, - }); - } - - /// Constructs a 3D matrix which rotates around the Z axis by `angle_radians`. - pub inline fn rotateZ(angle_radians: f32) Mat4x4 { - const c = std.math.cos(angle_radians); - const s = std.math.sin(angle_radians); - - return init(Mat4x4, .{ - c, s, 0, 0, - -s, c, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1, - }); - } -}; - -test "mat.identity" { - { - const identity_4x4: Mat4x4 = mat.identity(Mat4x4); - var row: u8 = 0; - while (row < 4) { - var column: u8 = 0; - while (column < 4) { - var value: f32 = if (row == column) 1 else 0; - try expect(identity_4x4[row][column] == value); - - column += 1; - } - row += 1; - } - } - - { - const identity_3x3: Mat3x3 = mat.identity(Mat3x3); - var row: u8 = 0; - while (row < 3) { - var column: u8 = 0; - while (column < 4) { - var value: f32 = if (row == column) 1 else 0; - try expect(identity_3x3[row][column] == value); - - column += 1; - } - row += 1; - } - } -} - -test "mat.ortho" { - const ortho_mat = mat.ortho(-2, 2, -2, 3, 10, 110); - - // Computed Values - try expectEqual(ortho_mat[0][0], 0.5); - try expectEqual(ortho_mat[1][1], 0.4); - try expectEqual(ortho_mat[2][2], -0.01); - try expectEqual(ortho_mat[3][0], 0); - try expectEqual(ortho_mat[3][1], -0.2); - try expectEqual(ortho_mat[3][2], -0.1); - - // Constant values, which should not change but we still check for completeness - const zero_value_indexes = [_]u8{ - 1, 2, 3, - 4, 4 + 2, 4 + 3, - 4 * 2, 4 * 2 + 1, 4 * 2 + 3, - }; - for (zero_value_indexes) |index| { - try expectEqual(mat.index(ortho_mat, index), 0); - } - try expectEqual(ortho_mat[3][3], 1); -} - -test "mat.translate2d" { - const v = Vec2{ 1.0, -2.5 }; - const translation_mat = mat.translate2d(v); - - // Computed Values - try expectEqual(translation_mat[2][0], v[0]); - try expectEqual(translation_mat[2][1], v[1]); - - // Constant values, which should not change but we still check for completeness - const zero_value_indexes = [_]u8{ - 1, 2, 3, - 4, 4 + 2, 4 + 3, - 4 * 2 + 3, - }; - for (zero_value_indexes) |index| { - try expectEqual(mat.index(translation_mat, index), 0); - } - try expectEqual(translation_mat[0][0], 1); - try expectEqual(translation_mat[1][1], 1); - try expectEqual(translation_mat[2][2], 1); -} - -test "mat.translate3d" { - const v = Vec3{ 1.0, -2.5, 0.001 }; - const translation_mat = mat.translate3d(v); - - // Computed Values - try expectEqual(translation_mat[3][0], v[0]); - try expectEqual(translation_mat[3][1], v[1]); - try expectEqual(translation_mat[3][2], v[2]); - - // Constant values, which should not change but we still check for completeness - const zero_value_indexes = [_]u8{ - 1, 2, 3, - 4, 4 + 2, 4 + 3, - 4 * 2, 4 * 2 + 1, 4 * 2 + 3, - }; - for (zero_value_indexes) |index| { - try expectEqual(mat.index(translation_mat, index), 0); - } - try expectEqual(translation_mat[3][3], 1); -} - -test "mat.translation" { - { - const v = Vec2{ 1.0, -2.5 }; - const translation_mat = mat.translate2d(v); - const result = mat.translation2d(translation_mat); - try expectEqual(result[0], v[0]); - try expectEqual(result[1], v[1]); - } - - { - const v = Vec3{ 1.0, -2.5, 0.001 }; - const translation_mat = mat.translate3d(v); - const result = mat.translation3d(translation_mat); - try expectEqual(result[0], v[0]); - try expectEqual(result[1], v[1]); - try expectEqual(result[2], v[2]); - } -} - -test "mat.scale2d" { - const v = Vec2{ 1.0, -2.5 }; - const scale_mat = mat.scale2d(v); - - // Computed Values - try expectEqual(scale_mat[0][0], v[0]); - try expectEqual(scale_mat[1][1], v[1]); - - // Constant values, which should not change but we still check for completeness - const zero_value_indexes = [_]u8{ - 1, 2, 3, - 4, 4 + 2, 4 + 3, - 4 * 2, 4 * 2 + 1, 4 * 2 + 3, - }; - for (zero_value_indexes) |index| { - try expectEqual(mat.index(scale_mat, index), 0); - } - try expectEqual(scale_mat[2][2], 1); -} - -test "mat.scale3d" { - const v = Vec3{ 1.0, -2.5, 0.001 }; - const scale_mat = mat.scale3d(v); - - // Computed Values - try expectEqual(scale_mat[0][0], v[0]); - try expectEqual(scale_mat[1][1], v[1]); - try expectEqual(scale_mat[2][2], v[2]); - - // Constant values, which should not change but we still check for completeness - const zero_value_indexes = [_]u8{ - 1, 2, 3, - 4, 4 + 2, 4 + 3, - 4 * 2, 4 * 2 + 1, 4 * 2 + 3, - 4 * 3, 4 * 3 + 1, 4 * 3 + 2, - }; - for (zero_value_indexes) |index| { - try expectEqual(mat.index(scale_mat, index), 0); - } - try expectEqual(scale_mat[3][3], 1); -} - -const degreesToRadians = std.math.degreesToRadians; - -// TODO: Maybe reconsider based on feedback to join all test for rotation into one test as only -// location of values change. And create some kind of struct that will hold this indexes and -// coresponding values -test "mat.rotateX" { - const zero_value_indexes = [_]u8{ - 1, 2, 3, - 4, 4 + 3, 4 * 2, - 4 * 2 + 3, 4 * 3, 4 * 3 + 1, - 4 * 3 + 2, - }; - - const one_value_indexes = [_]u8{ - 0, 4 * 3 + 3, - }; - - const tolerance = 1e-7; - - { - const r = 90; - const R_x = mat.rotateX(degreesToRadians(f32, r)); - try expectApproxEqAbs(R_x[1][1], 0, tolerance); - try expectApproxEqAbs(R_x[2][2], 0, tolerance); - try expectApproxEqAbs(R_x[1][2], 1, tolerance); - try expectApproxEqAbs(R_x[2][1], -1, tolerance); - - for (zero_value_indexes) |index| { - try expectEqual(mat.index(R_x, index), 0); - } - - for (one_value_indexes) |index| { - try expectEqual(mat.index(R_x, index), 1); - } - } - - { - const r = 0; - const R_x = mat.rotateX(degreesToRadians(f32, r)); - try expectApproxEqAbs(R_x[1][1], 1, tolerance); - try expectApproxEqAbs(R_x[2][2], 1, tolerance); - try expectApproxEqAbs(R_x[1][2], 0, tolerance); - try expectApproxEqAbs(R_x[2][1], 0, tolerance); - - for (zero_value_indexes) |index| { - try expectEqual(mat.index(R_x, index), 0); - } - - for (one_value_indexes) |index| { - try expectEqual(mat.index(R_x, index), 1); - } - } - - { - const r = 45; - const result: f32 = std.math.sqrt(2.0) / 2.0; // sqrt(2) / 2 - const R_x = mat.rotateX(degreesToRadians(f32, r)); - try expectApproxEqAbs(R_x[1][1], result, tolerance); - try expectApproxEqAbs(R_x[2][2], result, tolerance); - try expectApproxEqAbs(R_x[1][2], result, tolerance); - try expectApproxEqAbs(R_x[2][1], -result, tolerance); - - for (zero_value_indexes) |index| { - try expectEqual(mat.index(R_x, index), 0); - } - - for (one_value_indexes) |index| { - try expectEqual(mat.index(R_x, index), 1); - } - } -} - -test "mat.rotateY" { - const zero_value_indexes = [_]u8{ - 1, 3, - 4, 4 + 2, - 4 + 3, 4 * 2 + 1, - 4 * 2 + 3, 4 * 3, - 4 * 3 + 1, 4 * 3 + 2, - }; - - const one_value_indexes = [_]u8{ - 4 + 1, 4 * 3 + 3, - }; - - const tolerance = 1e-7; - - { - const r = 90; - const R_y = mat.rotateY(degreesToRadians(f32, r)); - try expectApproxEqAbs(R_y[0][0], 0, tolerance); - try expectApproxEqAbs(R_y[2][2], 0, tolerance); - try expectApproxEqAbs(R_y[0][2], -1, tolerance); - try expectApproxEqAbs(R_y[2][0], 1, tolerance); - - for (zero_value_indexes) |index| { - try expectEqual(mat.index(R_y, index), 0); - } - - for (one_value_indexes) |index| { - try expectEqual(mat.index(R_y, index), 1); - } - } - - { - const r = 0; - const R_y = mat.rotateY(degreesToRadians(f32, r)); - try expectApproxEqAbs(R_y[0][0], 1, tolerance); - try expectApproxEqAbs(R_y[2][2], 1, tolerance); - try expectApproxEqAbs(R_y[0][2], 0, tolerance); - try expectApproxEqAbs(R_y[3][0], 0, tolerance); // TODO: [2][0] ? - - for (zero_value_indexes) |index| { - try expectEqual(mat.index(R_y, index), 0); - } - - for (one_value_indexes) |index| { - try expectEqual(mat.index(R_y, index), 1); - } - } - - { - const r = 45; - const result: f32 = std.math.sqrt(2.0) / 2.0; // sqrt(2) / 2 - const R_y = mat.rotateY(degreesToRadians(f32, r)); - try expectApproxEqAbs(R_y[0][0], result, tolerance); - try expectApproxEqAbs(R_y[2][2], result, tolerance); - try expectApproxEqAbs(R_y[0][2], -result, tolerance); - try expectApproxEqAbs(R_y[2][0], result, tolerance); - - for (zero_value_indexes) |index| { - try expectEqual(mat.index(R_y, index), 0); - } - - for (one_value_indexes) |index| { - try expectEqual(mat.index(R_y, index), 1); - } - } -} - -test "mat.rotateZ" { - const zero_value_indexes = [_]u8{ - 2, 3, - 4 + 2, 4 + 3, - 4 * 2, 4 * 2 + 1, - 4 * 2 + 3, 4 * 3, - 4 * 3 + 1, 4 * 3 + 2, - }; - - const one_value_indexes = [_]u8{ - 4 * 2 + 2, 4 * 3 + 3, - }; - - const tolerance = 1e-7; - - { - const r = 90; - const R_z = mat.rotateZ(degreesToRadians(f32, r)); - try expectApproxEqAbs(R_z[0][0], 0, tolerance); - try expectApproxEqAbs(R_z[1][1], 0, tolerance); - try expectApproxEqAbs(R_z[0][1], 1, tolerance); - try expectApproxEqAbs(R_z[1][0], -1, tolerance); - - for (zero_value_indexes) |index| { - try expectEqual(mat.index(R_z, index), 0); - } - - for (one_value_indexes) |index| { - try expectEqual(mat.index(R_z, index), 1); - } - } - - { - const r = 0; - const R_z = mat.rotateZ(degreesToRadians(f32, r)); - try expectApproxEqAbs(R_z[0][0], 1, tolerance); - try expectApproxEqAbs(R_z[1][1], 1, tolerance); - try expectApproxEqAbs(R_z[0][1], 0, tolerance); - try expectApproxEqAbs(R_z[1][0], 0, tolerance); - - for (zero_value_indexes) |index| { - try expectEqual(mat.index(R_z, index), 0); - } - - for (one_value_indexes) |index| { - try expectEqual(mat.index(R_z, index), 1); - } - } - - { - const r = 45; - const result: f32 = std.math.sqrt(2.0) / 2.0; // sqrt(2) / 2 - const R_z = mat.rotateZ(degreesToRadians(f32, r)); - try expectApproxEqAbs(R_z[0][0], result, tolerance); - try expectApproxEqAbs(R_z[1][1], result, tolerance); - try expectApproxEqAbs(R_z[0][1], result, tolerance); - try expectApproxEqAbs(R_z[1][0], -result, tolerance); - - for (zero_value_indexes) |index| { - try expectEqual(mat.index(R_z, index), 0); - } - - for (one_value_indexes) |index| { - try expectEqual(mat.index(R_z, index), 1); - } - } -} - -test "mat.mul" { - { - const tolerance = 1e-6; - const t = Vec3{ 1, 2, -3 }; - const T = mat.translate3d(t); - const s = Vec3{ 3, 1, -5 }; - const S = mat.scale3d(s); - const r = Vec3{ 30, -40, 235 }; - const R_x = mat.rotateX(degreesToRadians(f32, r[0])); - const R_y = mat.rotateY(degreesToRadians(f32, r[1])); - const R_z = mat.rotateZ(degreesToRadians(f32, r[2])); - - const R_yz = mat.mul(R_y, R_z); - // This values are calculated by hand with help of matrix calculator: https://matrix.reshish.com/multCalculation.php - const expected_R_yz = mat.init(Mat4x4, .{ - -0.43938504177070496278, -0.8191520442889918, -0.36868782649461236545, 0, - 0.62750687159713312638, -0.573576436351046, 0.52654078451836329713, 0, - -0.6427876096865394, 0, 0.766044443118978, 0, - 0, 0, 0, 1, - }); - try expect(mat.equals(R_yz, expected_R_yz, tolerance)); - - const R_xyz = mat.mul(R_x, R_yz); - const expected_R_xyz = mat.init(Mat4x4, .{ - -0.439385041770705, -0.52506256666891627986, -0.72886904595489960019, 0, - 0.6275068715971331, -0.76000215715133560834, 0.16920947734596765363, 0, - -0.6427876096865394, -0.383022221559489, 0.66341394816893832989, 0, - 0, 0, 0, 1, - }); - try expect(mat.equals(R_xyz, expected_R_xyz, tolerance)); - - const SR = mat.mul(S, R_xyz); - const expected_SR = mat.init(Mat4x4, .{ - -1.318155125312115, -0.5250625666689163, 3.6443452297744985, 0, - 1.8825206147913993, -0.7600021571513356, -0.8460473867298382, 0, - -1.9283628290596182, -0.383022221559489, -3.3170697408446915, 0, - 0, 0, 0, 1, - }); - try expect(mat.equals(SR, expected_SR, tolerance)); - - const TSR = mat.mul(T, SR); - const expected_TSR = mat.init(Mat4x4, .{ - -1.318155125312115, -0.5250625666689163, 3.6443452297744985, 0, - 1.8825206147913993, -0.7600021571513356, -0.8460473867298382, 0, - -1.9283628290596182, -0.383022221559489, -3.3170697408446914, 0, - 1, 2, -3, 1, - }); - - try expect(mat.equals(TSR, expected_TSR, tolerance)); - } -} - -test "gpu_compatibility" { - // https://www.w3.org/TR/WGSL/#alignment-and-size - try expectEqual(8, @sizeOf(Vec2)); - try expectEqual(16, @sizeOf(Vec3)); // WGSL SizeOf 12 - try expectEqual(16, @sizeOf(Vec4)); - try expectEqual(48, @sizeOf(Mat3x3)); - try expectEqual(64, @sizeOf(Mat4x4)); - - try expectEqual(8, @alignOf(Vec2)); - try expectEqual(16, @alignOf(Vec3)); - try expectEqual(16, @alignOf(Vec4)); - try expectEqual(16, @alignOf(Mat3x3)); - try expectEqual(16, @alignOf(Mat4x4)); -} diff --git a/src/math/main.zig b/src/math/main.zig new file mode 100644 index 00000000..4b365a8d --- /dev/null +++ b/src/math/main.zig @@ -0,0 +1,126 @@ +//! # mach/math is opinionated +//! +//! Math is hard enough as-is, without you having to question ground truths while problem solving. +//! As a result, mach/math takes a more opinionated approach than some other math libraries: we try +//! to encourage you through API design to use what we believe to be the best choices. For example, +//! other math libraries provide both LH and RH (left-handed and right-handed) variants for each +//! operation, and they sit on equal footing for you to choose from; mach/math may also provide both +//! variants as needed for conversions, but unlike other libraries will bless one choice with e.g. +//! a shorter function name to nudge you in the right direction and towards _consistency_. +//! +//! ## Matrices +//! +//! * Column-major matrix storage +//! * Column-vectors (i.e. right-associative multiplication, matrix * vector = vector) +//! +//! The benefit of using this "OpenGL-style" matrix is that it matches the conventions accepted by +//! the scientific community, it's what you'll find in linear algebra textbooks. It also matches +//! WebGPU, Vulkan, Unity3D, etc. It does NOT match DirectX-style which e.g. Unreal Engine uses. +//! +//! Note: many people will say "row major" or "column major" and implicitly mean three or more +//! different concepts; to avoid confusion we'll go over this in more depth below. +//! +//! ## Coordinate system (+Y up, left-handed) +//! +//! * Normalized Device coordinates: +Y up; (-1, -1) is at the bottom-left corner. +//! * Framebuffer coordinates: +Y down; (0, 0) is at the top-left corner. +//! * Texture coordinates: +Y down; (0, 0) is at the top-left corner. +//! +//! This coordinate system is consistent with WebGPU, Metal, DirectX, and Unity (NDC only.) +//! +//! Note that since +Y is up (not +Z), developers can seamlessly transition from 2D applications +//! to 3D applications by adding the Z component. This is in contrast to e.g. Z-up coordinate +//! systems, where 2D and 3D must differ. +//! +//! ## Additional reading +//! +//! * [Coordinate system explainer](https://machengine.org/engine/math/coordinate-system/) +//! * [Matrix storage explainer](https://machengine.org/engine/math/matrix-storage/) +//! + +const std = @import("std"); +const testing = std.testing; + +const vec = @import("vec.zig"); +const mat = @import("mat.zig"); +const q = @import("quat.zig"); + +/// Standard f32 precision types +pub const Vec2 = vec.Vec(2, f32); +pub const Vec3 = vec.Vec(3, f32); +pub const Vec4 = vec.Vec(4, f32); +pub const Quat = q.Quat(f32); +pub const Mat3x3 = mat.Mat(3, 3, Vec4); +pub const Mat4x4 = mat.Mat(4, 4, Vec4); + +/// Half-precision f16 types +pub const Vec2h = vec.Vec(2, f16); +pub const Vec3h = vec.Vec(3, f16); +pub const Vec4h = vec.Vec(4, f16); +pub const Quath = q.Quat(f16); +pub const Mat3x3h = mat.Mat(3, 3, Vec4h); +pub const Mat4x4h = mat.Mat(4, 4, Vec4h); + +/// Double-precision f64 types +pub const Vec2d = vec.Vec(2, f64); +pub const Vec3d = vec.Vec(3, f64); +pub const Vec4d = vec.Vec(4, f64); +pub const Quatd = q.Quat(f64); +pub const Mat3x3d = mat.Mat(3, 3, Vec4d); +pub const Mat4x4d = mat.Mat(4, 4, Vec4d); + +/// Standard f32 precision initializers +pub const vec2 = Vec2.init; +pub const vec3 = Vec3.init; +pub const vec4 = Vec4.init; +pub const quat = Quat.init; +pub const mat3x3 = Mat3x3.init; +pub const mat4x4 = Mat4x4.init; + +/// Half-precision f16 initializers +pub const vec2h = Vec2h.init; +pub const vec3h = Vec3h.init; +pub const vec4h = Vec4h.init; +pub const quath = Quath.init; +pub const mat3x3h = Mat3x3h.init; +pub const mat4x4h = Mat4x4h.init; + +/// Double-precision f64 initializers +pub const vec2d = Vec2d.init; +pub const vec3d = Vec3d.init; +pub const vec4d = Vec4d.init; +pub const quatd = Quatd.init; +pub const mat3x3d = Mat3x3d.init; +pub const mat4x4d = Mat4x4d.init; + +test { + testing.refAllDeclsRecursive(@This()); +} + +// std.math customizations +pub const eql = std.math.approxEqAbs; +pub const eps = std.math.floatEps; +pub const eps_f16 = std.math.floatEps(f16); +pub const eps_f32 = std.math.floatEps(f32); +pub const eps_f64 = std.math.floatEps(f64); +pub const nan_f16 = std.math.nan(f16); +pub const nan_f32 = std.math.nan(f32); +pub const nan_f64 = std.math.nan(f64); + +// std.math 1:1 re-exports below here +// +// Having two 'math' imports in your code is annoying, so we in general expect that people will not +// need to do this and instead can just import mach.math - we add to this list of re-exports as +// needed. + +pub const sqrt = std.math.sqrt; +pub const isNan = std.math.isNan; + +/// 2/sqrt(π) +pub const two_sqrtpi = std.math.two_sqrtpi; + +/// sqrt(2) +pub const sqrt2 = std.math.sqrt2; + +/// 1/sqrt(2) +pub const sqrt1_2 = std.math.sqrt1_2; diff --git a/src/math/mat.zig b/src/math/mat.zig new file mode 100644 index 00000000..ec304990 --- /dev/null +++ b/src/math/mat.zig @@ -0,0 +1,773 @@ +const std = @import("std"); + +const mach = @import("../main.zig"); +const testing = mach.testing; +const math = mach.math; +const vec = @import("vec.zig"); + +pub fn Mat( + comptime n_cols: usize, + comptime n_rows: usize, + comptime Vector: type, +) type { + return struct { + v: [cols]Vec, + + /// The number of columns, e.g. Mat3x4.cols == 3 + pub const cols = n_cols; + + /// The number of rows, e.g. Mat3x4.rows == 4 + pub const rows = n_rows; + + /// The scalar type of this matrix, e.g. Mat3x3.T == f32 + pub const T = Vector.T; + + /// The underlying Vec type, e.g. Mat3x3.Vec == Vec4 + pub const Vec = Vector; + + /// The Vec type corresponding to the number of rows, e.g. Mat3x3.RowVec == Vec3 + pub const RowVec = vec.Vec(rows, T); + + const Matrix = @This(); + + /// Identity matrix + pub const ident = switch (Matrix) { + inline math.Mat3x3, math.Mat3x3h, math.Mat3x3d => Matrix.init( + RowVec.init(1, 0, 0), + RowVec.init(0, 1, 0), + RowVec.init(0, 0, 1), + ), + inline math.Mat4x4, math.Mat4x4h, math.Mat4x4d => Matrix.init( + Vec.init(1, 0, 0, 0), + Vec.init(0, 1, 0, 0), + Vec.init(0, 0, 1, 0), + Vec.init(0, 0, 0, 1), + ), + else => @compileError("Expected Mat3x3, Mat4x4 found '" ++ @typeName(Matrix) ++ "'"), + }; + + pub usingnamespace switch (Matrix) { + inline math.Mat3x3, math.Mat3x3h, math.Mat3x3d => struct { + pub inline fn init( + col0: RowVec, + col1: RowVec, + col2: RowVec, + ) Matrix { + return .{ .v = [_]Vec{ + Vec.init(col0.x(), col0.y(), col0.z(), 1), + Vec.init(col1.x(), col1.y(), col1.z(), 1), + Vec.init(col2.x(), col2.y(), col2.z(), 1), + } }; + } + }, + inline math.Mat4x4, math.Mat4x4h, math.Mat4x4d => struct { + pub inline fn init(col0: Vec, col1: Vec, col2: Vec, col3: Vec) Matrix { + return .{ .v = [_]Vec{ + col0, + col1, + col2, + col3, + } }; + } + }, + else => @compileError("Expected Mat3x3, Mat4x4 found '" ++ @typeName(Matrix) ++ "'"), + }; + + // TODO: the below code was correct in our old implementation, it just needs to be updated + // to work with this new Mat approach, swapping f32 for the generic T float type, moving 3x3 + // and 4x4 specific functions into the mixin above, writing new tests, etc. + + // /// Constructs an orthographic projection matrix; an orthogonal transformation matrix which + // /// transforms from the given left, right, bottom, and top dimensions into -1 +1 in x and y, + // /// and 0 to +1 in z. + // /// + // /// The near/far parameters denotes the depth (z coordinate) of the near/far clipping plane. + // /// + // /// Returns an orthographic projection matrix. + // pub inline fn ortho( + // /// The sides of the near clipping plane viewport + // left: f32, + // right: f32, + // bottom: f32, + // top: f32, + // /// The depth (z coordinate) of the near/far clipping plane. + // near: f32, + // far: f32, + // ) Mat4x4 { + // const xx = 2 / (right - left); + // const yy = 2 / (top - bottom); + // const zz = 1 / (near - far); + // const tx = (right + left) / (left - right); + // const ty = (top + bottom) / (bottom - top); + // const tz = near / (near - far); + // return init(Mat4x4, .{ + // xx, 0, 0, 0, + // 0, yy, 0, 0, + // 0, 0, zz, 0, + // tx, ty, tz, 1, + // }); + // } + + // /// Constructs a 2D matrix which translates coordinates by v. + // pub inline fn translate2d(v: Vec2) Mat3x3 { + // return init(Mat3x3, .{ + // 1, 0, 0, 0, + // 0, 1, 0, 0, + // v[0], v[1], 1, 0, + // }); + // } + + // /// Constructs a 3D matrix which translates coordinates by v. + // pub inline fn translate3d(v: Vec3) Mat4x4 { + // return init(Mat4x4, .{ + // 1, 0, 0, 0, + // 0, 1, 0, 0, + // 0, 0, 1, 0, + // v[0], v[1], v[2], 1, + // }); + // } + + // /// Returns the translation component of the 2D matrix. + // pub inline fn translation2d(v: Mat3x3) Vec2 { + // return .{ mat.index(v, 8), mat.index(v, 9) }; + // } + + // /// Returns the translation component of the 3D matrix. + // pub inline fn translation3d(v: Mat4x4) Vec3 { + // return .{ mat.index(v, 12), mat.index(v, 13), mat.index(v, 14) }; + // } + + // /// Constructs a 3D matrix which scales each dimension by the given vector. + // pub inline fn scale3d(v: Vec3) Mat4x4 { + // return init(Mat4x4, .{ + // v[0], 0, 0, 0, + // 0, v[1], 0, 0, + // 0, 0, v[2], 0, + // 0, 0, 0, 1, + // }); + // } + + // /// Constructs a 3D matrix which scales each dimension by the given vector. + // pub inline fn scale2d(v: Vec2) Mat3x3 { + // return init(Mat3x3, .{ + // v[0], 0, 0, 0, + // 0, v[1], 0, 0, + // 0, 0, 1, 0, + // }); + // } + + // // Multiplies matrices a * b + // pub inline fn mul(a: anytype, b: @TypeOf(a)) @TypeOf(a) { + // return if (@TypeOf(a) == Mat3x3) { + // const a00 = a[0][0]; + // const a01 = a[0][1]; + // const a02 = a[0][2]; + // const a10 = a[1][0]; + // const a11 = a[1][1]; + // const a12 = a[1][2]; + // const a20 = a[2][0]; + // const a21 = a[2][1]; + // const a22 = a[2][2]; + // const b00 = b[0][0]; + // const b01 = b[0][1]; + // const b02 = b[0][2]; + // const b10 = b[1][0]; + // const b11 = b[1][1]; + // const b12 = b[1][2]; + // const b20 = b[2][0]; + // const b21 = b[2][1]; + // const b22 = b[2][2]; + // return init(Mat3x3, .{ + // a00 * b00 + a10 * b01 + a20 * b02, + // a01 * b00 + a11 * b01 + a21 * b02, + // a02 * b00 + a12 * b01 + a22 * b02, + // a00 * b10 + a10 * b11 + a20 * b12, + // a01 * b10 + a11 * b11 + a21 * b12, + // a02 * b10 + a12 * b11 + a22 * b12, + // a00 * b20 + a10 * b21 + a20 * b22, + // a01 * b20 + a11 * b21 + a21 * b22, + // a02 * b20 + a12 * b21 + a22 * b22, + // }); + // } else if (@TypeOf(a) == Mat4x4) { + // const a00 = a[0][0]; + // const a01 = a[0][1]; + // const a02 = a[0][2]; + // const a03 = a[0][3]; + // const a10 = a[1][0]; + // const a11 = a[1][1]; + // const a12 = a[1][2]; + // const a13 = a[1][3]; + // const a20 = a[2][0]; + // const a21 = a[2][1]; + // const a22 = a[2][2]; + // const a23 = a[2][3]; + // const a30 = a[3][0]; + // const a31 = a[3][1]; + // const a32 = a[3][2]; + // const a33 = a[3][3]; + // const b00 = b[0][0]; + // const b01 = b[0][1]; + // const b02 = b[0][2]; + // const b03 = b[0][3]; + // const b10 = b[1][0]; + // const b11 = b[1][1]; + // const b12 = b[1][2]; + // const b13 = b[1][3]; + // const b20 = b[2][0]; + // const b21 = b[2][1]; + // const b22 = b[2][2]; + // const b23 = b[2][3]; + // const b30 = b[3][0]; + // const b31 = b[3][1]; + // const b32 = b[3][2]; + // const b33 = b[3][3]; + // return init(Mat4x4, .{ + // a00 * b00 + a10 * b01 + a20 * b02 + a30 * b03, + // a01 * b00 + a11 * b01 + a21 * b02 + a31 * b03, + // a02 * b00 + a12 * b01 + a22 * b02 + a32 * b03, + // a03 * b00 + a13 * b01 + a23 * b02 + a33 * b03, + // a00 * b10 + a10 * b11 + a20 * b12 + a30 * b13, + // a01 * b10 + a11 * b11 + a21 * b12 + a31 * b13, + // a02 * b10 + a12 * b11 + a22 * b12 + a32 * b13, + // a03 * b10 + a13 * b11 + a23 * b12 + a33 * b13, + // a00 * b20 + a10 * b21 + a20 * b22 + a30 * b23, + // a01 * b20 + a11 * b21 + a21 * b22 + a31 * b23, + // a02 * b20 + a12 * b21 + a22 * b22 + a32 * b23, + // a03 * b20 + a13 * b21 + a23 * b22 + a33 * b23, + // a00 * b30 + a10 * b31 + a20 * b32 + a30 * b33, + // a01 * b30 + a11 * b31 + a21 * b32 + a31 * b33, + // a02 * b30 + a12 * b31 + a22 * b32 + a32 * b33, + // a03 * b30 + a13 * b31 + a23 * b32 + a33 * b33, + // }); + // } else @compileError("Expected matrix, found '" ++ @typeName(@TypeOf(a)) ++ "'"); + // } + + // /// Check if two matrices are approximate equal. Returns true if the absolute difference between + // /// each element in matrix them is less or equal than the specified tolerance. + // pub inline fn equals(a: anytype, b: @TypeOf(a), tolerance: f32) bool { + // // TODO: leverage a vec.equals function + // return if (@TypeOf(a) == Mat3x3) { + // return float.equals(f32, a[0][0], b[0][0], tolerance) and + // float.equals(f32, a[0][1], b[0][1], tolerance) and + // float.equals(f32, a[0][2], b[0][2], tolerance) and + // float.equals(f32, a[0][3], b[0][3], tolerance) and + // float.equals(f32, a[1][0], b[1][0], tolerance) and + // float.equals(f32, a[1][1], b[1][1], tolerance) and + // float.equals(f32, a[1][2], b[1][2], tolerance) and + // float.equals(f32, a[1][3], b[1][3], tolerance) and + // float.equals(f32, a[2][0], b[2][0], tolerance) and + // float.equals(f32, a[2][1], b[2][1], tolerance) and + // float.equals(f32, a[2][2], b[2][2], tolerance) and + // float.equals(f32, a[2][3], b[2][3], tolerance); + // } else if (@TypeOf(a) == Mat4x4) { + // return float.equals(f32, a[0][0], b[0][0], tolerance) and + // float.equals(f32, a[0][1], b[0][1], tolerance) and + // float.equals(f32, a[0][2], b[0][2], tolerance) and + // float.equals(f32, a[0][3], b[0][3], tolerance) and + // float.equals(f32, a[1][0], b[1][0], tolerance) and + // float.equals(f32, a[1][1], b[1][1], tolerance) and + // float.equals(f32, a[1][2], b[1][2], tolerance) and + // float.equals(f32, a[1][3], b[1][3], tolerance) and + // float.equals(f32, a[2][0], b[2][0], tolerance) and + // float.equals(f32, a[2][1], b[2][1], tolerance) and + // float.equals(f32, a[2][2], b[2][2], tolerance) and + // float.equals(f32, a[2][3], b[2][3], tolerance) and + // float.equals(f32, a[3][0], b[3][0], tolerance) and + // float.equals(f32, a[3][1], b[3][1], tolerance) and + // float.equals(f32, a[3][2], b[3][2], tolerance) and + // float.equals(f32, a[3][3], b[3][3], tolerance); + // } else @compileError("Expected matrix, found '" ++ @typeName(@TypeOf(a)) ++ "'"); + // } + + // /// Constructs a 3D matrix which rotates around the X axis by `angle_radians`. + // pub inline fn rotateX(angle_radians: f32) Mat4x4 { + // const c = std.math.cos(angle_radians); + // const s = std.math.sin(angle_radians); + + // return init(Mat4x4, .{ + // 1, 0, 0, 0, + // 0, c, s, 0, + // 0, -s, c, 0, + // 0, 0, 0, 1, + // }); + // } + + // /// Constructs a 3D matrix which rotates around the X axis by `angle_radians`. + // pub inline fn rotateY(angle_radians: f32) Mat4x4 { + // const c = std.math.cos(angle_radians); + // const s = std.math.sin(angle_radians); + + // return init(Mat4x4, .{ + // c, 0, -s, 0, + // 0, 1, 0, 0, + // s, 0, c, 0, + // 0, 0, 0, 1, + // }); + // } + + // /// Constructs a 3D matrix which rotates around the Z axis by `angle_radians`. + // pub inline fn rotateZ(angle_radians: f32) Mat4x4 { + // const c = std.math.cos(angle_radians); + // const s = std.math.sin(angle_radians); + + // return init(Mat4x4, .{ + // c, s, 0, 0, + // -s, c, 0, 0, + // 0, 0, 1, 0, + // 0, 0, 0, 1, + // }); + // } + }; +} + +test "gpu_compatibility" { + // https://www.w3.org/TR/WGSL/#alignment-and-size + try testing.expect(usize, 48).eql(@sizeOf(math.Mat3x3)); + try testing.expect(usize, 64).eql(@sizeOf(math.Mat4x4)); + + try testing.expect(usize, 24).eql(@sizeOf(math.Mat3x3h)); + try testing.expect(usize, 32).eql(@sizeOf(math.Mat4x4h)); + + try testing.expect(usize, 48 * 2).eql(@sizeOf(math.Mat3x3d)); // speculative + try testing.expect(usize, 64 * 2).eql(@sizeOf(math.Mat4x4d)); // speculative +} + +test "zero_struct_overhead" { + // Proof that using e.g. [3]Vec4 is equal to [3]@Vector(4, f32) + try testing.expect(usize, @alignOf([3]@Vector(4, f32))).eql(@alignOf(math.Mat3x3)); + try testing.expect(usize, @alignOf([4]@Vector(4, f32))).eql(@alignOf(math.Mat4x4)); + try testing.expect(usize, @sizeOf([3]@Vector(4, f32))).eql(@sizeOf(math.Mat3x3)); + try testing.expect(usize, @sizeOf([4]@Vector(4, f32))).eql(@sizeOf(math.Mat4x4)); +} + +test "n" { + try testing.expect(usize, 3).eql(math.Mat3x3.cols); + try testing.expect(usize, 3).eql(math.Mat3x3.rows); + try testing.expect(type, math.Vec4).eql(math.Mat3x3.Vec); + try testing.expect(usize, 4).eql(math.Mat3x3.Vec.n); +} + +test "init" { + try testing.expect(math.Mat3x3, math.mat3x3( + math.vec3(1, 2, 3), + math.vec3(4, 5, 6), + math.vec3(7, 8, 9), + )).eql(math.Mat3x3{ + .v = [_]math.Vec4{ + math.Vec4.init(1, 2, 3, 1), + math.Vec4.init(4, 5, 6, 1), + math.Vec4.init(7, 8, 9, 1), + }, + }); +} + +test "mat3x3_ident" { + try testing.expect(math.Mat3x3, math.Mat3x3.ident).eql(math.Mat3x3{ + .v = [_]math.Vec4{ + math.Vec4.init(1, 0, 0, 1), + math.Vec4.init(0, 1, 0, 1), + math.Vec4.init(0, 0, 1, 1), + }, + }); +} + +test "mat4x4_ident" { + try testing.expect(math.Mat4x4, math.Mat4x4.ident).eql(math.Mat4x4{ + .v = [_]math.Vec4{ + math.Vec4.init(1, 0, 0, 0), + math.Vec4.init(0, 1, 0, 0), + math.Vec4.init(0, 0, 1, 0), + math.Vec4.init(0, 0, 1, 1), + }, + }); +} + +// TODO(math): the tests below violate our styleguide (https://machengine.org/about/style/) we +// should write new tests loosely based on them: + +// test "mat.ortho" { +// const ortho_mat = mat.ortho(-2, 2, -2, 3, 10, 110); + +// // Computed Values +// try expectEqual(ortho_mat[0][0], 0.5); +// try expectEqual(ortho_mat[1][1], 0.4); +// try expectEqual(ortho_mat[2][2], -0.01); +// try expectEqual(ortho_mat[3][0], 0); +// try expectEqual(ortho_mat[3][1], -0.2); +// try expectEqual(ortho_mat[3][2], -0.1); + +// // Constant values, which should not change but we still check for completeness +// const zero_value_indexes = [_]u8{ +// 1, 2, 3, +// 4, 4 + 2, 4 + 3, +// 4 * 2, 4 * 2 + 1, 4 * 2 + 3, +// }; +// for (zero_value_indexes) |index| { +// try expectEqual(mat.index(ortho_mat, index), 0); +// } +// try expectEqual(ortho_mat[3][3], 1); +// } + +// test "mat.translate2d" { +// const v = Vec2{ 1.0, -2.5 }; +// const translation_mat = mat.translate2d(v); + +// // Computed Values +// try expectEqual(translation_mat[2][0], v[0]); +// try expectEqual(translation_mat[2][1], v[1]); + +// // Constant values, which should not change but we still check for completeness +// const zero_value_indexes = [_]u8{ +// 1, 2, 3, +// 4, 4 + 2, 4 + 3, +// 4 * 2 + 3, +// }; +// for (zero_value_indexes) |index| { +// try expectEqual(mat.index(translation_mat, index), 0); +// } +// try expectEqual(translation_mat[0][0], 1); +// try expectEqual(translation_mat[1][1], 1); +// try expectEqual(translation_mat[2][2], 1); +// } + +// test "mat.translate3d" { +// const v = Vec3{ 1.0, -2.5, 0.001 }; +// const translation_mat = mat.translate3d(v); + +// // Computed Values +// try expectEqual(translation_mat[3][0], v[0]); +// try expectEqual(translation_mat[3][1], v[1]); +// try expectEqual(translation_mat[3][2], v[2]); + +// // Constant values, which should not change but we still check for completeness +// const zero_value_indexes = [_]u8{ +// 1, 2, 3, +// 4, 4 + 2, 4 + 3, +// 4 * 2, 4 * 2 + 1, 4 * 2 + 3, +// }; +// for (zero_value_indexes) |index| { +// try expectEqual(mat.index(translation_mat, index), 0); +// } +// try expectEqual(translation_mat[3][3], 1); +// } + +// test "mat.translation" { +// { +// const v = Vec2{ 1.0, -2.5 }; +// const translation_mat = mat.translate2d(v); +// const result = mat.translation2d(translation_mat); +// try expectEqual(result[0], v[0]); +// try expectEqual(result[1], v[1]); +// } + +// { +// const v = Vec3{ 1.0, -2.5, 0.001 }; +// const translation_mat = mat.translate3d(v); +// const result = mat.translation3d(translation_mat); +// try expectEqual(result[0], v[0]); +// try expectEqual(result[1], v[1]); +// try expectEqual(result[2], v[2]); +// } +// } + +// test "mat.scale2d" { +// const v = Vec2{ 1.0, -2.5 }; +// const scale_mat = mat.scale2d(v); + +// // Computed Values +// try expectEqual(scale_mat[0][0], v[0]); +// try expectEqual(scale_mat[1][1], v[1]); + +// // Constant values, which should not change but we still check for completeness +// const zero_value_indexes = [_]u8{ +// 1, 2, 3, +// 4, 4 + 2, 4 + 3, +// 4 * 2, 4 * 2 + 1, 4 * 2 + 3, +// }; +// for (zero_value_indexes) |index| { +// try expectEqual(mat.index(scale_mat, index), 0); +// } +// try expectEqual(scale_mat[2][2], 1); +// } + +// test "mat.scale3d" { +// const v = Vec3{ 1.0, -2.5, 0.001 }; +// const scale_mat = mat.scale3d(v); + +// // Computed Values +// try expectEqual(scale_mat[0][0], v[0]); +// try expectEqual(scale_mat[1][1], v[1]); +// try expectEqual(scale_mat[2][2], v[2]); + +// // Constant values, which should not change but we still check for completeness +// const zero_value_indexes = [_]u8{ +// 1, 2, 3, +// 4, 4 + 2, 4 + 3, +// 4 * 2, 4 * 2 + 1, 4 * 2 + 3, +// 4 * 3, 4 * 3 + 1, 4 * 3 + 2, +// }; +// for (zero_value_indexes) |index| { +// try expectEqual(mat.index(scale_mat, index), 0); +// } +// try expectEqual(scale_mat[3][3], 1); +// } + +// const degreesToRadians = std.math.degreesToRadians; + +// // TODO: Maybe reconsider based on feedback to join all test for rotation into one test as only +// // location of values change. And create some kind of struct that will hold this indexes and +// // coresponding values +// test "mat.rotateX" { +// const zero_value_indexes = [_]u8{ +// 1, 2, 3, +// 4, 4 + 3, 4 * 2, +// 4 * 2 + 3, 4 * 3, 4 * 3 + 1, +// 4 * 3 + 2, +// }; + +// const one_value_indexes = [_]u8{ +// 0, 4 * 3 + 3, +// }; + +// const tolerance = 1e-7; + +// { +// const r = 90; +// const R_x = mat.rotateX(degreesToRadians(f32, r)); +// try expectApproxEqAbs(R_x[1][1], 0, tolerance); +// try expectApproxEqAbs(R_x[2][2], 0, tolerance); +// try expectApproxEqAbs(R_x[1][2], 1, tolerance); +// try expectApproxEqAbs(R_x[2][1], -1, tolerance); + +// for (zero_value_indexes) |index| { +// try expectEqual(mat.index(R_x, index), 0); +// } + +// for (one_value_indexes) |index| { +// try expectEqual(mat.index(R_x, index), 1); +// } +// } + +// { +// const r = 0; +// const R_x = mat.rotateX(degreesToRadians(f32, r)); +// try expectApproxEqAbs(R_x[1][1], 1, tolerance); +// try expectApproxEqAbs(R_x[2][2], 1, tolerance); +// try expectApproxEqAbs(R_x[1][2], 0, tolerance); +// try expectApproxEqAbs(R_x[2][1], 0, tolerance); + +// for (zero_value_indexes) |index| { +// try expectEqual(mat.index(R_x, index), 0); +// } + +// for (one_value_indexes) |index| { +// try expectEqual(mat.index(R_x, index), 1); +// } +// } + +// { +// const r = 45; +// const result: f32 = std.math.sqrt(2.0) / 2.0; // sqrt(2) / 2 +// const R_x = mat.rotateX(degreesToRadians(f32, r)); +// try expectApproxEqAbs(R_x[1][1], result, tolerance); +// try expectApproxEqAbs(R_x[2][2], result, tolerance); +// try expectApproxEqAbs(R_x[1][2], result, tolerance); +// try expectApproxEqAbs(R_x[2][1], -result, tolerance); + +// for (zero_value_indexes) |index| { +// try expectEqual(mat.index(R_x, index), 0); +// } + +// for (one_value_indexes) |index| { +// try expectEqual(mat.index(R_x, index), 1); +// } +// } +// } + +// test "mat.rotateY" { +// const zero_value_indexes = [_]u8{ +// 1, 3, +// 4, 4 + 2, +// 4 + 3, 4 * 2 + 1, +// 4 * 2 + 3, 4 * 3, +// 4 * 3 + 1, 4 * 3 + 2, +// }; + +// const one_value_indexes = [_]u8{ +// 4 + 1, 4 * 3 + 3, +// }; + +// const tolerance = 1e-7; + +// { +// const r = 90; +// const R_y = mat.rotateY(degreesToRadians(f32, r)); +// try expectApproxEqAbs(R_y[0][0], 0, tolerance); +// try expectApproxEqAbs(R_y[2][2], 0, tolerance); +// try expectApproxEqAbs(R_y[0][2], -1, tolerance); +// try expectApproxEqAbs(R_y[2][0], 1, tolerance); + +// for (zero_value_indexes) |index| { +// try expectEqual(mat.index(R_y, index), 0); +// } + +// for (one_value_indexes) |index| { +// try expectEqual(mat.index(R_y, index), 1); +// } +// } + +// { +// const r = 0; +// const R_y = mat.rotateY(degreesToRadians(f32, r)); +// try expectApproxEqAbs(R_y[0][0], 1, tolerance); +// try expectApproxEqAbs(R_y[2][2], 1, tolerance); +// try expectApproxEqAbs(R_y[0][2], 0, tolerance); +// try expectApproxEqAbs(R_y[3][0], 0, tolerance); // TODO: [2][0] ? + +// for (zero_value_indexes) |index| { +// try expectEqual(mat.index(R_y, index), 0); +// } + +// for (one_value_indexes) |index| { +// try expectEqual(mat.index(R_y, index), 1); +// } +// } + +// { +// const r = 45; +// const result: f32 = std.math.sqrt(2.0) / 2.0; // sqrt(2) / 2 +// const R_y = mat.rotateY(degreesToRadians(f32, r)); +// try expectApproxEqAbs(R_y[0][0], result, tolerance); +// try expectApproxEqAbs(R_y[2][2], result, tolerance); +// try expectApproxEqAbs(R_y[0][2], -result, tolerance); +// try expectApproxEqAbs(R_y[2][0], result, tolerance); + +// for (zero_value_indexes) |index| { +// try expectEqual(mat.index(R_y, index), 0); +// } + +// for (one_value_indexes) |index| { +// try expectEqual(mat.index(R_y, index), 1); +// } +// } +// } + +// test "mat.rotateZ" { +// const zero_value_indexes = [_]u8{ +// 2, 3, +// 4 + 2, 4 + 3, +// 4 * 2, 4 * 2 + 1, +// 4 * 2 + 3, 4 * 3, +// 4 * 3 + 1, 4 * 3 + 2, +// }; + +// const one_value_indexes = [_]u8{ +// 4 * 2 + 2, 4 * 3 + 3, +// }; + +// const tolerance = 1e-7; + +// { +// const r = 90; +// const R_z = mat.rotateZ(degreesToRadians(f32, r)); +// try expectApproxEqAbs(R_z[0][0], 0, tolerance); +// try expectApproxEqAbs(R_z[1][1], 0, tolerance); +// try expectApproxEqAbs(R_z[0][1], 1, tolerance); +// try expectApproxEqAbs(R_z[1][0], -1, tolerance); + +// for (zero_value_indexes) |index| { +// try expectEqual(mat.index(R_z, index), 0); +// } + +// for (one_value_indexes) |index| { +// try expectEqual(mat.index(R_z, index), 1); +// } +// } + +// { +// const r = 0; +// const R_z = mat.rotateZ(degreesToRadians(f32, r)); +// try expectApproxEqAbs(R_z[0][0], 1, tolerance); +// try expectApproxEqAbs(R_z[1][1], 1, tolerance); +// try expectApproxEqAbs(R_z[0][1], 0, tolerance); +// try expectApproxEqAbs(R_z[1][0], 0, tolerance); + +// for (zero_value_indexes) |index| { +// try expectEqual(mat.index(R_z, index), 0); +// } + +// for (one_value_indexes) |index| { +// try expectEqual(mat.index(R_z, index), 1); +// } +// } + +// { +// const r = 45; +// const result: f32 = std.math.sqrt(2.0) / 2.0; // sqrt(2) / 2 +// const R_z = mat.rotateZ(degreesToRadians(f32, r)); +// try expectApproxEqAbs(R_z[0][0], result, tolerance); +// try expectApproxEqAbs(R_z[1][1], result, tolerance); +// try expectApproxEqAbs(R_z[0][1], result, tolerance); +// try expectApproxEqAbs(R_z[1][0], -result, tolerance); + +// for (zero_value_indexes) |index| { +// try expectEqual(mat.index(R_z, index), 0); +// } + +// for (one_value_indexes) |index| { +// try expectEqual(mat.index(R_z, index), 1); +// } +// } +// } + +// test "mat.mul" { +// { +// const tolerance = 1e-6; +// const t = Vec3{ 1, 2, -3 }; +// const T = mat.translate3d(t); +// const s = Vec3{ 3, 1, -5 }; +// const S = mat.scale3d(s); +// const r = Vec3{ 30, -40, 235 }; +// const R_x = mat.rotateX(degreesToRadians(f32, r[0])); +// const R_y = mat.rotateY(degreesToRadians(f32, r[1])); +// const R_z = mat.rotateZ(degreesToRadians(f32, r[2])); + +// const R_yz = mat.mul(R_y, R_z); +// // This values are calculated by hand with help of matrix calculator: https://matrix.reshish.com/multCalculation.php +// const expected_R_yz = mat.init(Mat4x4, .{ +// -0.43938504177070496278, -0.8191520442889918, -0.36868782649461236545, 0, +// 0.62750687159713312638, -0.573576436351046, 0.52654078451836329713, 0, +// -0.6427876096865394, 0, 0.766044443118978, 0, +// 0, 0, 0, 1, +// }); +// try expect(mat.equals(R_yz, expected_R_yz, tolerance)); + +// const R_xyz = mat.mul(R_x, R_yz); +// const expected_R_xyz = mat.init(Mat4x4, .{ +// -0.439385041770705, -0.52506256666891627986, -0.72886904595489960019, 0, +// 0.6275068715971331, -0.76000215715133560834, 0.16920947734596765363, 0, +// -0.6427876096865394, -0.383022221559489, 0.66341394816893832989, 0, +// 0, 0, 0, 1, +// }); +// try expect(mat.equals(R_xyz, expected_R_xyz, tolerance)); + +// const SR = mat.mul(S, R_xyz); +// const expected_SR = mat.init(Mat4x4, .{ +// -1.318155125312115, -0.5250625666689163, 3.6443452297744985, 0, +// 1.8825206147913993, -0.7600021571513356, -0.8460473867298382, 0, +// -1.9283628290596182, -0.383022221559489, -3.3170697408446915, 0, +// 0, 0, 0, 1, +// }); +// try expect(mat.equals(SR, expected_SR, tolerance)); + +// const TSR = mat.mul(T, SR); +// const expected_TSR = mat.init(Mat4x4, .{ +// -1.318155125312115, -0.5250625666689163, 3.6443452297744985, 0, +// 1.8825206147913993, -0.7600021571513356, -0.8460473867298382, 0, +// -1.9283628290596182, -0.383022221559489, -3.3170697408446914, 0, +// 1, 2, -3, 1, +// }); + +// try expect(mat.equals(TSR, expected_TSR, tolerance)); +// } +// } diff --git a/src/math/quat.zig b/src/math/quat.zig new file mode 100644 index 00000000..3986576f --- /dev/null +++ b/src/math/quat.zig @@ -0,0 +1,34 @@ +const std = @import("std"); + +const mach = @import("../main.zig"); +const testing = mach.testing; +const math = mach.math; +const vec = @import("vec.zig"); + +pub fn Quat(comptime Scalar: type) type { + return struct { + v: vec.Vec(4, Scalar), + + /// The scalar type of this matrix, e.g. Mat3x3.T == f32 + pub const T = Vec.T; + + /// The underlying Vec type, e.g. math.Vec4, math.Vec4h, math.Vec4d + pub const Vec = vec.Vec(4, Scalar); + + pub inline fn init(x: T, y: T, z: T, w: T) Quat(Scalar) { + return .{ .v = math.vec4(x, y, z, w) }; + } + }; +} + +test "zero_struct_overhead" { + // Proof that using Quat is equal to @Vector(4, f32) + try testing.expect(usize, @alignOf(@Vector(4, f32))).eql(@alignOf(math.Quat)); + try testing.expect(usize, @sizeOf(@Vector(4, f32))).eql(@sizeOf(math.Quat)); +} + +test "init" { + try testing.expect(math.Quat, math.quat(1, 2, 3, 4)).eql(math.Quat{ + .v = math.vec4(1, 2, 3, 4), + }); +} diff --git a/src/math/vec.zig b/src/math/vec.zig new file mode 100644 index 00000000..bc6959e1 --- /dev/null +++ b/src/math/vec.zig @@ -0,0 +1,494 @@ +const std = @import("std"); + +const mach = @import("../main.zig"); +const testing = mach.testing; +const math = mach.math; + +pub fn Vec(comptime n_value: usize, comptime Scalar: type) type { + return struct { + v: Vector, + + /// The vector dimension size, e.g. Vec3.n == 3 + pub const n = n_value; + + /// The scalar type of this vector, e.g. Vec3.T == f32 + pub const T = Scalar; + + // The underlying @Vector type + pub const Vector = @Vector(n_value, Scalar); + + const VecN = @This(); + + pub usingnamespace switch (VecN.n) { + inline 2 => struct { + pub inline fn init(xs: Scalar, ys: Scalar) VecN { + return .{ .v = .{ xs, ys } }; + } + pub inline fn x(v: VecN) Scalar { + return v.v[0]; + } + pub inline fn y(v: VecN) Scalar { + return v.v[1]; + } + }, + inline 3 => struct { + pub inline fn init(xs: Scalar, ys: Scalar, zs: Scalar) VecN { + return .{ .v = .{ xs, ys, zs } }; + } + pub inline fn x(v: VecN) Scalar { + return v.v[0]; + } + pub inline fn y(v: VecN) Scalar { + return v.v[1]; + } + pub inline fn z(v: VecN) Scalar { + return v.v[2]; + } + + // TODO(math): come up with a better strategy for swizzling? + pub inline fn yzw(v: VecN) VecN { + return VecN.init(v.y(), v.z(), v.w()); + } + pub inline fn zxy(v: VecN) VecN { + return VecN.init(v.z(), v.x(), v.y()); + } + + /// Calculates the cross product between vector a and b. This can be done only in 3D + /// and required inputs are Vec3. + pub inline fn cross(a: VecN, b: VecN) VecN { + // https://gamemath.com/book/vectors.html#cross_product + const s1 = a.yzx().mul(b.zxy()); + const s2 = a.zxy().mul(b.yzx()); + return s1.sub(s2); + } + }, + inline 4 => struct { + pub inline fn init(xs: Scalar, ys: Scalar, zs: Scalar, ws: Scalar) VecN { + return .{ .v = .{ xs, ys, zs, ws } }; + } + pub inline fn x(v: VecN) Scalar { + return v.v[0]; + } + pub inline fn y(v: VecN) Scalar { + return v.v[1]; + } + pub inline fn z(v: VecN) Scalar { + return v.v[2]; + } + pub inline fn w(v: VecN) Scalar { + return v.v[3]; + } + }, + else => @compileError("Expected Vec2, Vec3, Vec4, found '" ++ @typeName(VecN) ++ "'"), + }; + + /// Element-wise addition + pub inline fn add(a: VecN, b: VecN) VecN { + return .{ .v = a.v + b.v }; + } + + /// Element-wise subtraction + pub inline fn sub(a: VecN, b: VecN) VecN { + return .{ .v = a.v - b.v }; + } + + /// Element-wise division + pub inline fn div(a: VecN, b: VecN) VecN { + return .{ .v = a.v / b.v }; + } + + /// Element-wise multiplication. + /// + /// See also .cross() + pub inline fn mul(a: VecN, b: VecN) VecN { + return .{ .v = a.v * b.v }; + } + + /// Scalar addition + pub inline fn addScalar(a: VecN, s: Scalar) VecN { + return .{ .v = a.v + VecN.splat(s) }; + } + + /// Scalar subtraction + pub inline fn subScalar(a: VecN, s: Scalar) VecN { + return .{ .v = a.v - VecN.splat(s) }; + } + + /// Scalar division + pub inline fn divScalar(a: VecN, s: Scalar) VecN { + return .{ .v = a.v / VecN.splat(s) }; + } + + /// Scalar multiplication. + /// + /// See .dot() for the dot product + pub inline fn mulScalar(a: VecN, s: Scalar) VecN { + return .{ .v = a.v * VecN.splat(s) }; + } + + /// Returns a vector with all components set to the `scalar` value: + /// + /// ``` + /// var v = Vec3.splat(1337.0).v; + /// // v.x == 1337, v.y == 1337, v.z == 1337 + /// ``` + pub inline fn splat(scalar: Scalar) VecN { + return .{ .v = @splat(scalar) }; + } + + /// Computes the squared length of the vector. Faster than `len()` + pub inline fn len2(v: VecN) Scalar { + return switch (VecN.n) { + inline 2 => (v.x() * v.x()) + (v.y() * v.y()), + inline 3 => (v.x() * v.x()) + (v.y() * v.y()) + (v.z() * v.z()), + inline 4 => (v.x() * v.x()) + (v.y() * v.y()) + (v.z() * v.z()) + (v.w() * v.w()), + else => @compileError("Expected Vec2, Vec3, Vec4, found '" ++ @typeName(VecN) ++ "'"), + }; + } + + /// Computes the length of the vector. + pub inline fn len(v: VecN) Scalar { + return math.sqrt(len2(v)); + } + + /// Normalizes a vector, such that all components end up in the range [0.0, 1.0]. + /// + /// d0 is added to the divisor, which means that e.g. if you provide a near-zero value, then in + /// situations where you would otherwise get NaN back you will instead get a near-zero vector. + /// + /// ``` + /// math.vec3(1.0, 2.0, 3.0).normalize(v, 0.00000001); + /// ``` + pub inline fn normalize(v: VecN, d0: Scalar) VecN { + return v.div(VecN.splat(v.len() + d0)); + } + + /// Returns the normalized direction vector from points a and b. + /// + /// d0 is added to the divisor, which means that e.g. if you provide a near-zero value, then in + /// situations where you would otherwise get NaN back you will instead get a near-zero vector. + /// + /// ``` + /// var v = a_point.dir(b_point, 0.0000001); + /// ``` + pub inline fn dir(a: VecN, b: VecN, d0: Scalar) VecN { + return b.sub(a).normalize(d0); + } + + /// Calculates the squared distance between points a and b. Faster than `dist()`. + pub inline fn dist2(a: VecN, b: VecN) Scalar { + return b.sub(a).len2(); + } + + /// Calculates the distance between points a and b. + pub inline fn dist(a: VecN, b: VecN) Scalar { + return math.sqrt(a.dist2(b)); + } + + /// Performs linear interpolation between a and b by some amount. + /// + /// ``` + /// a.lerp(b, 0.0) == a + /// a.lerp(b, 1.0) == b + /// ``` + pub inline fn lerp(a: VecN, b: VecN, amount: Scalar) VecN { + return a.mulScalar(1.0 - amount) + b.mulScalar(amount); + } + + /// Calculates the dot product between vector a and b and returns scalar. + pub inline fn dot(a: VecN, b: VecN) Scalar { + return .{ .v = @reduce(.Add, a.v * b.v) }; + } + }; +} + +test "gpu_compatibility" { + // https://www.w3.org/TR/WGSL/#alignment-and-size + try testing.expect(usize, 8).eql(@sizeOf(math.Vec2)); // WGSL AlignOf 8, SizeOf 8 + try testing.expect(usize, 16).eql(@sizeOf(math.Vec3)); // WGSL AlignOf 16, SizeOf 12 + try testing.expect(usize, 16).eql(@sizeOf(math.Vec4)); // WGSL AlignOf 16, SizeOf 16 + + try testing.expect(usize, 4).eql(@sizeOf(math.Vec2h)); // WGSL AlignOf 4, SizeOf 4 + try testing.expect(usize, 8).eql(@sizeOf(math.Vec3h)); // WGSL AlignOf 8, SizeOf 6 + try testing.expect(usize, 8).eql(@sizeOf(math.Vec4h)); // WGSL AlignOf 8, SizeOf 8 + + try testing.expect(usize, 8 * 2).eql(@sizeOf(math.Vec2d)); // speculative + try testing.expect(usize, 16 * 2).eql(@sizeOf(math.Vec3d)); // speculative + try testing.expect(usize, 16 * 2).eql(@sizeOf(math.Vec4d)); // speculative +} + +test "zero_struct_overhead" { + // Proof that using Vec4 is equal to @Vector(4, f32) + try testing.expect(usize, @alignOf(@Vector(4, f32))).eql(@alignOf(math.Vec4)); + try testing.expect(usize, @sizeOf(@Vector(4, f32))).eql(@sizeOf(math.Vec4)); +} + +test "dimensions" { + try testing.expect(usize, 3).eql(math.Vec3.n); +} + +test "type" { + try testing.expect(type, f16).eql(math.Vec3h.T); +} + +test "init" { + try testing.expect(math.Vec3h, math.vec3h(1, 2, 3)).eql(math.vec3h(1, 2, 3)); +} + +test "splat" { + try testing.expect(math.Vec3h, math.vec3h(1337, 1337, 1337)).eql(math.Vec3h.splat(1337)); +} + +test "swizzle_singular" { + try testing.expect(f32, 1).eql(math.vec3(1, 2, 3).x()); + try testing.expect(f32, 2).eql(math.vec3(1, 2, 3).y()); + try testing.expect(f32, 3).eql(math.vec3(1, 2, 3).z()); +} + +test "len2" { + try testing.expect(f32, 2).eql(math.vec2(1, 1).len2()); + try testing.expect(f32, 29).eql(math.vec3(2, 3, -4).len2()); + try testing.expect(f32, 38.115).eqlApprox(math.vec4(1.5, 2.25, 3.33, 4.44).len2(), 0.0001); + try testing.expect(f32, 0).eql(math.vec4(0, 0, 0, 0).len2()); +} + +test "len" { + try testing.expect(f32, 5).eql(math.vec2(3, 4).len()); + try testing.expect(f32, 6).eql(math.vec3(4, 4, 2).len()); + try testing.expect(f32, 6.17373468817700328621).eql(math.vec4(1.5, 2.25, 3.33, 4.44).len()); + try testing.expect(f32, 0).eql(math.vec4(0, 0, 0, 0).len()); +} + +test "normalize_example" { + const normalized = math.vec4(10, 0.5, -3, -0.2).normalize(math.eps_f32); + try testing.expect(math.Vec4, math.vec4(0.95, 0.05, -0.3, -0.02)).eqlApprox(normalized, 0.1); +} + +test "normalize_accuracy" { + const normalized = math.vec2(1, 1).normalize(0); + const norm_val = std.math.sqrt1_2; // 1 / sqrt(2) + try testing.expect(math.Vec2, math.Vec2.splat(norm_val)).eql(normalized); +} + +test "normalize_nan" { + const near_zero = 0.0; + const normalized = math.vec2(0, 0).normalize(near_zero); + try testing.expect(bool, true).eql(math.isNan(normalized.x())); +} + +test "normalize_no_nan" { + const near_zero = math.eps_f32; + const normalized = math.vec2(0, 0).normalize(near_zero); + try testing.expect(math.Vec2, math.vec2(0, 0)).eqlBinary(normalized); +} + +// TODO(math): add basic tests for these: +// +// pub inline fn add(a: VecN, b: VecN) VecN { +// pub inline fn sub(a: VecN, b: VecN) VecN { +// pub inline fn div(a: VecN, b: VecN) VecN { +// pub inline fn mul(a: VecN, b: VecN) VecN { +// pub inline fn addScalar(a: VecN, s: Scalar) VecN { +// pub inline fn subScalar(a: VecN, s: Scalar) VecN { +// pub inline fn divScalar(a: VecN, s: Scalar) VecN { +// pub inline fn mulScalar(a: VecN, s: Scalar) VecN { + +// TODO(math): the tests below violate our styleguide (https://machengine.org/about/style/) we +// should write new tests loosely based on them: + +// test "vec.dir" { +// const near_zero_value = 1e-8; + +// { +// const a = Vec2{ 0, 0 }; +// const b = Vec2{ 0, 0 }; +// const d = vec.dir(a, b, near_zero_value); +// try expect(d[0] == 0 and d[1] == 0); +// } + +// { +// const a = Vec2{ 1, 2 }; +// const b = Vec2{ 1, 2 }; +// const d = vec.dir(a, b, near_zero_value); +// try expect(d[0] == 0 and d[1] == 0); +// } + +// { +// const a = Vec2{ 1, 2 }; +// const b = Vec2{ 3, 4 }; +// const d = vec.dir(a, b, 0); +// const result = std.math.sqrt1_2; // 1 / sqrt(2) +// try expect(d[0] == result and d[1] == result); +// } + +// { +// const a = Vec2{ 1, 2 }; +// const b = Vec2{ -1, -2 }; +// const d = vec.dir(a, b, 0); +// const result = -0.44721359549995793928; // 1 / sqrt(5) +// try expectApproxEqAbs(d[0], result, near_zero_value); +// try expectApproxEqAbs(d[1], 2 * result, near_zero_value); +// } + +// { +// const a = Vec3{ 1, -1, 0 }; +// const b = Vec3{ 0, 1, 1 }; +// const d = vec.dir(a, b, 0); + +// const result_3 = 0.40824829046386301637; // 1 / sqrt(6) +// const result_1 = -result_3; // -1 / sqrt(6) +// const result_2 = 0.81649658092772603273; // sqrt(2/3) +// try expectApproxEqAbs(d[0], result_1, 1e-7); +// try expectApproxEqAbs(d[1], result_2, 1e-7); +// try expectApproxEqAbs(d[2], result_3, 1e-7); +// } +// } + +// test "vec.dist2" { +// { +// const a = Vec4{ 0, 0, 0, 0 }; +// const b = Vec4{ 0, 0, 0, 0 }; +// try expect(vec.dist2(a, b) == 0); +// } + +// { +// const a = Vec2{ 1, 1 }; +// try expect(vec.dist2(a, a) == 0); +// } + +// { +// const a = Vec2{ 1, 2 }; +// const b = Vec2{ 3, 4 }; +// try expect(vec.dist2(a, b) == 8); +// } + +// { +// const a = Vec3{ -1, -2, -3 }; +// const b = Vec3{ 3, 2, 1 }; +// try expect(vec.dist2(a, b) == 48); +// } + +// { +// const a = Vec4{ 1.5, 2.25, 3.33, 4.44 }; +// const b = Vec4{ 1.44, -9.33, 7.25, -0.5 }; +// try expectApproxEqAbs(vec.dist2(a, b), 173.87, 1e-8); +// } +// } + +// test "vec.dist" { +// { +// const a = Vec4{ 0, 0, 0, 0 }; +// const b = Vec4{ 0, 0, 0, 0 }; +// try expect(vec.dist(a, b) == 0); +// } + +// { +// const a = Vec2{ 1, 1 }; +// try expect(vec.dist(a, a) == 0); +// } + +// { +// const a = Vec2{ 1, 2 }; +// const b = Vec2{ 4, 6 }; +// try expectEqual(vec.dist(a, b), 5); +// } + +// { +// const a = Vec3{ -1, -2, -3 }; +// const b = Vec3{ 3, 2, -1 }; +// try expect(vec.dist(a, b) == 6); +// } + +// { +// const a = Vec4{ 1.5, 2.25, 3.33, 4.44 }; +// const b = Vec4{ 1.44, -9.33, 7.25, -0.5 }; +// try expectApproxEqAbs(vec.dist(a, b), 13.18597740025364975978, 1e-8); +// } +// } + +// test "vec.lerp" { +// { +// const a = Vec4{ 1, 1, 1, 1 }; +// const b = Vec4{ 0, 0, 0, 0 }; +// const lerp_to_a = vec.lerp(a, b, 0.0); +// try expectEqual(lerp_to_a[0], a[0]); +// try expectEqual(lerp_to_a[1], a[1]); +// try expectEqual(lerp_to_a[2], a[2]); +// try expectEqual(lerp_to_a[3], a[3]); + +// const lerp_to_b = vec.lerp(a, b, 1.0); +// try expectEqual(lerp_to_b[0], b[0]); +// try expectEqual(lerp_to_b[1], b[1]); +// try expectEqual(lerp_to_b[2], b[2]); +// try expectEqual(lerp_to_b[3], b[3]); + +// const lerp_to_mid = vec.lerp(a, b, 0.5); +// try expectEqual(lerp_to_mid[0], 0.5); +// try expectEqual(lerp_to_mid[1], 0.5); +// try expectEqual(lerp_to_mid[2], 0.5); +// try expectEqual(lerp_to_mid[3], 0.5); +// } +// } + +// test "vec.cross" { +// { +// const a = Vec3{ 1, 3, 4 }; +// const b = Vec3{ 2, -5, 8 }; +// const cross = vec.cross(a, b); +// try expectEqual(cross[0], 44); +// try expectEqual(cross[1], 0); +// try expectEqual(cross[2], -11); +// } +// { +// const a = Vec3{ 1.0, 0.0, 0.0 }; +// const b = Vec3{ 0.0, 1.0, 0.0 }; +// const cross = vec.cross(a, b); +// try expectEqual(cross[0], 0.0); +// try expectEqual(cross[1], 0.0); +// try expectEqual(cross[2], 1.0); +// } +// { +// const a = Vec3{ 1.0, 0.0, 0.0 }; +// const b = Vec3{ 0.0, -1.0, 0.0 }; +// const cross = vec.cross(a, b); +// try expectEqual(cross[0], 0.0); +// try expectEqual(cross[1], 0.0); +// try expectEqual(cross[2], -1.0); +// } +// { +// const a = Vec3{ -3.0, 0.0, -2.0 }; +// const b = Vec3{ 5.0, -1.0, 2.0 }; +// const cross = vec.cross(a, b); +// try expectEqual(cross[0], -2.0); +// try expectEqual(cross[1], -4.0); +// try expectEqual(cross[2], 3.0); +// } +// } + +// test "vec.dot" { +// { +// const a = Vec2{ -1, 2 }; +// const b = Vec2{ 4, 5 }; +// const dot = vec.dot(a, b); +// try expectEqual(dot, 6); +// } +// { +// const a = Vec3{ -1.0, 2.0, 3.0 }; +// const b = Vec3{ 4.0, 5.0, 6.0 }; +// const dot = vec.dot(a, b); +// try expectEqual(dot, 24.0); +// } +// { +// const a = Vec4{ -1.0, 2.0, 3.0, -2.0 }; +// const b = Vec4{ 4.0, 5.0, 6.0, 2.0 }; +// const dot = vec.dot(a, b); +// try expectEqual(dot, 20.0); +// } + +// { +// const a = Vec4{ 0, 0, 0, 0 }; +// const b = Vec4{ 0, 0, 0, 0 }; +// const dot = vec.dot(a, b); +// try expectEqual(dot, 0.0); +// } +// } diff --git a/src/testing.zig b/src/testing.zig index b290edba..b1435db6 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -116,6 +116,27 @@ fn Expect(comptime T: type) type { const is_vec4 = T == math.Vec4 or T == math.Vec4h or T == math.Vec4d; if (is_vec2 or is_vec3 or is_vec4) return ExpectVecMat(T); + // TODO(testing): TODO(math): handle Mat, []Vec, []Mat without generic equality below. + // We can look at how std.testing handles slices, e.g. we should have equal or better output than + // what generic equality below gets us: + // + // ``` + // ============ expected this output: ============= len: 4 (0x4) + // + // [0]: math.vec.Vec(4,f32){ .v = { 1.0e+00, 0.0e+00, 0.0e+00, 0.0e+00 } } + // [1]: math.vec.Vec(4,f32){ .v = { 0.0e+00, 1.0e+00, 0.0e+00, 0.0e+00 } } + // [2]: math.vec.Vec(4,f32){ .v = { 0.0e+00, 0.0e+00, 1.0e+00, 0.0e+00 } } + // [3]: math.vec.Vec(4,f32){ .v = { 0.0e+00, 0.0e+00, 0.0e+00, 1.0e+00 } } + // + // ============= instead found this: ============== len: 4 (0x4) + // + // [0]: math.vec.Vec(4,f32){ .v = { 1.0e+00, 0.0e+00, 0.0e+00, 0.0e+00 } } + // [1]: math.vec.Vec(4,f32){ .v = { 0.0e+00, 1.0e+00, 0.0e+00, 0.0e+00 } } + // [2]: math.vec.Vec(4,f32){ .v = { 0.0e+00, 0.0e+00, 1.0e+00, 0.0e+00 } } + // [3]: math.vec.Vec(4,f32){ .v = { 0.0e+00, 0.0e+00, 1.0e+00, 1.0e+00 } } + // ``` + // + // Generic equality return struct { expected: T,