lib/linear/src/vector.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 //! Vectors of two, three, and four f32 lanes.
  2 //!
  3 //! Each vector is an extern struct with the layout of `[N]f32`, so it can sit
  4 //! inside extern records and cross a C ABI unchanged. Every operation runs in
  5 //! strict float mode and evaluates lanes in the order written here: sums fold
  6 //! left from x, and no multiply fuses with an add. Results therefore agree bit
  7 //! for bit across x86-64 and aarch64.
  8 const std = @import("std");
  9 
 10 const assert = std.debug.assert;
 11 
 12 pub const Vec2 = extern struct {
 13     x: f32 = 0,
 14     y: f32 = 0,
 15 
 16     pub const zero: Vec2 = .{};
 17 
 18     pub fn init(x: f32, y: f32) Vec2 {
 19         return .{ .x = x, .y = y };
 20     }
 21 
 22     pub fn splat(value: f32) Vec2 {
 23         return .{ .x = value, .y = value };
 24     }
 25 
 26     pub fn fromArray(lanes: [2]f32) Vec2 {
 27         return .{ .x = lanes[0], .y = lanes[1] };
 28     }
 29 
 30     pub fn toArray(v: Vec2) [2]f32 {
 31         return .{ v.x, v.y };
 32     }
 33 
 34     pub fn add(a: Vec2, b: Vec2) Vec2 {
 35         return .{ .x = a.x + b.x, .y = a.y + b.y };
 36     }
 37 
 38     pub fn sub(a: Vec2, b: Vec2) Vec2 {
 39         return .{ .x = a.x - b.x, .y = a.y - b.y };
 40     }
 41 
 42     /// Lane-wise product.
 43     pub fn mul(a: Vec2, b: Vec2) Vec2 {
 44         return .{ .x = a.x * b.x, .y = a.y * b.y };
 45     }
 46 
 47     pub fn scale(v: Vec2, s: f32) Vec2 {
 48         return .{ .x = v.x * s, .y = v.y * s };
 49     }
 50 
 51     pub fn negate(v: Vec2) Vec2 {
 52         return .{ .x = -v.x, .y = -v.y };
 53     }
 54 
 55     pub fn dot(a: Vec2, b: Vec2) f32 {
 56         return a.x * b.x + a.y * b.y;
 57     }
 58 
 59     pub fn lengthSq(v: Vec2) f32 {
 60         return v.dot(v);
 61     }
 62 
 63     pub fn length(v: Vec2) f32 {
 64         return @sqrt(v.lengthSq());
 65     }
 66 
 67     pub fn distanceSq(a: Vec2, b: Vec2) f32 {
 68         return a.sub(b).lengthSq();
 69     }
 70 
 71     pub fn distance(a: Vec2, b: Vec2) f32 {
 72         return @sqrt(a.distanceSq(b));
 73     }
 74 
 75     /// The unit vector along `v`, or null when its length is at most
 76     /// `min_length` or is NaN.
 77     pub fn normalized(v: Vec2, min_length: f32) ?Vec2 {
 78         assert(min_length >= 0);
 79         const len = v.length();
 80         if (!(len > min_length)) return null;
 81         return v.scale(1 / len);
 82     }
 83 
 84     /// `a` at `t = 0` and `b` at `t = 1`.
 85     pub fn lerp(a: Vec2, b: Vec2, t: f32) Vec2 {
 86         return a.add(b.sub(a).scale(t));
 87     }
 88 
 89     pub fn min(a: Vec2, b: Vec2) Vec2 {
 90         return .{ .x = @min(a.x, b.x), .y = @min(a.y, b.y) };
 91     }
 92 
 93     pub fn max(a: Vec2, b: Vec2) Vec2 {
 94         return .{ .x = @max(a.x, b.x), .y = @max(a.y, b.y) };
 95     }
 96 
 97     pub fn abs(v: Vec2) Vec2 {
 98         return .{ .x = @abs(v.x), .y = @abs(v.y) };
 99     }
100 };
101 
102 pub const Vec3 = extern struct {
103     x: f32 = 0,
104     y: f32 = 0,
105     z: f32 = 0,
106 
107     pub const zero: Vec3 = .{};
108 
109     pub fn init(x: f32, y: f32, z: f32) Vec3 {
110         return .{ .x = x, .y = y, .z = z };
111     }
112 
113     pub fn splat(value: f32) Vec3 {
114         return .{ .x = value, .y = value, .z = value };
115     }
116 
117     pub fn fromArray(lanes: [3]f32) Vec3 {
118         return .{ .x = lanes[0], .y = lanes[1], .z = lanes[2] };
119     }
120 
121     pub fn toArray(v: Vec3) [3]f32 {
122         return .{ v.x, v.y, v.z };
123     }
124 
125     pub fn add(a: Vec3, b: Vec3) Vec3 {
126         return .{ .x = a.x + b.x, .y = a.y + b.y, .z = a.z + b.z };
127     }
128 
129     pub fn sub(a: Vec3, b: Vec3) Vec3 {
130         return .{ .x = a.x - b.x, .y = a.y - b.y, .z = a.z - b.z };
131     }
132 
133     /// Lane-wise product.
134     pub fn mul(a: Vec3, b: Vec3) Vec3 {
135         return .{ .x = a.x * b.x, .y = a.y * b.y, .z = a.z * b.z };
136     }
137 
138     pub fn scale(v: Vec3, s: f32) Vec3 {
139         return .{ .x = v.x * s, .y = v.y * s, .z = v.z * s };
140     }
141 
142     pub fn negate(v: Vec3) Vec3 {
143         return .{ .x = -v.x, .y = -v.y, .z = -v.z };
144     }
145 
146     pub fn dot(a: Vec3, b: Vec3) f32 {
147         return a.x * b.x + a.y * b.y + a.z * b.z;
148     }
149 
150     pub fn cross(a: Vec3, b: Vec3) Vec3 {
151         return .{
152             .x = a.y * b.z - a.z * b.y,
153             .y = a.z * b.x - a.x * b.z,
154             .z = a.x * b.y - a.y * b.x,
155         };
156     }
157 
158     pub fn lengthSq(v: Vec3) f32 {
159         return v.dot(v);
160     }
161 
162     pub fn length(v: Vec3) f32 {
163         return @sqrt(v.lengthSq());
164     }
165 
166     pub fn distanceSq(a: Vec3, b: Vec3) f32 {
167         return a.sub(b).lengthSq();
168     }
169 
170     pub fn distance(a: Vec3, b: Vec3) f32 {
171         return @sqrt(a.distanceSq(b));
172     }
173 
174     /// The unit vector along `v`, or null when its length is at most
175     /// `min_length` or is NaN.
176     pub fn normalized(v: Vec3, min_length: f32) ?Vec3 {
177         assert(min_length >= 0);
178         const len = v.length();
179         if (!(len > min_length)) return null;
180         return v.scale(1 / len);
181     }
182 
183     /// `v` shortened to `max_length` when it is longer, else `v` unchanged.
184     pub fn clampLength(v: Vec3, max_length: f32) Vec3 {
185         assert(max_length >= 0);
186         const len = v.length();
187         if (len > max_length) return v.scale(max_length / len);
188         return v;
189     }
190 
191     /// `a` at `t = 0` and `b` at `t = 1`.
192     pub fn lerp(a: Vec3, b: Vec3, t: f32) Vec3 {
193         return a.add(b.sub(a).scale(t));
194     }
195 
196     pub fn min(a: Vec3, b: Vec3) Vec3 {
197         return .{ .x = @min(a.x, b.x), .y = @min(a.y, b.y), .z = @min(a.z, b.z) };
198     }
199 
200     pub fn max(a: Vec3, b: Vec3) Vec3 {
201         return .{ .x = @max(a.x, b.x), .y = @max(a.y, b.y), .z = @max(a.z, b.z) };
202     }
203 
204     pub fn abs(v: Vec3) Vec3 {
205         return .{ .x = @abs(v.x), .y = @abs(v.y), .z = @abs(v.z) };
206     }
207 
208     pub fn minComponent(v: Vec3) f32 {
209         return @min(v.x, @min(v.y, v.z));
210     }
211 
212     pub fn maxComponent(v: Vec3) f32 {
213         return @max(v.x, @max(v.y, v.z));
214     }
215 };
216 
217 pub const Vec4 = extern struct {
218     x: f32 = 0,
219     y: f32 = 0,
220     z: f32 = 0,
221     w: f32 = 0,
222 
223     pub const zero: Vec4 = .{};
224 
225     pub fn init(x: f32, y: f32, z: f32, w: f32) Vec4 {
226         return .{ .x = x, .y = y, .z = z, .w = w };
227     }
228 
229     pub fn splat(value: f32) Vec4 {
230         return .{ .x = value, .y = value, .z = value, .w = value };
231     }
232 
233     /// A homogeneous vector: `w = 1` for a point, `w = 0` for a direction.
234     pub fn fromVec3(v: Vec3, w: f32) Vec4 {
235         return .{ .x = v.x, .y = v.y, .z = v.z, .w = w };
236     }
237 
238     pub fn fromArray(lanes: [4]f32) Vec4 {
239         return .{ .x = lanes[0], .y = lanes[1], .z = lanes[2], .w = lanes[3] };
240     }
241 
242     pub fn toArray(v: Vec4) [4]f32 {
243         return .{ v.x, v.y, v.z, v.w };
244     }
245 
246     pub fn xyz(v: Vec4) Vec3 {
247         return .{ .x = v.x, .y = v.y, .z = v.z };
248     }
249 
250     pub fn add(a: Vec4, b: Vec4) Vec4 {
251         return .{ .x = a.x + b.x, .y = a.y + b.y, .z = a.z + b.z, .w = a.w + b.w };
252     }
253 
254     pub fn sub(a: Vec4, b: Vec4) Vec4 {
255         return .{ .x = a.x - b.x, .y = a.y - b.y, .z = a.z - b.z, .w = a.w - b.w };
256     }
257 
258     /// Lane-wise product.
259     pub fn mul(a: Vec4, b: Vec4) Vec4 {
260         return .{ .x = a.x * b.x, .y = a.y * b.y, .z = a.z * b.z, .w = a.w * b.w };
261     }
262 
263     pub fn scale(v: Vec4, s: f32) Vec4 {
264         return .{ .x = v.x * s, .y = v.y * s, .z = v.z * s, .w = v.w * s };
265     }
266 
267     pub fn negate(v: Vec4) Vec4 {
268         return .{ .x = -v.x, .y = -v.y, .z = -v.z, .w = -v.w };
269     }
270 
271     pub fn dot(a: Vec4, b: Vec4) f32 {
272         return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;
273     }
274 
275     pub fn lengthSq(v: Vec4) f32 {
276         return v.dot(v);
277     }
278 
279     pub fn length(v: Vec4) f32 {
280         return @sqrt(v.lengthSq());
281     }
282 
283     /// The unit vector along `v`, or null when its length is at most
284     /// `min_length` or is NaN.
285     pub fn normalized(v: Vec4, min_length: f32) ?Vec4 {
286         assert(min_length >= 0);
287         const len = v.length();
288         if (!(len > min_length)) return null;
289         return v.scale(1 / len);
290     }
291 
292     /// `a` at `t = 0` and `b` at `t = 1`.
293     pub fn lerp(a: Vec4, b: Vec4, t: f32) Vec4 {
294         return a.add(b.sub(a).scale(t));
295     }
296 
297     pub fn min(a: Vec4, b: Vec4) Vec4 {
298         return .{ .x = @min(a.x, b.x), .y = @min(a.y, b.y), .z = @min(a.z, b.z), .w = @min(a.w, b.w) };
299     }
300 
301     pub fn max(a: Vec4, b: Vec4) Vec4 {
302         return .{ .x = @max(a.x, b.x), .y = @max(a.y, b.y), .z = @max(a.z, b.z), .w = @max(a.w, b.w) };
303     }
304 
305     pub fn abs(v: Vec4) Vec4 {
306         return .{ .x = @abs(v.x), .y = @abs(v.y), .z = @abs(v.z), .w = @abs(v.w) };
307     }
308 };
309 
310 comptime {
311     assert(@sizeOf(Vec2) == @sizeOf([2]f32));
312     assert(@alignOf(Vec2) == @alignOf(f32));
313     assert(@sizeOf(Vec3) == @sizeOf([3]f32));
314     assert(@alignOf(Vec3) == @alignOf(f32));
315     assert(@sizeOf(Vec4) == @sizeOf([4]f32));
316     assert(@alignOf(Vec4) == @alignOf(f32));
317 }
318 
319 const testing = std.testing;
320 
321 fn expectApproxVec3(expected: Vec3, actual: Vec3, tolerance: f32) !void {
322     try testing.expectApproxEqAbs(expected.x, actual.x, tolerance);
323     try testing.expectApproxEqAbs(expected.y, actual.y, tolerance);
324     try testing.expectApproxEqAbs(expected.z, actual.z, tolerance);
325 }
326 
327 test "vector arithmetic evaluates each lane exactly" {
328     const a = Vec3.init(1, 2, 3);
329     const b = Vec3.init(4, -5, 6);
330     try testing.expectEqual(Vec3.init(5, -3, 9), a.add(b));
331     try testing.expectEqual(Vec3.init(-3, 7, -3), a.sub(b));
332     try testing.expectEqual(Vec3.init(4, -10, 18), a.mul(b));
333     try testing.expectEqual(Vec3.init(2, 4, 6), a.scale(2));
334     try testing.expectEqual(@as(f32, 12), a.dot(b));
335     try testing.expectEqual(Vec3.init(27, 6, -13), a.cross(b));
336     try testing.expectEqual(Vec3.init(3, 2, 1), Vec3.fromArray(.{ 3, 2, 1 }));
337     try testing.expectEqual([3]f32{ 1, 2, 3 }, a.toArray());
338     try testing.expectEqual(@as(f32, 3), a.maxComponent());
339     try testing.expectEqual(@as(f32, -5), b.minComponent());
340 }
341 
342 test "cross product follows the right-hand rule" {
343     const x = Vec3.init(1, 0, 0);
344     const y = Vec3.init(0, 1, 0);
345     try testing.expectEqual(Vec3.init(0, 0, 1), x.cross(y));
346     try testing.expectEqual(Vec3.init(0, 0, -1), y.cross(x));
347 }
348 
349 test "normalized returns null at or below the minimum length" {
350     try testing.expectEqual(@as(?Vec3, null), Vec3.zero.normalized(0));
351     try testing.expectEqual(@as(?Vec3, null), Vec3.init(1e-9, 0, 0).normalized(1e-8));
352     try testing.expectEqual(@as(?Vec3, null), Vec3.init(std.math.nan(f32), 0, 0).normalized(0));
353     try testing.expectEqual(@as(?Vec2, null), Vec2.zero.normalized(0));
354     try testing.expectEqual(@as(?Vec4, null), Vec4.zero.normalized(0));
355     try expectApproxVec3(Vec3.init(0, 0.6, 0.8), Vec3.init(0, 3, 4).normalized(0).?, 1e-7);
356     try testing.expectEqual(Vec2.init(1, 0), Vec2.init(1e-9, 0).normalized(0).?);
357 }
358 
359 test "normalized multiplies by the reciprocal length" {
360     const v = Vec3.init(1, 2, 2);
361     const inv: f32 = 1.0 / 3.0;
362     try testing.expectEqual(Vec3.init(1 * inv, 2 * inv, 2 * inv), v.normalized(0).?);
363 }
364 
365 test "clampLength shortens only longer vectors" {
366     try testing.expectEqual(Vec3.init(0, 3, 4), Vec3.init(0, 3, 4).clampLength(5));
367     try expectApproxVec3(Vec3.init(0, 0.6, 0.8), Vec3.init(0, 3, 4).clampLength(1), 1e-7);
368     try testing.expectEqual(Vec3.zero, Vec3.zero.clampLength(0));
369 }
370 
371 test "lerp reaches both endpoints" {
372     const a = Vec4.init(1, -2, 3, 0);
373     const b = Vec4.init(5, 6, -7, 1);
374     try testing.expectEqual(a, a.lerp(b, 0));
375     try testing.expectEqual(b, a.lerp(b, 1));
376     try testing.expectEqual(Vec4.init(3, 2, -2, 0.5), a.lerp(b, 0.5));
377 }
378 
379 test "homogeneous vectors carry their w lane" {
380     const p = Vec4.fromVec3(Vec3.init(1, 2, 3), 1);
381     try testing.expectEqual(Vec4.init(1, 2, 3, 1), p);
382     try testing.expectEqual(Vec3.init(1, 2, 3), p.xyz());
383 }
384 
385 test "vectors share the layout of f32 arrays" {
386     const v = Vec3.init(1, 2, 3);
387     const lanes: *const [3]f32 = @ptrCast(&v);
388     try testing.expectEqual([3]f32{ 1, 2, 3 }, lanes.*);
389     try testing.expectEqual(@as(usize, 4), @offsetOf(Vec3, "y"));
390     try testing.expectEqual(@as(usize, 12), @offsetOf(Vec4, "w"));
391 }