littlejsengine 1.24.1 → 1.24.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +59 -1
  2. package/EDITOR.md +2 -0
  3. package/FAQ.md +13 -2
  4. package/README.md +1 -1
  5. package/REFERENCE.md +20 -6
  6. package/dist/littlejs.d.ts +77 -13
  7. package/dist/littlejs.esm.js +2395 -1939
  8. package/dist/littlejs.esm.min.js +3 -3
  9. package/dist/littlejs.js +2392 -1939
  10. package/dist/littlejs.min.js +2 -2
  11. package/dist/littlejs.release.js +2356 -1952
  12. package/package.json +1 -1
  13. package/plugins/gltf.js +62 -9
  14. package/plugins/lightSystem.js +1 -1
  15. package/plugins/math3d.js +1014 -1013
  16. package/plugins/medalSystem.js +7 -11
  17. package/plugins/particleEffects.js +2 -2
  18. package/plugins/pathFinder.js +2 -2
  19. package/plugins/pluginExport.js +1 -0
  20. package/plugins/render3d.js +131 -55
  21. package/plugins/render3dEditor.js +24 -10
  22. package/plugins/render3dExtras.js +29 -17
  23. package/plugins/render3dLevel.js +21 -12
  24. package/plugins/render3dVoxels.js +2 -2
  25. package/plugins/textureSheet.js +97 -3
  26. package/plugins/tweakables.js +1 -1
  27. package/plugins/tweenSystem.js +2 -2
  28. package/plugins/uiSystem.js +6 -2
  29. package/src/engine.js +34 -11
  30. package/src/engineAudio.js +13 -3
  31. package/src/engineDebug.js +4 -1
  32. package/src/engineDraw.js +29 -16
  33. package/src/engineEditor.js +82 -50
  34. package/src/engineExport.js +2 -0
  35. package/src/engineInput.js +17 -3
  36. package/src/engineMath.js +6 -2
  37. package/src/engineObject.js +675 -675
  38. package/src/engineParticles.js +1 -1
  39. package/src/engineSettings.js +16 -0
  40. package/src/engineTileLayer.js +85 -22
  41. package/src/engineUtilities.js +20 -8
  42. package/src/engineWebGL.js +12 -8
