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 }