mach/src/math/vec.zig
Joel D. Schüller 7597f03b71 math: Implement more Vec operations
max, min, inverse, negate, maxScalar, minScalar
2023-09-22 09:51:55 -07:00

604 lines
20 KiB
Zig

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 extern 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: *const VecN) Scalar {
return v.v[0];
}
pub inline fn y(v: *const 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: *const VecN) Scalar {
return v.v[0];
}
pub inline fn y(v: *const VecN) Scalar {
return v.v[1];
}
pub inline fn z(v: *const VecN) Scalar {
return v.v[2];
}
// TODO(math): come up with a better strategy for swizzling?
pub inline fn yzw(v: *const VecN) VecN {
return VecN.init(v.y(), v.z(), v.w());
}
pub inline fn zxy(v: *const 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: *const VecN, b: *const 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: *const VecN) Scalar {
return v.v[0];
}
pub inline fn y(v: *const VecN) Scalar {
return v.v[1];
}
pub inline fn z(v: *const VecN) Scalar {
return v.v[2];
}
pub inline fn w(v: *const VecN) Scalar {
return v.v[3];
}
},
else => @compileError("Expected Vec2, Vec3, Vec4, found '" ++ @typeName(VecN) ++ "'"),
};
/// Element-wise addition
pub inline fn add(a: *const VecN, b: *const VecN) VecN {
return .{ .v = a.v + b.v };
}
/// Element-wise subtraction
pub inline fn sub(a: *const VecN, b: *const VecN) VecN {
return .{ .v = a.v - b.v };
}
/// Element-wise division
pub inline fn div(a: *const VecN, b: *const VecN) VecN {
return .{ .v = a.v / b.v };
}
/// Element-wise multiplication.
///
/// See also .cross()
pub inline fn mul(a: *const VecN, b: *const VecN) VecN {
return .{ .v = a.v * b.v };
}
/// Scalar addition
pub inline fn addScalar(a: *const VecN, s: Scalar) VecN {
return .{ .v = a.v + VecN.splat(s).v };
}
/// Scalar subtraction
pub inline fn subScalar(a: *const VecN, s: Scalar) VecN {
return .{ .v = a.v - VecN.splat(s).v };
}
/// Scalar division
pub inline fn divScalar(a: *const VecN, s: Scalar) VecN {
return .{ .v = a.v / VecN.splat(s).v };
}
/// Scalar multiplication.
///
/// See .dot() for the dot product
pub inline fn mulScalar(a: *const VecN, s: Scalar) VecN {
return .{ .v = a.v * VecN.splat(s).v };
}
/// Element-wise a < b
pub inline fn less(a: *const VecN, b: Scalar) bool {
return a.v < b.v;
}
/// Element-wise a <= b
pub inline fn lessEq(a: *const VecN, b: Scalar) bool {
return a.v <= b.v;
}
/// Element-wise a > b
pub inline fn greater(a: *const VecN, b: Scalar) bool {
return a.v > b.v;
}
/// Element-wise a >= b
pub inline fn greaterEq(a: *const VecN, b: Scalar) bool {
return a.v >= b.v;
}
/// 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: *const 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: *const 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: *const 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: *const VecN, b: *const 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: *const VecN, b: *const VecN) Scalar {
return b.sub(a).len2();
}
/// Calculates the distance between points a and b.
pub inline fn dist(a: *const VecN, b: *const 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: *const VecN, b: *const VecN, amount: Scalar) VecN {
return a.mulScalar(1.0 - amount).add(&b.mulScalar(amount));
}
/// Calculates the dot product between vector a and b and returns scalar.
pub inline fn dot(a: *const VecN, b: *const VecN) Scalar {
return @reduce(.Add, a.v * b.v);
}
// Returns a new vector with the max values of two vectors
pub inline fn max(a: *const VecN, b: *const VecN) VecN {
return switch (VecN.n) {
inline 2 => VecN.init(
@max(a.x(), b.x()),
@max(a.y(), b.y()),
),
inline 3 => VecN.init(
@max(a.x(), b.x()),
@max(a.y(), b.y()),
@max(a.z(), b.z()),
),
inline 4 => VecN.init(
@max(a.x(), b.x()),
@max(a.y(), b.y()),
@max(a.z(), b.z()),
@max(a.w(), b.w()),
),
else => @compileError("Expected Vec2, Vec3, Vec4, found '" ++ @typeName(VecN) ++ "'"),
};
}
// Returns a new vector with the min values of two vectors
pub inline fn min(a: *const VecN, b: *const VecN) VecN {
return switch (VecN.n) {
inline 2 => VecN.init(
@min(a.x(), b.x()),
@min(a.y(), b.y()),
),
inline 3 => VecN.init(
@min(a.x(), b.x()),
@min(a.y(), b.y()),
@min(a.z(), b.z()),
),
inline 4 => VecN.init(
@min(a.x(), b.x()),
@min(a.y(), b.y()),
@min(a.z(), b.z()),
@min(a.w(), b.w()),
),
else => @compileError("Expected Vec2, Vec3, Vec4, found '" ++ @typeName(VecN) ++ "'"),
};
}
// Returns the inverse of a given vector
pub inline fn inverse(a: *const VecN) VecN {
return switch (VecN.n) {
inline 2 => .{ .v = (math.vec2(1, 1).v / a.v) },
inline 3 => .{ .v = (math.vec3(1, 1, 1).v / a.v) },
inline 4 => .{ .v = (math.vec4(1, 1, 1, 1).v / a.v) },
else => @compileError("Expected Vec2, Vec3, Vec4, found '" ++ @typeName(VecN) ++ "'"),
};
}
// Negates a given vector
pub inline fn negate(a: *const VecN) VecN {
return switch (VecN.n) {
inline 2 => .{ .v = math.vec2(-1, -1).v * a.v },
inline 3 => .{ .v = math.vec3(-1, -1, -1).v * a.v },
inline 4 => .{ .v = math.vec4(-1, -1, -1, -1).v * a.v },
else => @compileError("Expected Vec2, Vec3, Vec4, found '" ++ @typeName(VecN) ++ "'"),
};
}
// Returns the largest scalar of two vectors
pub inline fn maxScalar(a: *const VecN, b: *const VecN) Scalar {
var max_scalar: Scalar = a.v[0];
inline for (0..VecN.n) |i| {
if (a.v[i] > max_scalar)
max_scalar = a.v[i];
if (b.v[i] > max_scalar)
max_scalar = b.v[i];
}
return max_scalar;
}
// Returns the smallest scalar of two vectors
pub inline fn minScalar(a: *const VecN, b: *const VecN) Scalar {
var min_scalar: Scalar = a.v[0];
inline for (0..VecN.n) |i| {
if (a.v[i] < min_scalar)
min_scalar = a.v[i];
if (b.v[i] < min_scalar)
min_scalar = b.v[i];
}
return min_scalar;
}
};
}
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: *const VecN, b: *const VecN) VecN {
// pub inline fn sub(a: *const VecN, b: *const VecN) VecN {
// pub inline fn div(a: *const VecN, b: *const VecN) VecN {
// pub inline fn mul(a: *const VecN, b: *const VecN) VecN {
// pub inline fn addScalar(a: *const VecN, s: Scalar) VecN {
// pub inline fn subScalar(a: *const VecN, s: Scalar) VecN {
// pub inline fn divScalar(a: *const VecN, s: Scalar) VecN {
// pub inline fn mulScalar(a: *const 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);
// }
// }