package/plugins/math3d.js CHANGED
@@ -1,1013 +1,1014 @@
1
- /**
2
- * LittleJS 3D Math Plugin
3
- * - Vector3 and Matrix4 for 3D games and plugins
4
- * - Right handed, Y up, angles in radians
5
- * - Used by the Render3D plugin, but has no rendering dependencies
6
- * @namespace Math3D
7
- */
8
-
9
- 'use strict';
10
-
11
- ///////////////////////////////////////////////////////////////////////////////
12
-
13
- /**
14
- * Create a 3D vector, can take 0, 1, 2 or 3 numbers
15
- * - vec3() is zero, vec3(s) fills all three, vec3(x, y) sets z to 0
16
- * @param {number} [x]
17
- * @param {number} [y]
18
- * @param {number} [z]
19
- * @return {Vector3}
20
- * @memberof Math3D
21
- */
22
- function vec3(x=0, y, z)
23
- {
24
- return y === undefined ? new Vector3(x, x, x) : new Vector3(x, y, z === undefined ? 0 : z);
25
- }
26
-
27
- /**
28
- * Check if the object is a valid Vector3
29
- * @param {any} v
30
- * @return {boolean}
31
- * @memberof Math3D
32
- */
33
- function isVector3(v) { return v instanceof Vector3 && v.isValid(); }
34
-
35
- // debug check that a value is a usable Vector3, stripped in release like the 2D one
36
- function ASSERT_VECTOR3_VALID(v) { ASSERT(isVector3(v), 'Vector3 is invalid.', v); }
37
-
38
- /**
39
- * Returns a random Vector3 of a given length, pointing any direction evenly, or within a cone around +Y
40
- * @param {number} [length]
41
- * @param {number} [coneAngle] - Half angle of the cone around +Y in radians, PI is every direction
42
- * @return {Vector3}
43
- * @memberof Math3D
44
- */
45
- function randVector3(length=1, coneAngle=PI)
46
- {
47
- // a random height on the sphere is uniform over its surface, then a random turn around Y
48
- const y = rand(cos(coneAngle), 1), s = (1 - y * y) ** .5, a = rand(2 * PI);
49
- return new Vector3(s * cos(a) * length, y * length, s * sin(a) * length);
50
- }
51
-
52
- /**
53
- * Returns a random Vector3 inside a sphere, spread evenly through its volume, the 3D twin of randInCircle
54
- * @param {number} [radius]
55
- * @param {number} [minRadius] - Leave a hollow middle this big
56
- * @return {Vector3}
57
- * @memberof Math3D
58
- */
59
- function randInSphere(radius=1, minRadius=0)
60
- {
61
- // the volume inside a radius grows with its cube, so that is what has to come out even
62
- if (radius <= 0) return new Vector3;
63
- const ratio = clamp(minRadius / radius);
64
- return randVector3(radius * rand(ratio**3, 1) ** (1/3));
65
- }
66
-
67
- /**
68
- * 3D Vector object, right handed with Y up
69
- * - Methods return new vectors except set and setFrom
70
- * @memberof Math3D
71
- * @example
72
- * const a = vec3(1, 2, 3);
73
- * const b = a.add(vec3(0, 1, 0)).normalize();
74
- */
75
- class Vector3
76
- {
77
- /** Create a 3D vector
78
- * @param {number} [x]
79
- * @param {number} [y]
80
- * @param {number} [z] */
81
- constructor(x=0, y=0, z=0)
82
- {
83
- ASSERT(isNumber(x) && isNumber(y) && isNumber(z), 'Vector3 components must be numbers');
84
- /** @property {number} - X axis location */
85
- this.x = x;
86
- /** @property {number} - Y axis location */
87
- this.y = y;
88
- /** @property {number} - Z axis location */
89
- this.z = z;
90
- }
91
-
92
- /** Sets values of this vector and returns self
93
- * @param {number} [x]
94
- * @param {number} [y]
95
- * @param {number} [z]
96
- * @return {Vector3} */
97
- set(x=0, y=0, z=0) { this.x = x; this.y = y; this.z = z; ASSERT_VECTOR3_VALID(this); return this; }
98
-
99
- /** Copies the values of another vector into this one and returns self
100
- * @param {Vector3} v
101
- * @return {Vector3} */
102
- setFrom(v) { return this.set(v.x, v.y, v.z); }
103
-
104
- /** Returns a new vector that is a copy of this
105
- * @return {Vector3} */
106
- copy() { return new Vector3(this.x, this.y, this.z); }
107
-
108
- /** Returns a copy of this vector plus the vector passed in
109
- * @param {Vector3} v
110
- * @return {Vector3} */
111
- add(v) { return new Vector3(this.x + v.x, this.y + v.y, this.z + v.z); }
112
-
113
- /** Returns a copy of this vector minus the vector passed in
114
- * @param {Vector3} v
115
- * @return {Vector3} */
116
- subtract(v) { return new Vector3(this.x - v.x, this.y - v.y, this.z - v.z); }
117
-
118
- /** Returns a copy of this vector times the vector passed in
119
- * @param {Vector3} v
120
- * @return {Vector3} */
121
- multiply(v) { return new Vector3(this.x * v.x, this.y * v.y, this.z * v.z); }
122
-
123
- /** Returns a copy of this vector divided by the vector passed in
124
- * @param {Vector3} v
125
- * @return {Vector3} */
126
- divide(v) { return new Vector3(this.x / v.x, this.y / v.y, this.z / v.z); }
127
-
128
- /** Returns a copy of this vector scaled by the number passed in
129
- * @param {number} s
130
- * @return {Vector3} */
131
- scale(s) { return new Vector3(this.x * s, this.y * s, this.z * s); }
132
-
133
- /** Returns the length of this vector
134
- * @return {number} */
135
- length() { return this.lengthSquared()**.5; }
136
-
137
- /** Returns the length of this vector squared
138
- * @return {number} */
139
- lengthSquared() { return this.x**2 + this.y**2 + this.z**2; }
140
-
141
- /** Returns a copy of this vector reflected by a surface normal
142
- * @param {Vector3} normal - Surface normal, should be normalized
143
- * @param {number} [restitution] - How much to bounce, 1 is a perfect bounce, 0 slides along the surface
144
- * @return {Vector3} */
145
- reflect(normal, restitution=1) { return this.subtract(normal.scale((1 + restitution) * this.dot(normal))); }
146
-
147
- /** Returns the distance from this vector to the vector passed in
148
- * @param {Vector3} v
149
- * @return {number} */
150
- distance(v) { return this.distanceSquared(v)**.5; }
151
-
152
- /** Returns the distance squared from this vector to the vector passed in
153
- * @param {Vector3} v
154
- * @return {number} */
155
- distanceSquared(v) { return (this.x - v.x)**2 + (this.y - v.y)**2 + (this.z - v.z)**2; }
156
-
157
- /** Returns a new vector in the same direction with the length passed in, zero stays zero
158
- * @param {number} [length]
159
- * @return {Vector3} */
160
- normalize(length=1)
161
- {
162
- const l = this.length();
163
- return l ? this.scale(length/l) : new Vector3;
164
- }
165
-
166
- /** Returns a new vector clamped to the length passed in
167
- * @param {number} [length]
168
- * @return {Vector3} */
169
- clampLength(length=1)
170
- {
171
- const l = this.length();
172
- return l > length ? this.scale(length/l) : this.copy();
173
- }
174
-
175
- /** Returns the dot product of this vector and the vector passed in
176
- * @param {Vector3} v
177
- * @return {number} */
178
- dot(v) { return this.x*v.x + this.y*v.y + this.z*v.z; }
179
-
180
- /** Returns a vector at right angles to both this and the one passed in
181
- * @param {Vector3} v
182
- * @return {Vector3} */
183
- cross(v)
184
- {
185
- return new Vector3(
186
- this.y*v.z - this.z*v.y,
187
- this.z*v.x - this.x*v.z,
188
- this.x*v.y - this.y*v.x);
189
- }
190
-
191
- /** Returns a new vector interpolated between this and the vector passed in, percent is clamped to 0-1
192
- * @param {Vector3} v
193
- * @param {number} percent
194
- * @return {Vector3} */
195
- lerp(v, percent)
196
- {
197
- ASSERT_VECTOR3_VALID(v);
198
- return this.add(v.subtract(this).scale(clamp(percent)));
199
- }
200
-
201
- /** Returns a new vector turned around an axis, counter clockwise when the axis points at you
202
- * @param {Vector3} axis - Unit length
203
- * @param {number} angle - Radians
204
- * @return {Vector3} */
205
- rotate(axis, angle)
206
- {
207
- ASSERT_VECTOR3_VALID(axis); // unlike Vector2.rotate this takes an axis first
208
- // Rodrigues' formula: the part along the axis stays, the rest turns
209
- const c = cos(angle), s = sin(angle), d = axis.dot(this) * (1 - c);
210
- return this.scale(c).add(axis.cross(this).scale(s)).add(axis.scale(d));
211
- }
212
-
213
- /** Returns a new vector turned around the X axis, the way a positive pitch in rotation3D turns things
214
- * @param {number} angle - Radians
215
- * @return {Vector3} */
216
- rotateX(angle)
217
- {
218
- const c = cos(angle), s = sin(angle);
219
- return new Vector3(this.x, this.y*c - this.z*s, this.y*s + this.z*c);
220
- }
221
-
222
- /** Returns a new vector turned around the Y axis, the way a positive yaw in rotation3D turns things
223
- * @param {number} angle - Radians
224
- * @return {Vector3} */
225
- rotateY(angle)
226
- {
227
- const c = cos(angle), s = sin(angle);
228
- return new Vector3(this.x*c + this.z*s, this.y, this.z*c - this.x*s);
229
- }
230
-
231
- /** Returns a new vector turned around the Z axis, the way a positive roll in rotation3D turns things
232
- * @param {number} angle - Radians
233
- * @return {Vector3} */
234
- rotateZ(angle)
235
- {
236
- const c = cos(angle), s = sin(angle);
237
- return new Vector3(this.x*c - this.y*s, this.x*s + this.y*c, this.z);
238
- }
239
-
240
- /** Returns a new vector with the absolute value of each component
241
- * @return {Vector3} */
242
- abs() { return new Vector3(abs(this.x), abs(this.y), abs(this.z)); }
243
-
244
- /** Returns a new vector with each component floored
245
- * @return {Vector3} */
246
- floor() { return new Vector3(floor(this.x), floor(this.y), floor(this.z)); }
247
-
248
- /** Returns a new vector with each component rounded
249
- * @return {Vector3} */
250
- round() { return new Vector3(round(this.x), round(this.y), round(this.z)); }
251
-
252
- /** Returns a new vector snapped down to a grid, grid is the number of steps per unit like Vector2.snap
253
- * @param {number} grid - Snap steps per unit, 2 snaps to halves
254
- * @return {Vector3} */
255
- snap(grid)
256
- {
257
- ASSERT_NUMBER_VALID(grid);
258
- return new Vector3(floor(this.x*grid)/grid, floor(this.y*grid)/grid, floor(this.z*grid)/grid);
259
- }
260
-
261
- /** Returns this point transformed by a matrix, translation included
262
- * @param {Matrix4} matrix
263
- * @return {Vector3} */
264
- transform(matrix) { return matrix.transformPoint(this); }
265
-
266
- /** Returns this direction transformed by a matrix, rotation and scale only
267
- * @param {Matrix4} matrix
268
- * @return {Vector3} */
269
- transformDirection(matrix) { return matrix.transformDirection(this); }
270
-
271
- /** Checks if this is a valid vector
272
- * @return {boolean} */
273
- isValid() { return isNumber(this.x) && isNumber(this.y) && isNumber(this.z); }
274
-
275
- /** Returns a string representation of this vector for debugging
276
- * @param {number} [digits] - Number of digits to display
277
- * @return {string} */
278
- toString(digits=3)
279
- {
280
- if (!this.isValid())
281
- return `(${this.x},${this.y},${this.z})`; // show the bad values instead of throwing
282
- const f = (v)=> (v < 0 ? '' : ' ') + v.toFixed(digits);
283
- return `(${f(this.x)},${f(this.y)},${f(this.z)} )`;
284
- }
285
- }
286
-
287
- ///////////////////////////////////////////////////////////////////////////////
288
-
289
- // scratch for multiply, nothing keeps a reference to it
290
- const matrix4Scratch = new Float32Array(16);
291
- const matrix4Identity = new Float32Array([1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]);
292
-
293
- /**
294
- * 4x4 transform matrix for moving, rotating and scaling points in 3D
295
- * - Static builders like Matrix4.translation return a new matrix
296
- * - Methods on a matrix change it in place and return it, so calls can chain
297
- * - a.multiply(b) means b happens first, then a
298
- * - Stored the way WebGL wants it, so it can be sent to a shader as is
299
- * @memberof Math3D
300
- * @example
301
- * const m = buildMatrix(vec3(0, 1, 0), vec3(0, PI/2, 0)); // rotate then move up
302
- * const p = m.transformPoint(vec3(1, 0, 0));
303
- */
304
- class Matrix4
305
- {
306
- /** Create a matrix, identity by default
307
- * @param {Float32Array|Array<number>} [m] - 16 column major values */
308
- constructor(m)
309
- {
310
- /** @property {Float32Array} - The 16 column major values */
311
- this.m = new Float32Array(16);
312
- ASSERT(!m || m.length == 16, 'Matrix4 takes 16 values, use copy() to duplicate a matrix');
313
- if (m)
314
- this.m.set(m);
315
- else
316
- this.m[0] = this.m[5] = this.m[10] = this.m[15] = 1;
317
- }
318
-
319
- /** Returns a new identity matrix
320
- * @return {Matrix4} */
321
- static identity() { return new Matrix4; }
322
-
323
- /** Returns a new translation matrix
324
- * @param {Vector3} v
325
- * @return {Matrix4} */
326
- static translation(v)
327
- {
328
- ASSERT_VECTOR3_VALID(v);
329
- const r = new Matrix4;
330
- r.m[12] = v.x; r.m[13] = v.y; r.m[14] = v.z;
331
- return r;
332
- }
333
-
334
- /** Returns a rotation matrix, rolled first, then pitched, then yawed
335
- * @param {Vector3} euler - vec3(pitch, yaw, roll) in radians
336
- * @param {Matrix4} [matrix] - Written into instead of a new one, for a loop that builds many
337
- * @return {Matrix4} */
338
- static rotation(euler, matrix=new Matrix4)
339
- {
340
- ASSERT_VECTOR3_VALID(euler);
341
- matrix4RotationAt(matrix.m, 0, euler);
342
- return matrix;
343
- }
344
-
345
- /** Returns a new scale matrix
346
- * @param {Vector3} v
347
- * @return {Matrix4} */
348
- static scaling(v)
349
- {
350
- ASSERT_VECTOR3_VALID(v);
351
- const r = new Matrix4;
352
- r.m[0] = v.x; r.m[5] = v.y; r.m[10] = v.z;
353
- return r;
354
- }
355
-
356
- /** Returns a new perspective projection, camera looks down -Z
357
- * @param {number} fov - Vertical field of view in radians
358
- * @param {number} aspect - Width divided by height
359
- * @param {number} near - Closest visible distance
360
- * @param {number} far - Furthest visible distance, Infinity is allowed
361
- * @return {Matrix4} */
362
- static perspective(fov, aspect, near, far)
363
- {
364
- ASSERT(near > 0 && far > near, 'a perspective projection needs 0 < near < far, or nothing is visible', near, far);
365
- const f = 1 / tan(fov/2);
366
- const r = new Matrix4;
367
- const m = r.m;
368
- m[0] = f / aspect;
369
- m[5] = f;
370
- m[10] = far == Infinity ? -1 : (far + near) / (near - far); // the infinite case is the limit of the formula
371
- m[11] = -1;
372
- m[14] = far == Infinity ? -2 * near : 2 * far * near / (near - far);
373
- m[15] = 0;
374
- return r;
375
- }
376
-
377
- /** Returns a new orthographic projection, camera looks down -Z
378
- * @param {number} left - Edge of the visible box
379
- * @param {number} right - Edge of the visible box
380
- * @param {number} bottom - Edge of the visible box
381
- * @param {number} top - Edge of the visible box
382
- * @param {number} near - Closest visible distance
383
- * @param {number} far - Furthest visible distance, Infinity is not allowed here
384
- * @return {Matrix4} */
385
- static orthographic(left, right, bottom, top, near, far)
386
- {
387
- // an infinite far plane has no orthographic form: every depth would land on the near plane,
388
- // and the formula below works out to NaN, which quietly clips the whole scene away
389
- ASSERT(far > near && far != Infinity, 'an orthographic projection needs a real far plane past near, Infinity is perspective only', near, far);
390
- const r = new Matrix4;
391
- const m = r.m;
392
- m[0] = 2 / (right - left);
393
- m[5] = 2 / (top - bottom);
394
- m[10] = -2 / (far - near);
395
- m[12] = -(right + left) / (right - left);
396
- m[13] = -(top + bottom) / (top - bottom);
397
- m[14] = -(far + near) / (far - near);
398
- return r;
399
- }
400
-
401
- /** Returns the transform of something at eye turned to face target
402
- * - Invert it to get a view matrix for a camera there
403
- * @param {Vector3} eye
404
- * @param {Vector3} target
405
- * @param {Vector3} [up]
406
- * @return {Matrix4} */
407
- static lookAt(eye, target, up=vec3(0, 1, 0))
408
- {
409
- let z = eye.subtract(target).normalize();
410
- if (!z.lengthSquared())
411
- z = vec3(0, 0, 1); // eye is on the target, face -Z
412
- let x = up.cross(z).normalize();
413
- // up is along the view direction, pick another; looking straight down or up keeps +X as the right axis,
414
- // as three.js does and as a view tilting there from the +Z side ends up
415
- if (!x.lengthSquared())
416
- x = (abs(z.y) > .99 ? vec3(0, 0, z.y > 0 ? -1 : 1) : vec3(0, 1, 0)).cross(z).normalize();
417
- const y = z.cross(x);
418
- return new Matrix4([x.x, x.y, x.z, 0, y.x, y.y, y.z, 0, z.x, z.y, z.z, 0, eye.x, eye.y, eye.z, 1]);
419
- }
420
-
421
- /** Returns a new matrix that is a copy of this
422
- * @return {Matrix4} */
423
- copy() { return new Matrix4(this.m); }
424
-
425
- /** Multiply this matrix by another and return this, the other happens first
426
- * @param {Matrix4} matrix
427
- * @return {Matrix4} */
428
- multiply(matrix)
429
- {
430
- const a = this.m, b = matrix.m, r = matrix4Scratch;
431
- for (let j = 0; j < 4; ++j)
432
- for (let i = 0; i < 4; ++i)
433
- r[j*4 + i] = a[i]*b[j*4] + a[4 + i]*b[j*4 + 1] + a[8 + i]*b[j*4 + 2] + a[12 + i]*b[j*4 + 3];
434
- this.m.set(r);
435
- return this;
436
- }
437
-
438
- /** Append a translation, returns self
439
- * @param {Vector3} v
440
- * @return {Matrix4} */
441
- translate(v) { return this.multiply(Matrix4.translation(v)); }
442
-
443
- /** Append a rotation, returns self
444
- * @param {Vector3} euler - vec3(pitch, yaw, roll) in radians
445
- * @return {Matrix4} */
446
- rotate(euler) { return this.multiply(Matrix4.rotation(euler)); }
447
-
448
- /** Append a scale, returns self
449
- * @param {Vector3} v
450
- * @return {Matrix4} */
451
- scale(v) { return this.multiply(Matrix4.scaling(v)); }
452
-
453
- /** Transpose this matrix in place, returns self
454
- * @return {Matrix4} */
455
- transpose()
456
- {
457
- const m = this.m;
458
- for (let i = 0; i < 4; ++i)
459
- for (let j = i + 1; j < 4; ++j)
460
- {
461
- const t = m[i*4 + j];
462
- m[i*4 + j] = m[j*4 + i];
463
- m[j*4 + i] = t;
464
- }
465
- return this;
466
- }
467
-
468
- /** Flip this matrix so it undoes itself, returns this and does nothing if it cannot be inverted
469
- * @return {Matrix4} */
470
- invert()
471
- {
472
- const m = this.m;
473
- const [a00, a01, a02, a03, a10, a11, a12, a13, a20, a21, a22, a23, a30, a31, a32, a33] = m;
474
- const b00 = a00*a11 - a01*a10, b01 = a00*a12 - a02*a10, b02 = a00*a13 - a03*a10;
475
- const b03 = a01*a12 - a02*a11, b04 = a01*a13 - a03*a11, b05 = a02*a13 - a03*a12;
476
- const b06 = a20*a31 - a21*a30, b07 = a20*a32 - a22*a30, b08 = a20*a33 - a23*a30;
477
- const b09 = a21*a32 - a22*a31, b10 = a21*a33 - a23*a31, b11 = a22*a33 - a23*a32;
478
- let det = b00*b11 - b01*b10 + b02*b09 + b03*b08 - b04*b07 + b05*b06;
479
- if (!det)
480
- return this;
481
- det = 1 / det;
482
- m[0] = (a11*b11 - a12*b10 + a13*b09) * det;
483
- m[1] = (a02*b10 - a01*b11 - a03*b09) * det;
484
- m[2] = (a31*b05 - a32*b04 + a33*b03) * det;
485
- m[3] = (a22*b04 - a21*b05 - a23*b03) * det;
486
- m[4] = (a12*b08 - a10*b11 - a13*b07) * det;
487
- m[5] = (a00*b11 - a02*b08 + a03*b07) * det;
488
- m[6] = (a32*b02 - a30*b05 - a33*b01) * det;
489
- m[7] = (a20*b05 - a22*b02 + a23*b01) * det;
490
- m[8] = (a10*b10 - a11*b08 + a13*b06) * det;
491
- m[9] = (a01*b08 - a00*b10 - a03*b06) * det;
492
- m[10] = (a30*b04 - a31*b02 + a33*b00) * det;
493
- m[11] = (a21*b02 - a20*b04 - a23*b00) * det;
494
- m[12] = (a11*b07 - a10*b09 - a12*b06) * det;
495
- m[13] = (a00*b09 - a01*b07 + a02*b06) * det;
496
- m[14] = (a31*b01 - a30*b03 - a32*b00) * det;
497
- m[15] = (a20*b03 - a21*b01 + a22*b00) * det;
498
- return this;
499
- }
500
-
501
- /** Transform a point, translation included
502
- * @param {Vector3} v
503
- * @return {Vector3} */
504
- transformPoint(v)
505
- {
506
- const m = this.m;
507
- return new Vector3(
508
- m[0]*v.x + m[4]*v.y + m[8]*v.z + m[12],
509
- m[1]*v.x + m[5]*v.y + m[9]*v.z + m[13],
510
- m[2]*v.x + m[6]*v.y + m[10]*v.z + m[14]);
511
- }
512
-
513
- /** Transform a direction, rotation and scale only
514
- * @param {Vector3} v
515
- * @return {Vector3} */
516
- transformDirection(v)
517
- {
518
- const m = this.m;
519
- return new Vector3(
520
- m[0]*v.x + m[4]*v.y + m[8]*v.z,
521
- m[1]*v.x + m[5]*v.y + m[9]*v.z,
522
- m[2]*v.x + m[6]*v.y + m[10]*v.z);
523
- }
524
-
525
- /** Returns the translation part of this matrix
526
- * @return {Vector3} */
527
- getTranslation() { return new Vector3(this.m[12], this.m[13], this.m[14]); }
528
-
529
- /** Returns the determinant of the rotation and scale part: negative when the matrix mirrors, 0 when it flattens a
530
- * shape and has no inverse
531
- * @return {number} */
532
- determinant()
533
- {
534
- const m = this.m;
535
- return m[0]*(m[5]*m[10] - m[6]*m[9]) - m[4]*(m[1]*m[10] - m[2]*m[9]) + m[8]*(m[1]*m[6] - m[2]*m[5]);
536
- }
537
-
538
- /** Returns the scale part of this matrix, the length of each axis; a mirroring matrix shows as a negative x
539
- * @return {Vector3} */
540
- getScale()
541
- {
542
- const m = this.m;
543
- const x = hypot(m[0], m[1], m[2]), y = hypot(m[4], m[5], m[6]), z = hypot(m[8], m[9], m[10]);
544
- return new Vector3(this.determinant() < 0 ? -x : x, y, z);
545
- }
546
-
547
- /** Returns the rotation part of this matrix as vec3(pitch, yaw, roll), the angles Matrix4.rotation builds it from
548
- * - The scale is divided out first, so it works on a full transform
549
- * - A matrix with shear, from a scaled parent with a turned child, has no exact answer and gets the nearest
550
- * @return {Vector3} */
551
- getRotation()
552
- {
553
- // the axes at unit length, see Matrix4.rotation for which element is which
554
- const m = this.m, s = this.getScale();
555
- const sx = s.x || 1, sy = s.y || 1, sz = s.z || 1;
556
- const m1 = m[1] / sx, m5 = m[5] / sy, m8 = m[8] / sz, m9 = m[9] / sz, m10 = m[10] / sz;
557
- const pitch = Math.asin(clamp(-m9, -1, 1));
558
- if (abs(m9) < 1 - 1e-6)
559
- return new Vector3(pitch, atan2(m8, m10), atan2(m1, m5));
560
- // straight up or down: yaw and roll turn about the same axis, so the roll is zero and yaw takes it all
561
- return new Vector3(pitch, atan2(m[4] / sy * -m9, m[0] / sx), 0);
562
- }
563
-
564
- /** Returns a string representation of this matrix for debugging
565
- * @return {string} */
566
- toString()
567
- {
568
- const m = this.m, f = (i)=> m[i].toFixed(2).padStart(7);
569
- let s = '';
570
- for (let row = 0; row < 4; ++row)
571
- s += `[${f(row)} ${f(4 + row)} ${f(8 + row)} ${f(12 + row)} ]\n`;
572
- return s;
573
- }
574
- }
575
-
576
- ///////////////////////////////////////////////////////////////////////////////
577
-
578
- /**
579
- * Build a transform for an object from its position, rotation and scale
580
- * - A point is scaled first, then rotated, then moved, which is what you want for a game object
581
- * @param {Vector3} [pos]
582
- * @param {Vector3} [rotation] - vec3(pitch, yaw, roll) in radians
583
- * @param {Vector3} [scale]
584
- * @param {Matrix4} [matrix] - Written into instead of a new one, for a loop that builds many
585
- * @return {Matrix4}
586
- * @memberof Math3D
587
- */
588
- function buildMatrix(pos, rotation, scale, matrix=new Matrix4)
589
- {
590
- ASSERT(matrix instanceof Matrix4, 'the matrix to write into must be a Matrix4');
591
- matrix4Compose(matrix.m, 0, pos, rotation, scale);
592
- return matrix;
593
- }
594
-
595
- // the rotation matrix of euler angles written into 16 floats of an array at k
596
- function matrix4RotationAt(m, k, euler)
597
- {
598
- const cx = cos(euler.x), sx = sin(euler.x);
599
- const cy = cos(euler.y), sy = sin(euler.y);
600
- const cz = cos(euler.z), sz = sin(euler.z);
601
- // R = Ry * Rx * Rz written out, column major, every element set so a reused matrix comes out clean
602
- m[k] = cy*cz + sy*sx*sz; m[k+1] = cx*sz; m[k+2] = -sy*cz + cy*sx*sz; m[k+3] = 0;
603
- m[k+4] = -cy*sz + sy*sx*cz; m[k+5] = cx*cz; m[k+6] = sy*sz + cy*sx*cz; m[k+7] = 0;
604
- m[k+8] = sy*cx; m[k+9] = -sx; m[k+10] = cy*cx; m[k+11] = 0;
605
- m[k+12] = m[k+13] = m[k+14] = 0; m[k+15] = 1;
606
- }
607
-
608
- // the transform buildMatrix makes, written into 16 floats of an array at k, so an instance buffer takes it straight in
609
- // the rotation columns are scaled and the position dropped in, instead of multiplying three matrices, each value
610
- // worked out whole and stored once; InstancedMesh3D.setTransforms writes the same expressions for many at a time
611
- function matrix4Compose(m, k, pos, rotation, scale)
612
- {
613
- ASSERT(!pos || isVector3(pos), 'pos must be a Vector3', pos);
614
- ASSERT(!scale || isVector3(scale), 'scale must be a Vector3', scale);
615
- const scaleX = scale ? scale.x : 1, scaleY = scale ? scale.y : 1, scaleZ = scale ? scale.z : 1;
616
- // an object that is not turned at all is most of a big scene, and identity is what the six
617
- // trig calls would have worked out to anyway
618
- if (rotation && (rotation.x || rotation.y || rotation.z))
619
- {
620
- ASSERT_VECTOR3_VALID(rotation);
621
- const cx = cos(rotation.x), sx = sin(rotation.x);
622
- const cy = cos(rotation.y), sy = sin(rotation.y);
623
- const cz = cos(rotation.z), sz = sin(rotation.z);
624
- // R = Ry * Rx * Rz written out, column major, as matrix4RotationAt, each column times its scale
625
- m[k] = (cy*cz + sy*sx*sz) * scaleX; m[k+1] = cx*sz * scaleX; m[k+2] = (-sy*cz + cy*sx*sz) * scaleX;
626
- m[k+4] = (-cy*sz + sy*sx*cz) * scaleY; m[k+5] = cx*cz * scaleY; m[k+6] = (sy*sz + cy*sx*cz) * scaleY;
627
- m[k+8] = sy*cx * scaleZ; m[k+9] = -sx * scaleZ; m[k+10] = cy*cx * scaleZ;
628
- }
629
- else
630
- {
631
- m[k] = scaleX; m[k+1] = 0; m[k+2] = 0;
632
- m[k+4] = 0; m[k+5] = scaleY; m[k+6] = 0;
633
- m[k+8] = 0; m[k+9] = 0; m[k+10] = scaleZ;
634
- }
635
- m[k+3] = m[k+7] = m[k+11] = 0; m[k+15] = 1;
636
- m[k+12] = pos ? pos.x : 0, m[k+13] = pos ? pos.y : 0, m[k+14] = pos ? pos.z : 0;
637
- }
638
-
639
- ///////////////////////////////////////////////////////////////////////////////
640
- /**
641
- * Ray3D - A start point and a direction, what screenToRay returns and the raycast helpers take
642
- * - The direction need not be unit length, the distances that come back are in units of it
643
- * @memberof Math3D
644
- * @example
645
- * const ray = render3D.screenToRay(mousePosScreen);
646
- * const distance = raycastPlane(ray, vec3(), vec3(0, 1, 0));
647
- * if (distance !== undefined)
648
- * ball.pos3D = ray.getPosition(distance);
649
- */
650
- class Ray3D
651
- {
652
- /** Create a ray
653
- * @param {Vector3} [origin]
654
- * @param {Vector3} [direction] - Defaults to -Z, forward */
655
- constructor(origin=vec3(), direction=vec3(0, 0, -1))
656
- {
657
- ASSERT_VECTOR3_VALID(origin);
658
- ASSERT_VECTOR3_VALID(direction);
659
- /** @property {Vector3} - Where the ray starts */
660
- this.origin = origin;
661
- /** @property {Vector3} - Which way it goes */
662
- this.direction = direction;
663
- }
664
-
665
- /** Returns the point a distance along the ray
666
- * @param {number} distance - What the raycast helpers return
667
- * @return {Vector3} */
668
- getPosition(distance) { return this.origin.add(this.direction.scale(distance)); }
669
-
670
- /** Returns a new ray that is a copy of this
671
- * @return {Ray3D} */
672
- copy() { return new Ray3D(this.origin.copy(), this.direction.copy()); }
673
- }
674
-
675
- ///////////////////////////////////////////////////////////////////////////////
676
- // 3D collision helpers, none of them change anything that is passed in
677
- // Boxes sit centered on pos and take a full size, like drawRect, upright unless given a rotation, an Euler vec3 like
678
- // rotation3D, which turns them about their center
679
- // Cylinders stand up the Y axis, centered on pos, with a full height
680
- // Names that could be mistaken for 2D functions get a 3D suffix
681
-
682
- // whether a rotation turns anything, a missing one does not
683
- const isTurned3D = (rotation)=> !!rotation && !!(rotation.x || rotation.y || rotation.z);
684
-
685
- // a turned box's three unit axes, the columns of its rotation
686
- function boxAxes3D(rotation)
687
- {
688
- const m = buildMatrix(undefined, rotation).m;
689
- return [vec3(m[0], m[1], m[2]), vec3(m[4], m[5], m[6]), vec3(m[8], m[9], m[10])];
690
- }
691
-
692
- // a point in a turned box's own space, from its center, and a vector from that space back to the world's
693
- function boxLocal3D(point, pos, axes)
694
- {
695
- const d = point.subtract(pos);
696
- return vec3(d.dot(axes[0]), d.dot(axes[1]), d.dot(axes[2]));
697
- }
698
- const boxWorld3D = (v, axes)=> axes[0].scale(v.x).add(axes[1].scale(v.y)).add(axes[2].scale(v.z));
699
-
700
- /**
701
- * Check if a point is inside a box, boundary is inclusive
702
- * @param {Vector3} point
703
- * @param {Vector3} pos - Center of the box
704
- * @param {Vector3} size - Full size of the box
705
- * @param {Vector3} [rotation] - How the box is turned, upright when left out
706
- * @return {boolean}
707
- * @memberof Math3D
708
- */
709
- function isPointInBox3D(point, pos, size, rotation)
710
- {
711
- if (isTurned3D(rotation))
712
- return isPointInBox3D(boxLocal3D(point, pos, boxAxes3D(rotation)), vec3(), size);
713
- return abs(point.x - pos.x) <= size.x/2 &&
714
- abs(point.y - pos.y) <= size.y/2 &&
715
- abs(point.z - pos.z) <= size.z/2;
716
- }
717
-
718
- /**
719
- * Check if two boxes are overlapping, touching edges do not overlap
720
- * @param {Vector3} posA
721
- * @param {Vector3} sizeA - Full size of box A
722
- * @param {Vector3} posB
723
- * @param {Vector3} [sizeB] - Full size of box B, zero for a point
724
- * @param {Vector3} [rotationA] - How box A is turned, upright when left out
725
- * @param {Vector3} [rotationB] - How box B is turned
726
- * @return {boolean}
727
- * @memberof Math3D
728
- */
729
- function isOverlapping3D(posA, sizeA, posB, sizeB=vec3(), rotationA, rotationB)
730
- {
731
- if (isTurned3D(rotationA) || isTurned3D(rotationB))
732
- return !!collideBoxBox3D(posA, sizeA, posB, sizeB, rotationA, rotationB);
733
- const d = posA.subtract(posB);
734
- return abs(d.x) < (sizeA.x + sizeB.x)/2 &&
735
- abs(d.y) < (sizeA.y + sizeB.y)/2 &&
736
- abs(d.z) < (sizeA.z + sizeB.z)/2;
737
- }
738
-
739
- /**
740
- * Returns the vector to move sphere A by so it no longer overlaps sphere B, or undefined
741
- * @param {Vector3} posA
742
- * @param {number} radiusA
743
- * @param {Vector3} posB
744
- * @param {number} radiusB
745
- * @return {Vector3|undefined}
746
- * @memberof Math3D
747
- */
748
- function collideSphereSphere(posA, radiusA, posB, radiusB)
749
- {
750
- const d = posA.subtract(posB);
751
- const r = radiusA + radiusB;
752
- const dist = d.length();
753
- if (dist >= r)
754
- return undefined;
755
- if (!dist)
756
- return vec3(0, r, 0); // coincident centers, push straight up
757
- return d.normalize(r - dist);
758
- }
759
-
760
- /**
761
- * Returns the vector to move a sphere out of a box, or undefined
762
- * @param {Vector3} pos - Sphere center
763
- * @param {number} radius
764
- * @param {Vector3} boxPos
765
- * @param {Vector3} boxSize - Full size of the box
766
- * @param {Vector3} [boxRotation] - How the box is turned, upright when left out
767
- * @return {Vector3|undefined}
768
- * @memberof Math3D
769
- */
770
- function collideSphereBox(pos, radius, boxPos, boxSize, boxRotation)
771
- {
772
- if (isTurned3D(boxRotation))
773
- return collideSphereOrientedBox3D(pos, radius, boxPos, boxSize, boxAxes3D(boxRotation));
774
- const h = boxSize.scale(.5);
775
- const closest = vec3(
776
- clamp(pos.x, boxPos.x - h.x, boxPos.x + h.x),
777
- clamp(pos.y, boxPos.y - h.y, boxPos.y + h.y),
778
- clamp(pos.z, boxPos.z - h.z, boxPos.z + h.z));
779
- const d = pos.subtract(closest), distSq = d.lengthSquared();
780
- if (distSq)
781
- return distSq >= radius*radius ? undefined : d.normalize(radius - distSq**.5);
782
-
783
- // center is inside the box, push out along the axis of least penetration
784
- const offset = pos.subtract(boxPos);
785
- return pushOutAxis3D(offset, h.x - abs(offset.x), h.y - abs(offset.y), h.z - abs(offset.z), radius);
786
- }
787
-
788
- /**
789
- * Returns the vector to move a sphere back inside an axis aligned box, or undefined when it is all inside
790
- * - The inside out twin of collideSphereBox, for keeping things in a room or an arena
791
- * - A sphere too big for the box on some axis is held at the middle of it on that axis
792
- * @param {Vector3} pos - Sphere center
793
- * @param {number} radius
794
- * @param {Vector3} boxPos
795
- * @param {Vector3} boxSize - Full size of the box
796
- * @return {Vector3|undefined}
797
- * @memberof Math3D
798
- */
799
- function collideSphereInBox(pos, radius, boxPos, boxSize)
800
- {
801
- // the box shrunk by the radius is everywhere the center can be, so clamp the center into it
802
- const x = max(0, boxSize.x/2 - radius), y = max(0, boxSize.y/2 - radius), z = max(0, boxSize.z/2 - radius);
803
- const push = vec3(
804
- clamp(pos.x, boxPos.x - x, boxPos.x + x) - pos.x,
805
- clamp(pos.y, boxPos.y - y, boxPos.y + y) - pos.y,
806
- clamp(pos.z, boxPos.z - z, boxPos.z + z) - pos.z);
807
- return push.lengthSquared() ? push : undefined;
808
- }
809
-
810
- // the axis with the smallest penetration, pointing the way d does, with extra distance added
811
- function pushOutAxis3D(d, penX, penY, penZ, extra=0)
812
- {
813
- const s = (v)=> v >= 0 ? 1 : -1; // sign() gives 0 on a tie, which would be no push
814
- if (penX <= penY && penX <= penZ)
815
- return vec3(s(d.x)*(penX + extra), 0, 0);
816
- if (penY <= penZ)
817
- return vec3(0, s(d.y)*(penY + extra), 0);
818
- return vec3(0, 0, s(d.z)*(penZ + extra));
819
- }
820
-
821
- /**
822
- * Returns the vector to move a sphere out of a vertical cylinder, or undefined
823
- * @param {Vector3} pos - Sphere center
824
- * @param {number} radius
825
- * @param {Vector3} cylinderPos
826
- * @param {number} cylinderRadius
827
- * @param {number} cylinderHeight - Full height along Y
828
- * @return {Vector3|undefined}
829
- * @memberof Math3D
830
- */
831
- function collideSphereCylinder(pos, radius, cylinderPos, cylinderRadius, cylinderHeight)
832
- {
833
- const halfHeight = cylinderHeight/2;
834
- const offsetX = pos.x - cylinderPos.x;
835
- const offsetZ = pos.z - cylinderPos.z;
836
- const offsetY = pos.y - cylinderPos.y;
837
- const radialDist = (offsetX**2 + offsetZ**2)**.5;
838
- const radialScale = radialDist ? min(radialDist, cylinderRadius)/radialDist : 0; // pull the point onto the wall, or the axis
839
- const closest = vec3(
840
- cylinderPos.x + offsetX*radialScale,
841
- clamp(pos.y, cylinderPos.y - halfHeight, cylinderPos.y + halfHeight),
842
- cylinderPos.z + offsetZ*radialScale);
843
- const d = pos.subtract(closest), distSq = d.lengthSquared();
844
- if (distSq)
845
- return distSq >= radius*radius ? undefined : d.normalize(radius - distSq**.5);
846
-
847
- // center is inside the cylinder, push out through the nearer surface
848
- const sidePen = cylinderRadius - radialDist;
849
- const capPen = halfHeight - abs(offsetY);
850
- if (sidePen <= capPen)
851
- {
852
- const dir = radialDist ? vec3(offsetX/radialDist, 0, offsetZ/radialDist) : vec3(1, 0, 0);
853
- return dir.scale(sidePen + radius);
854
- }
855
- return vec3(0, (offsetY >= 0 ? 1 : -1)*(capPen + radius), 0);
856
- }
857
-
858
- /**
859
- * Returns the vector to move box A by so it no longer overlaps box B, the shortest way out, or undefined
860
- * - The 3D twin of collideBoxBox
861
- * - Turned boxes are tested by the separating axis test, the push is along the direction they overlap least on
862
- * @param {Vector3} posA
863
- * @param {Vector3} sizeA - Full size of box A
864
- * @param {Vector3} posB
865
- * @param {Vector3} sizeB - Full size of box B
866
- * @param {Vector3} [rotationA] - How box A is turned, upright when left out
867
- * @param {Vector3} [rotationB] - How box B is turned
868
- * @return {Vector3|undefined}
869
- * @memberof Math3D
870
- */
871
- function collideBoxBox3D(posA, sizeA, posB, sizeB, rotationA, rotationB)
872
- {
873
- if (isTurned3D(rotationA) || isTurned3D(rotationB))
874
- return collideOrientedBoxes3D(posA, sizeA, boxAxes3D(rotationA), posB, sizeB, boxAxes3D(rotationB));
875
- const d = posA.subtract(posB);
876
- const overlapX = (sizeA.x + sizeB.x)/2 - abs(d.x);
877
- const overlapY = (sizeA.y + sizeB.y)/2 - abs(d.y);
878
- const overlapZ = (sizeA.z + sizeB.z)/2 - abs(d.z);
879
- if (overlapX <= 0 || overlapY <= 0 || overlapZ <= 0)
880
- return undefined;
881
- return pushOutAxis3D(d, overlapX, overlapY, overlapZ);
882
- }
883
-
884
- // the push to move a sphere out of a box turned to these axes: in the box's own space it is upright, and the push
885
- // goes back out turned
886
- function collideSphereOrientedBox3D(pos, radius, boxPos, boxSize, axes)
887
- {
888
- const push = collideSphereBox(boxLocal3D(pos, boxPos, axes), radius, vec3(), boxSize);
889
- return push && boxWorld3D(push, axes);
890
- }
891
-
892
- // the world's axes, for an upright box among turned ones
893
- const BOX_WORLD_AXES = Object.freeze([vec3(1, 0, 0), vec3(0, 1, 0), vec3(0, 0, 1)]);
894
-
895
- // the shortest push to move box A out of box B by the separating axis test: each box's three face directions and
896
- // the nine across an edge of each; a gap on any of them is no touch, and the push is along the least overlap, from
897
- // B to A; an edge direction has to overlap clearly less to win, so a box lying on another leaves by the face
898
- function collideOrientedBoxes3D(posA, sizeA, axesA, posB, sizeB, axesB)
899
- {
900
- const d = posA.subtract(posB), ha = sizeA.scale(.5), hb = sizeB.scale(.5);
901
- const reach = (h, axes, n)=> h.x * abs(axes[0].dot(n)) + h.y * abs(axes[1].dot(n)) + h.z * abs(axes[2].dot(n));
902
- let push, least = Infinity;
903
- const separated = (axis, weight)=>
904
- {
905
- const lengthSquared = axis.lengthSquared();
906
- if (lengthSquared < 1e-12) return false; // parallel edges give no direction to test
907
- const n = axis.scale(lengthSquared ** -.5), along = d.dot(n);
908
- const overlap = reach(ha, axesA, n) + reach(hb, axesB, n) - abs(along);
909
- if (overlap <= 0) return true;
910
- if (overlap * weight < least)
911
- least = overlap * weight, push = n.scale(along < 0 ? -overlap : overlap);
912
- return false;
913
- };
914
- for (const axis of [...axesA, ...axesB])
915
- if (separated(axis, 1)) return;
916
- for (const a of axesA)
917
- for (const b of axesB)
918
- if (separated(a.cross(b), 1.05)) return;
919
- return push;
920
- }
921
-
922
- /**
923
- * Returns the distance along the ray to the first intersection with a sphere, or undefined
924
- * - The hit is ray.getPosition(distance), a direction that is not unit length scales the distance
925
- * - A ray starting inside the sphere is already there, so it gets back 0
926
- * @param {Ray3D} ray
927
- * @param {Vector3} pos - Sphere center
928
- * @param {number} radius
929
- * @return {number|undefined}
930
- * @memberof Math3D
931
- */
932
- function raycastSphere(ray, pos, radius)
933
- {
934
- const {origin, direction} = ray;
935
- const oc = origin.subtract(pos);
936
- const c = oc.dot(oc) - radius*radius;
937
- if (c < 0)
938
- return 0; // origin is inside the sphere, even for a ray of no length, as raycastBox gives
939
- const a = direction.dot(direction);
940
- if (!a)
941
- return undefined;
942
- const b = 2*oc.dot(direction);
943
- const discriminant = b*b - 4*a*c;
944
- if (discriminant < 0)
945
- return undefined;
946
- const t = (-b - discriminant**.5)/(2*a);
947
- return t >= 0 ? t : undefined;
948
- }
949
-
950
- /**
951
- * Returns the distance along the ray to a plane, or undefined if parallel or behind
952
- * - The hit is ray.getPosition(distance), a direction that is not unit length scales the distance
953
- * @param {Ray3D} ray
954
- * @param {Vector3} planePos
955
- * @param {Vector3} planeNormal
956
- * @return {number|undefined}
957
- * @memberof Math3D
958
- */
959
- function raycastPlane(ray, planePos, planeNormal)
960
- {
961
- const {origin, direction} = ray;
962
- const denominator = direction.dot(planeNormal);
963
- if (abs(denominator) < 1e-9)
964
- return undefined;
965
- const t = planePos.subtract(origin).dot(planeNormal)/denominator;
966
- return t < 0 ? undefined : t;
967
- }
968
-
969
- /**
970
- * Returns the distance along the ray to the first intersection with a box, or undefined
971
- * - The hit is ray.getPosition(distance), a direction that is not unit length scales the distance
972
- * - A ray starting inside the box is already there, so it gets back 0
973
- * @param {Ray3D} ray
974
- * @param {Vector3} pos - Center of the box
975
- * @param {Vector3} size - Full size of the box
976
- * @param {Vector3} [rotation] - How the box is turned, upright when left out
977
- * @return {number|undefined}
978
- * @memberof Math3D
979
- */
980
- function raycastBox(ray, pos, size, rotation)
981
- {
982
- if (isTurned3D(rotation))
983
- {
984
- // the ray in the box's own space, where the box is upright; turning keeps the distances
985
- const axes = boxAxes3D(rotation), d = ray.direction;
986
- const direction = vec3(d.dot(axes[0]), d.dot(axes[1]), d.dot(axes[2]));
987
- return raycastBox(new Ray3D(boxLocal3D(ray.origin, pos, axes), direction), vec3(), size);
988
- }
989
- const {origin, direction} = ray;
990
- const h = size.scale(.5);
991
- const boxMin = pos.subtract(h), boxMax = pos.add(h);
992
- let tMin = 0, tMax = Infinity;
993
- for (const axis of 'xyz')
994
- {
995
- const o = origin[axis], d = direction[axis];
996
- const mn = boxMin[axis], mx = boxMax[axis];
997
- if (!d)
998
- {
999
- if (o < mn || o > mx)
1000
- return undefined; // ray is parallel to this slab and outside it
1001
- continue;
1002
- }
1003
- let t0 = (mn - o)/d;
1004
- let t1 = (mx - o)/d;
1005
- if (t0 > t1)
1006
- [t0, t1] = [t1, t0];
1007
- tMin = max(tMin, t0);
1008
- tMax = min(tMax, t1);
1009
- if (tMin > tMax)
1010
- return undefined;
1011
- }
1012
- return tMin;
1013
- }
1
+ /**
2
+ * LittleJS 3D Math Plugin
3
+ * - Vector3 and Matrix4 for 3D games and plugins
4
+ * - Right handed, Y up, angles in radians
5
+ * - Used by the Render3D plugin, but has no rendering dependencies
6
+ * @namespace Math3D
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ ///////////////////////////////////////////////////////////////////////////////
12
+
13
+ /**
14
+ * Create a 3D vector, can take 0, 1, 2 or 3 numbers
15
+ * - vec3() is zero, vec3(s) fills all three, vec3(x, y) sets z to 0
16
+ * @param {number} [x]
17
+ * @param {number} [y]
18
+ * @param {number} [z]
19
+ * @return {Vector3}
20
+ * @memberof Math3D
21
+ */
22
+ function vec3(x=0, y, z)
23
+ {
24
+ return y === undefined ? new Vector3(x, x, x) : new Vector3(x, y, z === undefined ? 0 : z);
25
+ }
26
+
27
+ /**
28
+ * Check if the object is a valid Vector3
29
+ * @param {any} v
30
+ * @return {boolean}
31
+ * @memberof Math3D
32
+ */
33
+ function isVector3(v) { return v instanceof Vector3 && v.isValid(); }
34
+
35
+ // debug check that a value is a usable Vector3, stripped in release like the 2D one
36
+ function ASSERT_VECTOR3_VALID(v) { ASSERT(isVector3(v), 'Vector3 is invalid.', v); }
37
+
38
+ /**
39
+ * Returns a random Vector3 of a given length, pointing any direction evenly, or within a cone around +Y
40
+ * @param {number} [length]
41
+ * @param {number} [coneAngle] - Half angle of the cone around +Y in radians, PI is every direction
42
+ * @return {Vector3}
43
+ * @memberof Math3D
44
+ */
45
+ function randVector3(length=1, coneAngle=PI)
46
+ {
47
+ // a random height on the sphere is uniform over its surface, then a random turn around Y
48
+ const y = rand(cos(coneAngle), 1), s = (1 - y * y) ** .5, a = rand(2 * PI);
49
+ return new Vector3(s * cos(a) * length, y * length, s * sin(a) * length);
50
+ }
51
+
52
+ /**
53
+ * Returns a random Vector3 inside a sphere, spread evenly through its volume, the 3D twin of randInCircle
54
+ * @param {number} [radius]
55
+ * @param {number} [minRadius] - Leave a hollow middle this big
56
+ * @return {Vector3}
57
+ * @memberof Math3D
58
+ */
59
+ function randInSphere(radius=1, minRadius=0)
60
+ {
61
+ // the volume inside a radius grows with its cube, so that is what has to come out even
62
+ if (radius <= 0) return new Vector3;
63
+ const ratio = clamp(minRadius / radius);
64
+ return randVector3(radius * rand(ratio**3, 1) ** (1/3));
65
+ }
66
+
67
+ /**
68
+ * 3D Vector object, right handed with Y up
69
+ * - Methods return new vectors except set and setFrom
70
+ * @memberof Math3D
71
+ * @example
72
+ * const a = vec3(1, 2, 3);
73
+ * const b = a.add(vec3(0, 1, 0)).normalize();
74
+ */
75
+ class Vector3
76
+ {
77
+ /** Create a 3D vector
78
+ * @param {number} [x]
79
+ * @param {number} [y]
80
+ * @param {number} [z] */
81
+ constructor(x=0, y=0, z=0)
82
+ {
83
+ ASSERT(isNumber(x) && isNumber(y) && isNumber(z), 'Vector3 components must be numbers');
84
+ /** @property {number} - X axis location */
85
+ this.x = x;
86
+ /** @property {number} - Y axis location */
87
+ this.y = y;
88
+ /** @property {number} - Z axis location */
89
+ this.z = z;
90
+ }
91
+
92
+ /** Sets values of this vector and returns self
93
+ * @param {number} [x]
94
+ * @param {number} [y]
95
+ * @param {number} [z]
96
+ * @return {Vector3} */
97
+ set(x=0, y=0, z=0) { this.x = x; this.y = y; this.z = z; ASSERT_VECTOR3_VALID(this); return this; }
98
+
99
+ /** Copies the values of another vector into this one and returns self
100
+ * @param {Vector3} v
101
+ * @return {Vector3} */
102
+ setFrom(v) { return this.set(v.x, v.y, v.z); }
103
+
104
+ /** Returns a new vector that is a copy of this
105
+ * @return {Vector3} */
106
+ copy() { return new Vector3(this.x, this.y, this.z); }
107
+
108
+ /** Returns a copy of this vector plus the vector passed in
109
+ * @param {Vector3} v
110
+ * @return {Vector3} */
111
+ add(v) { return new Vector3(this.x + v.x, this.y + v.y, this.z + v.z); }
112
+
113
+ /** Returns a copy of this vector minus the vector passed in
114
+ * @param {Vector3} v
115
+ * @return {Vector3} */
116
+ subtract(v) { return new Vector3(this.x - v.x, this.y - v.y, this.z - v.z); }
117
+
118
+ /** Returns a copy of this vector times the vector passed in
119
+ * @param {Vector3} v
120
+ * @return {Vector3} */
121
+ multiply(v) { return new Vector3(this.x * v.x, this.y * v.y, this.z * v.z); }
122
+
123
+ /** Returns a copy of this vector divided by the vector passed in
124
+ * @param {Vector3} v
125
+ * @return {Vector3} */
126
+ divide(v) { return new Vector3(this.x / v.x, this.y / v.y, this.z / v.z); }
127
+
128
+ /** Returns a copy of this vector scaled by the number passed in
129
+ * @param {number} s
130
+ * @return {Vector3} */
131
+ scale(s) { return new Vector3(this.x * s, this.y * s, this.z * s); }
132
+
133
+ /** Returns the length of this vector
134
+ * @return {number} */
135
+ length() { return this.lengthSquared()**.5; }
136
+
137
+ /** Returns the length of this vector squared
138
+ * @return {number} */
139
+ lengthSquared() { return this.x**2 + this.y**2 + this.z**2; }
140
+
141
+ /** Returns a copy of this vector reflected by a surface normal
142
+ * @param {Vector3} normal - Surface normal, should be normalized
143
+ * @param {number} [restitution] - How much to bounce, 1 is a perfect bounce, 0 slides along the surface
144
+ * @return {Vector3} */
145
+ reflect(normal, restitution=1) { return this.subtract(normal.scale((1 + restitution) * this.dot(normal))); }
146
+
147
+ /** Returns the distance from this vector to the vector passed in
148
+ * @param {Vector3} v
149
+ * @return {number} */
150
+ distance(v) { return this.distanceSquared(v)**.5; }
151
+
152
+ /** Returns the distance squared from this vector to the vector passed in
153
+ * @param {Vector3} v
154
+ * @return {number} */
155
+ distanceSquared(v) { return (this.x - v.x)**2 + (this.y - v.y)**2 + (this.z - v.z)**2; }
156
+
157
+ /** Returns a new vector in the same direction with the length passed in, zero stays zero
158
+ * @param {number} [length]
159
+ * @return {Vector3} */
160
+ normalize(length=1)
161
+ {
162
+ const l = this.length();
163
+ return l ? this.scale(length/l) : new Vector3;
164
+ }
165
+
166
+ /** Returns a new vector clamped to the length passed in
167
+ * @param {number} [length]
168
+ * @return {Vector3} */
169
+ clampLength(length=1)
170
+ {
171
+ const l = this.length();
172
+ return l > length ? this.scale(length/l) : this.copy();
173
+ }
174
+
175
+ /** Returns the dot product of this vector and the vector passed in
176
+ * @param {Vector3} v
177
+ * @return {number} */
178
+ dot(v) { return this.x*v.x + this.y*v.y + this.z*v.z; }
179
+
180
+ /** Returns a vector at right angles to both this and the one passed in
181
+ * @param {Vector3} v
182
+ * @return {Vector3} */
183
+ cross(v)
184
+ {
185
+ return new Vector3(
186
+ this.y*v.z - this.z*v.y,
187
+ this.z*v.x - this.x*v.z,
188
+ this.x*v.y - this.y*v.x);
189
+ }
190
+
191
+ /** Returns a new vector interpolated between this and the vector passed in, percent is clamped to 0-1
192
+ * @param {Vector3} v
193
+ * @param {number} percent
194
+ * @return {Vector3} */
195
+ lerp(v, percent)
196
+ {
197
+ ASSERT_VECTOR3_VALID(v);
198
+ return this.add(v.subtract(this).scale(clamp(percent)));
199
+ }
200
+
201
+ /** Returns a new vector turned around an axis, counter clockwise when the axis points at you
202
+ * @param {Vector3} axis - Unit length
203
+ * @param {number} angle - Radians
204
+ * @return {Vector3} */
205
+ rotate(axis, angle)
206
+ {
207
+ ASSERT_VECTOR3_VALID(axis); // unlike Vector2.rotate this takes an axis first
208
+ // Rodrigues' formula: the part along the axis stays, the rest turns
209
+ const c = cos(angle), s = sin(angle), d = axis.dot(this) * (1 - c);
210
+ return this.scale(c).add(axis.cross(this).scale(s)).add(axis.scale(d));
211
+ }
212
+
213
+ /** Returns a new vector turned around the X axis, the way a positive pitch in rotation3D turns things
214
+ * @param {number} angle - Radians
215
+ * @return {Vector3} */
216
+ rotateX(angle)
217
+ {
218
+ const c = cos(angle), s = sin(angle);
219
+ return new Vector3(this.x, this.y*c - this.z*s, this.y*s + this.z*c);
220
+ }
221
+
222
+ /** Returns a new vector turned around the Y axis, the way a positive yaw in rotation3D turns things
223
+ * @param {number} angle - Radians
224
+ * @return {Vector3} */
225
+ rotateY(angle)
226
+ {
227
+ const c = cos(angle), s = sin(angle);
228
+ return new Vector3(this.x*c + this.z*s, this.y, this.z*c - this.x*s);
229
+ }
230
+
231
+ /** Returns a new vector turned around the Z axis, the way a positive roll in rotation3D turns things
232
+ * @param {number} angle - Radians
233
+ * @return {Vector3} */
234
+ rotateZ(angle)
235
+ {
236
+ const c = cos(angle), s = sin(angle);
237
+ return new Vector3(this.x*c - this.y*s, this.x*s + this.y*c, this.z);
238
+ }
239
+
240
+ /** Returns a new vector with the absolute value of each component
241
+ * @return {Vector3} */
242
+ abs() { return new Vector3(abs(this.x), abs(this.y), abs(this.z)); }
243
+
244
+ /** Returns a new vector with each component floored
245
+ * @return {Vector3} */
246
+ floor() { return new Vector3(floor(this.x), floor(this.y), floor(this.z)); }
247
+
248
+ /** Returns a new vector with each component rounded
249
+ * @return {Vector3} */
250
+ round() { return new Vector3(round(this.x), round(this.y), round(this.z)); }
251
+
252
+ /** Returns a new vector snapped down to a grid, grid is the number of steps per unit like Vector2.snap
253
+ * @param {number} grid - Snap steps per unit, 2 snaps to halves
254
+ * @return {Vector3} */
255
+ snap(grid)
256
+ {
257
+ ASSERT_NUMBER_VALID(grid);
258
+ return new Vector3(floor(this.x*grid)/grid, floor(this.y*grid)/grid, floor(this.z*grid)/grid);
259
+ }
260
+
261
+ /** Returns this point transformed by a matrix, translation included
262
+ * @param {Matrix4} matrix
263
+ * @return {Vector3} */
264
+ transform(matrix) { return matrix.transformPoint(this); }
265
+
266
+ /** Returns this direction transformed by a matrix, rotation and scale only
267
+ * @param {Matrix4} matrix
268
+ * @return {Vector3} */
269
+ transformDirection(matrix) { return matrix.transformDirection(this); }
270
+
271
+ /** Checks if this is a valid vector
272
+ * @return {boolean} */
273
+ isValid() { return isNumber(this.x) && isNumber(this.y) && isNumber(this.z); }
274
+
275
+ /** Returns a string representation of this vector for debugging
276
+ * @param {number} [digits] - Number of digits to display
277
+ * @return {string} */
278
+ toString(digits=3)
279
+ {
280
+ if (!this.isValid())
281
+ return `(${this.x},${this.y},${this.z})`; // show the bad values instead of throwing
282
+ const f = (v)=> (v < 0 ? '' : ' ') + v.toFixed(digits);
283
+ return `(${f(this.x)},${f(this.y)},${f(this.z)} )`;
284
+ }
285
+ }
286
+
287
+ ///////////////////////////////////////////////////////////////////////////////
288
+
289
+ // scratch for multiply, nothing keeps a reference to it
290
+ const matrix4Scratch = new Float32Array(16);
291
+ const matrix4Identity = new Float32Array([1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]);
292
+
293
+ /**
294
+ * 4x4 transform matrix for moving, rotating and scaling points in 3D
295
+ * - Static builders like Matrix4.translation return a new matrix
296
+ * - Methods on a matrix change it in place and return it, so calls can chain
297
+ * - a.multiply(b) means b happens first, then a
298
+ * - Stored the way WebGL wants it, so it can be sent to a shader as is
299
+ * @memberof Math3D
300
+ * @example
301
+ * const m = buildMatrix(vec3(0, 1, 0), vec3(0, PI/2, 0)); // rotate then move up
302
+ * const p = m.transformPoint(vec3(1, 0, 0));
303
+ */
304
+ class Matrix4
305
+ {
306
+ /** Create a matrix, identity by default
307
+ * @param {Float32Array|Array<number>} [m] - 16 column major values */
308
+ constructor(m)
309
+ {
310
+ /** @property {Float32Array} - The 16 column major values */
311
+ this.m = new Float32Array(16);
312
+ ASSERT(!m || m.length == 16, 'Matrix4 takes 16 values, use copy() to duplicate a matrix');
313
+ if (m)
314
+ this.m.set(m);
315
+ else
316
+ this.m[0] = this.m[5] = this.m[10] = this.m[15] = 1;
317
+ }
318
+
319
+ /** Returns a new identity matrix
320
+ * @return {Matrix4} */
321
+ static identity() { return new Matrix4; }
322
+
323
+ /** Returns a new translation matrix
324
+ * @param {Vector3} v
325
+ * @return {Matrix4} */
326
+ static translation(v)
327
+ {
328
+ ASSERT_VECTOR3_VALID(v);
329
+ const r = new Matrix4;
330
+ r.m[12] = v.x; r.m[13] = v.y; r.m[14] = v.z;
331
+ return r;
332
+ }
333
+
334
+ /** Returns a rotation matrix, rolled first, then pitched, then yawed
335
+ * @param {Vector3} euler - vec3(pitch, yaw, roll) in radians
336
+ * @param {Matrix4} [matrix] - Written into instead of a new one, for a loop that builds many
337
+ * @return {Matrix4} */
338
+ static rotation(euler, matrix=new Matrix4)
339
+ {
340
+ ASSERT_VECTOR3_VALID(euler);
341
+ matrix4RotationAt(matrix.m, 0, euler);
342
+ return matrix;
343
+ }
344
+
345
+ /** Returns a new scale matrix
346
+ * @param {Vector3} v
347
+ * @return {Matrix4} */
348
+ static scaling(v)
349
+ {
350
+ ASSERT_VECTOR3_VALID(v);
351
+ const r = new Matrix4;
352
+ r.m[0] = v.x; r.m[5] = v.y; r.m[10] = v.z;
353
+ return r;
354
+ }
355
+
356
+ /** Returns a new perspective projection, camera looks down -Z
357
+ * @param {number} fov - Vertical field of view in radians
358
+ * @param {number} aspect - Width divided by height
359
+ * @param {number} near - Closest visible distance
360
+ * @param {number} far - Furthest visible distance, Infinity is allowed
361
+ * @return {Matrix4} */
362
+ static perspective(fov, aspect, near, far)
363
+ {
364
+ ASSERT(near > 0 && far > near, 'a perspective projection needs 0 < near < far, or nothing is visible', near, far);
365
+ const f = 1 / tan(fov/2);
366
+ const r = new Matrix4;
367
+ const m = r.m;
368
+ m[0] = f / aspect;
369
+ m[5] = f;
370
+ m[10] = far == Infinity ? -1 : (far + near) / (near - far); // the infinite case is the limit of the formula
371
+ m[11] = -1;
372
+ m[14] = far == Infinity ? -2 * near : 2 * far * near / (near - far);
373
+ m[15] = 0;
374
+ return r;
375
+ }
376
+
377
+ /** Returns a new orthographic projection, camera looks down -Z
378
+ * @param {number} left - Edge of the visible box
379
+ * @param {number} right - Edge of the visible box
380
+ * @param {number} bottom - Edge of the visible box
381
+ * @param {number} top - Edge of the visible box
382
+ * @param {number} near - Closest visible distance
383
+ * @param {number} far - Furthest visible distance, Infinity is not allowed here
384
+ * @return {Matrix4} */
385
+ static orthographic(left, right, bottom, top, near, far)
386
+ {
387
+ // an infinite far plane has no orthographic form: every depth would land on the near plane,
388
+ // and the formula below works out to NaN, which quietly clips the whole scene away
389
+ ASSERT(far > near && far != Infinity, 'an orthographic projection needs a real far plane past near, Infinity is perspective only', near, far);
390
+ const r = new Matrix4;
391
+ const m = r.m;
392
+ m[0] = 2 / (right - left);
393
+ m[5] = 2 / (top - bottom);
394
+ m[10] = -2 / (far - near);
395
+ m[12] = -(right + left) / (right - left);
396
+ m[13] = -(top + bottom) / (top - bottom);
397
+ m[14] = -(far + near) / (far - near);
398
+ return r;
399
+ }
400
+
401
+ /** Returns the transform of something at eye turned to face target
402
+ * - Invert it to get a view matrix for a camera there
403
+ * @param {Vector3} eye
404
+ * @param {Vector3} target
405
+ * @param {Vector3} [up]
406
+ * @return {Matrix4} */
407
+ static lookAt(eye, target, up=vec3(0, 1, 0))
408
+ {
409
+ let z = eye.subtract(target).normalize();
410
+ if (!z.lengthSquared())
411
+ z = vec3(0, 0, 1); // eye is on the target, face -Z
412
+ let x = up.cross(z).normalize();
413
+ // up is along the view direction, pick another; looking straight down or up keeps +X as the right axis,
414
+ // as three.js does and as a view tilting there from the +Z side ends up
415
+ if (!x.lengthSquared())
416
+ x = (abs(z.y) > .99 ? vec3(0, 0, z.y > 0 ? -1 : 1) : vec3(0, 1, 0)).cross(z).normalize();
417
+ const y = z.cross(x);
418
+ return new Matrix4([x.x, x.y, x.z, 0, y.x, y.y, y.z, 0, z.x, z.y, z.z, 0, eye.x, eye.y, eye.z, 1]);
419
+ }
420
+
421
+ /** Returns a new matrix that is a copy of this
422
+ * @return {Matrix4} */
423
+ copy() { return new Matrix4(this.m); }
424
+
425
+ /** Multiply this matrix by another and return this, the other happens first
426
+ * @param {Matrix4} matrix
427
+ * @return {Matrix4} */
428
+ multiply(matrix)
429
+ {
430
+ const a = this.m, b = matrix.m, r = matrix4Scratch;
431
+ for (let j = 0; j < 4; ++j)
432
+ for (let i = 0; i < 4; ++i)
433
+ r[j*4 + i] = a[i]*b[j*4] + a[4 + i]*b[j*4 + 1] + a[8 + i]*b[j*4 + 2] + a[12 + i]*b[j*4 + 3];
434
+ this.m.set(r);
435
+ return this;
436
+ }
437
+
438
+ /** Append a translation, returns self
439
+ * @param {Vector3} v
440
+ * @return {Matrix4} */
441
+ translate(v) { return this.multiply(Matrix4.translation(v)); }
442
+
443
+ /** Append a rotation, returns self
444
+ * @param {Vector3} euler - vec3(pitch, yaw, roll) in radians
445
+ * @return {Matrix4} */
446
+ rotate(euler) { return this.multiply(Matrix4.rotation(euler)); }
447
+
448
+ /** Append a scale, returns self
449
+ * @param {Vector3} v
450
+ * @return {Matrix4} */
451
+ scale(v) { return this.multiply(Matrix4.scaling(v)); }
452
+
453
+ /** Transpose this matrix in place, returns self
454
+ * @return {Matrix4} */
455
+ transpose()
456
+ {
457
+ const m = this.m;
458
+ for (let i = 0; i < 4; ++i)
459
+ for (let j = i + 1; j < 4; ++j)
460
+ {
461
+ const t = m[i*4 + j];
462
+ m[i*4 + j] = m[j*4 + i];
463
+ m[j*4 + i] = t;
464
+ }
465
+ return this;
466
+ }
467
+
468
+ /** Flip this matrix so it undoes itself, returns this and does nothing if it cannot be inverted
469
+ * @return {Matrix4} */
470
+ invert()
471
+ {
472
+ const m = this.m;
473
+ const [a00, a01, a02, a03, a10, a11, a12, a13, a20, a21, a22, a23, a30, a31, a32, a33] = m;
474
+ const b00 = a00*a11 - a01*a10, b01 = a00*a12 - a02*a10, b02 = a00*a13 - a03*a10;
475
+ const b03 = a01*a12 - a02*a11, b04 = a01*a13 - a03*a11, b05 = a02*a13 - a03*a12;
476
+ const b06 = a20*a31 - a21*a30, b07 = a20*a32 - a22*a30, b08 = a20*a33 - a23*a30;
477
+ const b09 = a21*a32 - a22*a31, b10 = a21*a33 - a23*a31, b11 = a22*a33 - a23*a32;
478
+ let det = b00*b11 - b01*b10 + b02*b09 + b03*b08 - b04*b07 + b05*b06;
479
+ if (!det)
480
+ return this;
481
+ det = 1 / det;
482
+ m[0] = (a11*b11 - a12*b10 + a13*b09) * det;
483
+ m[1] = (a02*b10 - a01*b11 - a03*b09) * det;
484
+ m[2] = (a31*b05 - a32*b04 + a33*b03) * det;
485
+ m[3] = (a22*b04 - a21*b05 - a23*b03) * det;
486
+ m[4] = (a12*b08 - a10*b11 - a13*b07) * det;
487
+ m[5] = (a00*b11 - a02*b08 + a03*b07) * det;
488
+ m[6] = (a32*b02 - a30*b05 - a33*b01) * det;
489
+ m[7] = (a20*b05 - a22*b02 + a23*b01) * det;
490
+ m[8] = (a10*b10 - a11*b08 + a13*b06) * det;
491
+ m[9] = (a01*b08 - a00*b10 - a03*b06) * det;
492
+ m[10] = (a30*b04 - a31*b02 + a33*b00) * det;
493
+ m[11] = (a21*b02 - a20*b04 - a23*b00) * det;
494
+ m[12] = (a11*b07 - a10*b09 - a12*b06) * det;
495
+ m[13] = (a00*b09 - a01*b07 + a02*b06) * det;
496
+ m[14] = (a31*b01 - a30*b03 - a32*b00) * det;
497
+ m[15] = (a20*b03 - a21*b01 + a22*b00) * det;
498
+ return this;
499
+ }
500
+
501
+ /** Transform a point, translation included
502
+ * @param {Vector3} v
503
+ * @return {Vector3} */
504
+ transformPoint(v)
505
+ {
506
+ const m = this.m;
507
+ return new Vector3(
508
+ m[0]*v.x + m[4]*v.y + m[8]*v.z + m[12],
509
+ m[1]*v.x + m[5]*v.y + m[9]*v.z + m[13],
510
+ m[2]*v.x + m[6]*v.y + m[10]*v.z + m[14]);
511
+ }
512
+
513
+ /** Transform a direction, rotation and scale only
514
+ * @param {Vector3} v
515
+ * @return {Vector3} */
516
+ transformDirection(v)
517
+ {
518
+ const m = this.m;
519
+ return new Vector3(
520
+ m[0]*v.x + m[4]*v.y + m[8]*v.z,
521
+ m[1]*v.x + m[5]*v.y + m[9]*v.z,
522
+ m[2]*v.x + m[6]*v.y + m[10]*v.z);
523
+ }
524
+
525
+ /** Returns the translation part of this matrix
526
+ * @return {Vector3} */
527
+ getTranslation() { return new Vector3(this.m[12], this.m[13], this.m[14]); }
528
+
529
+ /** Returns the determinant of the rotation and scale part: negative when the matrix mirrors, 0 when it flattens a
530
+ * shape and has no inverse
531
+ * @return {number} */
532
+ determinant()
533
+ {
534
+ const m = this.m;
535
+ return m[0]*(m[5]*m[10] - m[6]*m[9]) - m[4]*(m[1]*m[10] - m[2]*m[9]) + m[8]*(m[1]*m[6] - m[2]*m[5]);
536
+ }
537
+
538
+ /** Returns the scale part of this matrix, the length of each axis; a mirroring matrix shows as a negative x
539
+ * @return {Vector3} */
540
+ getScale()
541
+ {
542
+ const m = this.m;
543
+ const x = hypot(m[0], m[1], m[2]), y = hypot(m[4], m[5], m[6]), z = hypot(m[8], m[9], m[10]);
544
+ return new Vector3(this.determinant() < 0 ? -x : x, y, z);
545
+ }
546
+
547
+ /** Returns the rotation part of this matrix as vec3(pitch, yaw, roll), the angles Matrix4.rotation builds it from
548
+ * - The scale is divided out first, so it works on a full transform
549
+ * - A matrix with shear, from a scaled parent with a turned child, has no exact answer and gets the nearest
550
+ * @return {Vector3} */
551
+ getRotation()
552
+ {
553
+ // the axes at unit length, see Matrix4.rotation for which element is which
554
+ const m = this.m, s = this.getScale();
555
+ const sx = s.x || 1, sy = s.y || 1, sz = s.z || 1;
556
+ const m1 = m[1] / sx, m5 = m[5] / sy, m8 = m[8] / sz, m9 = m[9] / sz, m10 = m[10] / sz;
557
+ const pitch = Math.asin(clamp(-m9, -1, 1));
558
+ if (abs(m9) < 1 - 1e-6)
559
+ return new Vector3(pitch, atan2(m8, m10), atan2(m1, m5));
560
+ // straight up or down: yaw and roll turn about the same axis, so the roll is zero and yaw takes it all
561
+ return new Vector3(pitch, atan2(m[4] / sy * -m9, m[0] / sx), 0);
562
+ }
563
+
564
+ /** Returns a string representation of this matrix for debugging
565
+ * @return {string} */
566
+ toString()
567
+ {
568
+ const m = this.m, f = (i)=> m[i].toFixed(2).padStart(7);
569
+ let s = '';
570
+ for (let row = 0; row < 4; ++row)
571
+ s += `[${f(row)} ${f(4 + row)} ${f(8 + row)} ${f(12 + row)} ]\n`;
572
+ return s;
573
+ }
574
+ }
575
+
576
+ ///////////////////////////////////////////////////////////////////////////////
577
+
578
+ /**
579
+ * Build a transform for an object from its position, rotation and scale
580
+ * - A point is scaled first, then rotated, then moved, which is what you want for a game object
581
+ * @param {Vector3} [pos]
582
+ * @param {Vector3} [rotation] - vec3(pitch, yaw, roll) in radians
583
+ * @param {Vector3} [scale]
584
+ * @param {Matrix4} [matrix] - Written into instead of a new one, for a loop that builds many
585
+ * @return {Matrix4}
586
+ * @memberof Math3D
587
+ */
588
+ function buildMatrix(pos, rotation, scale, matrix=new Matrix4)
589
+ {
590
+ ASSERT(matrix instanceof Matrix4, 'the matrix to write into must be a Matrix4');
591
+ matrix4Compose(matrix.m, 0, pos, rotation, scale);
592
+ return matrix;
593
+ }
594
+
595
+ // the rotation matrix of euler angles written into 16 floats of an array at k
596
+ function matrix4RotationAt(m, k, euler)
597
+ {
598
+ const cx = cos(euler.x), sx = sin(euler.x);
599
+ const cy = cos(euler.y), sy = sin(euler.y);
600
+ const cz = cos(euler.z), sz = sin(euler.z);
601
+ // R = Ry * Rx * Rz written out, column major, every element set so a reused matrix comes out clean
602
+ m[k] = cy*cz + sy*sx*sz; m[k+1] = cx*sz; m[k+2] = -sy*cz + cy*sx*sz; m[k+3] = 0;
603
+ m[k+4] = -cy*sz + sy*sx*cz; m[k+5] = cx*cz; m[k+6] = sy*sz + cy*sx*cz; m[k+7] = 0;
604
+ m[k+8] = sy*cx; m[k+9] = -sx; m[k+10] = cy*cx; m[k+11] = 0;
605
+ m[k+12] = m[k+13] = m[k+14] = 0; m[k+15] = 1;
606
+ }
607
+
608
+ // the transform buildMatrix makes, written into 16 floats of an array at k, so an instance buffer takes it straight in
609
+ // the rotation columns are scaled and the position dropped in, instead of multiplying three matrices, each value
610
+ // worked out whole and stored once; InstancedMesh3D.setTransforms writes the same expressions for many at a time
611
+ function matrix4Compose(m, k, pos, rotation, scale)
612
+ {
613
+ ASSERT(!pos || isVector3(pos), 'pos must be a Vector3', pos);
614
+ ASSERT(!scale || isVector3(scale), 'scale must be a Vector3', scale);
615
+ const scaleX = scale ? scale.x : 1, scaleY = scale ? scale.y : 1, scaleZ = scale ? scale.z : 1;
616
+ // an object that is not turned at all is most of a big scene, and identity is what the six
617
+ // trig calls would have worked out to anyway
618
+ if (rotation && (rotation.x || rotation.y || rotation.z))
619
+ {
620
+ ASSERT_VECTOR3_VALID(rotation);
621
+ const cx = cos(rotation.x), sx = sin(rotation.x);
622
+ const cy = cos(rotation.y), sy = sin(rotation.y);
623
+ const cz = cos(rotation.z), sz = sin(rotation.z);
624
+ // R = Ry * Rx * Rz written out, column major, as matrix4RotationAt, each column times its scale
625
+ m[k] = (cy*cz + sy*sx*sz) * scaleX; m[k+1] = cx*sz * scaleX; m[k+2] = (-sy*cz + cy*sx*sz) * scaleX;
626
+ m[k+4] = (-cy*sz + sy*sx*cz) * scaleY; m[k+5] = cx*cz * scaleY; m[k+6] = (sy*sz + cy*sx*cz) * scaleY;
627
+ m[k+8] = sy*cx * scaleZ; m[k+9] = -sx * scaleZ; m[k+10] = cy*cx * scaleZ;
628
+ }
629
+ else
630
+ {
631
+ m[k] = scaleX; m[k+1] = 0; m[k+2] = 0;
632
+ m[k+4] = 0; m[k+5] = scaleY; m[k+6] = 0;
633
+ m[k+8] = 0; m[k+9] = 0; m[k+10] = scaleZ;
634
+ }
635
+ m[k+3] = m[k+7] = m[k+11] = 0; m[k+15] = 1;
636
+ m[k+12] = pos ? pos.x : 0, m[k+13] = pos ? pos.y : 0, m[k+14] = pos ? pos.z : 0;
637
+ }
638
+
639
+ ///////////////////////////////////////////////////////////////////////////////
640
+ /**
641
+ * Ray3D - A start point and a direction, what screenToRay returns and the raycast helpers take
642
+ * - The direction need not be unit length, the distances that come back are in units of it
643
+ * @memberof Math3D
644
+ * @example
645
+ * const ray = render3D.screenToRay(mousePosScreen);
646
+ * const distance = raycastPlane(ray, vec3(), vec3(0, 1, 0));
647
+ * if (distance !== undefined)
648
+ * ball.pos3D = ray.getPosition(distance);
649
+ */
650
+ class Ray3D
651
+ {
652
+ /** Create a ray
653
+ * @param {Vector3} [origin]
654
+ * @param {Vector3} [direction] - Defaults to -Z, forward */
655
+ constructor(origin=vec3(), direction=vec3(0, 0, -1))
656
+ {
657
+ ASSERT_VECTOR3_VALID(origin);
658
+ ASSERT_VECTOR3_VALID(direction);
659
+ /** @property {Vector3} - Where the ray starts */
660
+ this.origin = origin;
661
+ /** @property {Vector3} - Which way it goes */
662
+ this.direction = direction;
663
+ }
664
+
665
+ /** Returns the point a distance along the ray
666
+ * @param {number} distance - What the raycast helpers return
667
+ * @return {Vector3} */
668
+ getPosition(distance) { return this.origin.add(this.direction.scale(distance)); }
669
+
670
+ /** Returns a new ray that is a copy of this
671
+ * @return {Ray3D} */
672
+ copy() { return new Ray3D(this.origin.copy(), this.direction.copy()); }
673
+ }
674
+
675
+ ///////////////////////////////////////////////////////////////////////////////
676
+ // 3D collision helpers, none of them change anything that is passed in
677
+ // Boxes sit centered on pos and take a full size, like drawRect, upright unless given a rotation, an Euler vec3 like
678
+ // rotation3D, which turns them about their center
679
+ // Cylinders stand up the Y axis, centered on pos, with a full height
680
+ // Names that could be mistaken for 2D functions get a 3D suffix
681
+
682
+ // whether a rotation turns anything, a missing one does not
683
+ const isTurned3D = (rotation)=> !!rotation && !!(rotation.x || rotation.y || rotation.z);
684
+
685
+ // a turned box's three unit axes, the columns of its rotation
686
+ function boxAxes3D(rotation)
687
+ {
688
+ const m = buildMatrix(undefined, rotation).m;
689
+ return [vec3(m[0], m[1], m[2]), vec3(m[4], m[5], m[6]), vec3(m[8], m[9], m[10])];
690
+ }
691
+
692
+ // a point in a turned box's own space, from its center, and a vector from that space back to the world's
693
+ function boxLocal3D(point, pos, axes)
694
+ {
695
+ const d = point.subtract(pos);
696
+ return vec3(d.dot(axes[0]), d.dot(axes[1]), d.dot(axes[2]));
697
+ }
698
+ const boxWorld3D = (v, axes)=> axes[0].scale(v.x).add(axes[1].scale(v.y)).add(axes[2].scale(v.z));
699
+
700
+ /**
701
+ * Check if a point is inside a box, boundary is inclusive
702
+ * @param {Vector3} point
703
+ * @param {Vector3} pos - Center of the box
704
+ * @param {Vector3} size - Full size of the box
705
+ * @param {Vector3} [rotation] - How the box is turned, upright when left out
706
+ * @return {boolean}
707
+ * @memberof Math3D
708
+ */
709
+ function isPointInBox3D(point, pos, size, rotation)
710
+ {
711
+ if (isTurned3D(rotation))
712
+ return isPointInBox3D(boxLocal3D(point, pos, boxAxes3D(rotation)), vec3(), size);
713
+ return abs(point.x - pos.x) <= size.x/2 &&
714
+ abs(point.y - pos.y) <= size.y/2 &&
715
+ abs(point.z - pos.z) <= size.z/2;
716
+ }
717
+
718
+ /**
719
+ * Check if two boxes are overlapping, touching edges do not overlap
720
+ * @param {Vector3} posA
721
+ * @param {Vector3} sizeA - Full size of box A
722
+ * @param {Vector3} posB
723
+ * @param {Vector3} [sizeB] - Full size of box B, zero for a point
724
+ * @param {Vector3} [rotationA] - How box A is turned, upright when left out
725
+ * @param {Vector3} [rotationB] - How box B is turned
726
+ * @return {boolean}
727
+ * @memberof Math3D
728
+ */
729
+ function isOverlapping3D(posA, sizeA, posB, sizeB=vec3(), rotationA, rotationB)
730
+ {
731
+ if (isTurned3D(rotationA) || isTurned3D(rotationB))
732
+ return !!collideBoxBox3D(posA, sizeA, posB, sizeB, rotationA, rotationB);
733
+ const d = posA.subtract(posB);
734
+ return abs(d.x) < (sizeA.x + sizeB.x)/2 &&
735
+ abs(d.y) < (sizeA.y + sizeB.y)/2 &&
736
+ abs(d.z) < (sizeA.z + sizeB.z)/2;
737
+ }
738
+
739
+ /**
740
+ * Returns the vector to move sphere A by so it no longer overlaps sphere B, or undefined
741
+ * @param {Vector3} posA
742
+ * @param {number} radiusA
743
+ * @param {Vector3} posB
744
+ * @param {number} radiusB
745
+ * @return {Vector3|undefined}
746
+ * @memberof Math3D
747
+ */
748
+ function collideSphereSphere(posA, radiusA, posB, radiusB)
749
+ {
750
+ const d = posA.subtract(posB);
751
+ const r = radiusA + radiusB;
752
+ const dist = d.length();
753
+ if (dist >= r)
754
+ return undefined;
755
+ if (!dist)
756
+ return vec3(0, r, 0); // coincident centers, push straight up
757
+ return d.normalize(r - dist);
758
+ }
759
+
760
+ /**
761
+ * Returns the vector to move a sphere out of a box, or undefined
762
+ * @param {Vector3} pos - Sphere center
763
+ * @param {number} radius
764
+ * @param {Vector3} boxPos
765
+ * @param {Vector3} boxSize - Full size of the box
766
+ * @param {Vector3} [boxRotation] - How the box is turned, upright when left out
767
+ * @return {Vector3|undefined}
768
+ * @memberof Math3D
769
+ */
770
+ function collideSphereBox(pos, radius, boxPos, boxSize, boxRotation)
771
+ {
772
+ if (isTurned3D(boxRotation))
773
+ return collideSphereOrientedBox3D(pos, radius, boxPos, boxSize, boxAxes3D(boxRotation));
774
+ const h = boxSize.scale(.5);
775
+ const closest = vec3(
776
+ clamp(pos.x, boxPos.x - h.x, boxPos.x + h.x),
777
+ clamp(pos.y, boxPos.y - h.y, boxPos.y + h.y),
778
+ clamp(pos.z, boxPos.z - h.z, boxPos.z + h.z));
779
+ const d = pos.subtract(closest), distSq = d.lengthSquared();
780
+ if (distSq)
781
+ return distSq >= radius*radius ? undefined : d.normalize(radius - distSq**.5);
782
+
783
+ // center is inside the box, push out along the axis of least penetration
784
+ const offset = pos.subtract(boxPos);
785
+ return pushOutAxis3D(offset, h.x - abs(offset.x), h.y - abs(offset.y), h.z - abs(offset.z), radius);
786
+ }
787
+
788
+ /**
789
+ * Returns the vector to move a sphere back inside an axis aligned box, or undefined when it is all inside
790
+ * - The inside out twin of collideSphereBox, for keeping things in a room or an arena
791
+ * - A sphere too big for the box on some axis is held at the middle of it on that axis
792
+ * @param {Vector3} pos - Sphere center
793
+ * @param {number} radius
794
+ * @param {Vector3} boxPos
795
+ * @param {Vector3} boxSize - Full size of the box
796
+ * @return {Vector3|undefined}
797
+ * @memberof Math3D
798
+ */
799
+ function collideSphereInBox(pos, radius, boxPos, boxSize)
800
+ {
801
+ // the box shrunk by the radius is everywhere the center can be, so clamp the center into it
802
+ const x = max(0, boxSize.x/2 - radius), y = max(0, boxSize.y/2 - radius), z = max(0, boxSize.z/2 - radius);
803
+ const push = vec3(
804
+ clamp(pos.x, boxPos.x - x, boxPos.x + x) - pos.x,
805
+ clamp(pos.y, boxPos.y - y, boxPos.y + y) - pos.y,
806
+ clamp(pos.z, boxPos.z - z, boxPos.z + z) - pos.z);
807
+ return push.lengthSquared() ? push : undefined;
808
+ }
809
+
810
+ // the axis with the smallest penetration, pointing the way d does, with extra distance added
811
+ function pushOutAxis3D(d, penX, penY, penZ, extra=0)
812
+ {
813
+ const s = (v)=> v >= 0 ? 1 : -1; // sign() gives 0 on a tie, which would be no push
814
+ if (penX <= penY && penX <= penZ)
815
+ return vec3(s(d.x)*(penX + extra), 0, 0);
816
+ if (penY <= penZ)
817
+ return vec3(0, s(d.y)*(penY + extra), 0);
818
+ return vec3(0, 0, s(d.z)*(penZ + extra));
819
+ }
820
+
821
+ /**
822
+ * Returns the vector to move a sphere out of a vertical cylinder, or undefined
823
+ * @param {Vector3} pos - Sphere center
824
+ * @param {number} radius
825
+ * @param {Vector3} cylinderPos
826
+ * @param {number} cylinderRadius
827
+ * @param {number} cylinderHeight - Full height along Y
828
+ * @return {Vector3|undefined}
829
+ * @memberof Math3D
830
+ */
831
+ function collideSphereCylinder(pos, radius, cylinderPos, cylinderRadius, cylinderHeight)
832
+ {
833
+ const halfHeight = cylinderHeight/2;
834
+ const offsetX = pos.x - cylinderPos.x;
835
+ const offsetZ = pos.z - cylinderPos.z;
836
+ const offsetY = pos.y - cylinderPos.y;
837
+ const radialDist = (offsetX**2 + offsetZ**2)**.5;
838
+ const radialScale = radialDist ? min(radialDist, cylinderRadius)/radialDist : 0; // pull the point onto the wall, or the axis
839
+ const closest = vec3(
840
+ cylinderPos.x + offsetX*radialScale,
841
+ clamp(pos.y, cylinderPos.y - halfHeight, cylinderPos.y + halfHeight),
842
+ cylinderPos.z + offsetZ*radialScale);
843
+ const d = pos.subtract(closest), distSq = d.lengthSquared();
844
+ if (distSq)
845
+ return distSq >= radius*radius ? undefined : d.normalize(radius - distSq**.5);
846
+
847
+ // center is inside the cylinder, push out through the nearer surface
848
+ const sidePen = cylinderRadius - radialDist;
849
+ const capPen = halfHeight - abs(offsetY);
850
+ if (sidePen <= capPen)
851
+ {
852
+ const dir = radialDist ? vec3(offsetX/radialDist, 0, offsetZ/radialDist) : vec3(1, 0, 0);
853
+ return dir.scale(sidePen + radius);
854
+ }
855
+ return vec3(0, (offsetY >= 0 ? 1 : -1)*(capPen + radius), 0);
856
+ }
857
+
858
+ /**
859
+ * Returns the vector to move box A by so it no longer overlaps box B, the shortest way out, or undefined
860
+ * - The 3D twin of collideBoxBox
861
+ * - Turned boxes are tested by the separating axis test, the push is along the direction they overlap least on
862
+ * @param {Vector3} posA
863
+ * @param {Vector3} sizeA - Full size of box A
864
+ * @param {Vector3} posB
865
+ * @param {Vector3} sizeB - Full size of box B
866
+ * @param {Vector3} [rotationA] - How box A is turned, upright when left out
867
+ * @param {Vector3} [rotationB] - How box B is turned
868
+ * @return {Vector3|undefined}
869
+ * @memberof Math3D
870
+ */
871
+ function collideBoxBox3D(posA, sizeA, posB, sizeB, rotationA, rotationB)
872
+ {
873
+ if (isTurned3D(rotationA) || isTurned3D(rotationB))
874
+ return collideOrientedBoxes3D(posA, sizeA, boxAxes3D(rotationA), posB, sizeB, boxAxes3D(rotationB));
875
+ const d = posA.subtract(posB);
876
+ const overlapX = (sizeA.x + sizeB.x)/2 - abs(d.x);
877
+ const overlapY = (sizeA.y + sizeB.y)/2 - abs(d.y);
878
+ const overlapZ = (sizeA.z + sizeB.z)/2 - abs(d.z);
879
+ if (overlapX <= 0 || overlapY <= 0 || overlapZ <= 0)
880
+ return undefined;
881
+ return pushOutAxis3D(d, overlapX, overlapY, overlapZ);
882
+ }
883
+
884
+ // the push to move a sphere out of a box turned to these axes: in the box's own space it is upright, and the push
885
+ // goes back out turned
886
+ function collideSphereOrientedBox3D(pos, radius, boxPos, boxSize, axes)
887
+ {
888
+ const push = collideSphereBox(boxLocal3D(pos, boxPos, axes), radius, vec3(), boxSize);
889
+ return push && boxWorld3D(push, axes);
890
+ }
891
+
892
+ // the world's axes, for an upright box among turned ones
893
+ const BOX_WORLD_AXES = Object.freeze([vec3(1, 0, 0), vec3(0, 1, 0), vec3(0, 0, 1)]);
894
+
895
+ // the shortest push to move box A out of box B by the separating axis test: each box's three face directions and
896
+ // the nine across an edge of each; a gap on any of them is no touch, and the push is along the least overlap, from
897
+ // B to A; an edge direction has to overlap clearly less to win, so a box lying on another leaves by the face
898
+ function collideOrientedBoxes3D(posA, sizeA, axesA, posB, sizeB, axesB)
899
+ {
900
+ const d = posA.subtract(posB), ha = sizeA.scale(.5), hb = sizeB.scale(.5);
901
+ const reach = (h, axes, n)=> h.x * abs(axes[0].dot(n)) + h.y * abs(axes[1].dot(n)) + h.z * abs(axes[2].dot(n));
902
+ let push, least = Infinity;
903
+ const separated = (axis, weight)=>
904
+ {
905
+ const lengthSquared = axis.lengthSquared();
906
+ if (lengthSquared < 1e-12) return false; // parallel edges give no direction to test
907
+ const n = axis.scale(lengthSquared ** -.5), along = d.dot(n);
908
+ const overlap = reach(ha, axesA, n) + reach(hb, axesB, n) - abs(along);
909
+ if (overlap <= 0) return true;
910
+ if (overlap * weight < least)
911
+ least = overlap * weight, push = n.scale(along < 0 ? -overlap : overlap);
912
+ return false;
913
+ };
914
+ for (const axis of [...axesA, ...axesB])
915
+ if (separated(axis, 1)) return;
916
+ for (const a of axesA)
917
+ for (const b of axesB)
918
+ if (separated(a.cross(b), 1.05)) return;
919
+ return push;
920
+ }
921
+
922
+ /**
923
+ * Returns the distance along the ray to the first intersection with a sphere, or undefined
924
+ * - The hit is ray.getPosition(distance), a direction that is not unit length scales the distance
925
+ * - A ray starting inside the sphere is already there, so it gets back 0
926
+ * @param {Ray3D} ray
927
+ * @param {Vector3} pos - Sphere center
928
+ * @param {number} radius
929
+ * @return {number|undefined}
930
+ * @memberof Math3D
931
+ */
932
+ function raycastSphere(ray, pos, radius)
933
+ {
934
+ // in numbers, as a pick runs it for every object
935
+ const {origin, direction} = ray;
936
+ const x = origin.x - pos.x, y = origin.y - pos.y, z = origin.z - pos.z;
937
+ const c = x*x + y*y + z*z - radius*radius;
938
+ if (c < 0)
939
+ return 0; // origin is inside the sphere, even for a ray of no length, as raycastBox gives
940
+ const a = direction.dot(direction);
941
+ if (!a)
942
+ return undefined;
943
+ const b = 2*(x*direction.x + y*direction.y + z*direction.z);
944
+ const discriminant = b*b - 4*a*c;
945
+ if (discriminant < 0)
946
+ return undefined;
947
+ const t = (-b - discriminant**.5)/(2*a);
948
+ return t >= 0 ? t : undefined;
949
+ }
950
+
951
+ /**
952
+ * Returns the distance along the ray to a plane, or undefined if parallel or behind
953
+ * - The hit is ray.getPosition(distance), a direction that is not unit length scales the distance
954
+ * @param {Ray3D} ray
955
+ * @param {Vector3} planePos
956
+ * @param {Vector3} planeNormal
957
+ * @return {number|undefined}
958
+ * @memberof Math3D
959
+ */
960
+ function raycastPlane(ray, planePos, planeNormal)
961
+ {
962
+ const {origin, direction} = ray;
963
+ const denominator = direction.dot(planeNormal);
964
+ if (abs(denominator) < 1e-9)
965
+ return undefined;
966
+ const t = planePos.subtract(origin).dot(planeNormal)/denominator;
967
+ return t < 0 ? undefined : t;
968
+ }
969
+
970
+ /**
971
+ * Returns the distance along the ray to the first intersection with a box, or undefined
972
+ * - The hit is ray.getPosition(distance), a direction that is not unit length scales the distance
973
+ * - A ray starting inside the box is already there, so it gets back 0
974
+ * @param {Ray3D} ray
975
+ * @param {Vector3} pos - Center of the box
976
+ * @param {Vector3} size - Full size of the box
977
+ * @param {Vector3} [rotation] - How the box is turned, upright when left out
978
+ * @return {number|undefined}
979
+ * @memberof Math3D
980
+ */
981
+ function raycastBox(ray, pos, size, rotation)
982
+ {
983
+ if (isTurned3D(rotation))
984
+ {
985
+ // the ray in the box's own space, where the box is upright; turning keeps the distances
986
+ const axes = boxAxes3D(rotation), d = ray.direction;
987
+ const direction = vec3(d.dot(axes[0]), d.dot(axes[1]), d.dot(axes[2]));
988
+ return raycastBox(new Ray3D(boxLocal3D(ray.origin, pos, axes), direction), vec3(), size);
989
+ }
990
+ const {origin, direction} = ray;
991
+ const h = size.scale(.5);
992
+ const boxMin = pos.subtract(h), boxMax = pos.add(h);
993
+ let tMin = 0, tMax = Infinity;
994
+ for (const axis of 'xyz')
995
+ {
996
+ const o = origin[axis], d = direction[axis];
997
+ const mn = boxMin[axis], mx = boxMax[axis];
998
+ if (!d)
999
+ {
1000
+ if (o < mn || o > mx)
1001
+ return undefined; // ray is parallel to this slab and outside it
1002
+ continue;
1003
+ }
1004
+ let t0 = (mn - o)/d;
1005
+ let t1 = (mx - o)/d;
1006
+ if (t0 > t1)
1007
+ [t0, t1] = [t1, t0];
1008
+ tMin = max(tMin, t0);
1009
+ tMax = min(tMax, t1);
1010
+ if (tMin > tMax)
1011
+ return undefined;
1012
+ }
1013
+ return tMin;
1014
+ }