littlejsengine 1.18.29 → 1.20.0

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 (51) hide show
  1. package/FAQ.md +841 -815
  2. package/README.md +211 -199
  3. package/dist/littlejs.d.ts +4383 -812
  4. package/dist/littlejs.esm.js +31848 -16680
  5. package/dist/littlejs.esm.min.js +14 -1
  6. package/dist/littlejs.js +31230 -16166
  7. package/dist/littlejs.min.js +13 -1
  8. package/dist/littlejs.release.js +27419 -15457
  9. package/package.json +10 -5
  10. package/plugins/audioEffects.js +375 -0
  11. package/plugins/box2d.js +2516 -2073
  12. package/plugins/drawUtilities.js +206 -31
  13. package/plugins/gltf.js +740 -0
  14. package/plugins/jsconfig.json +19 -0
  15. package/plugins/lightSystem.js +719 -330
  16. package/plugins/math3d.js +911 -0
  17. package/plugins/medalSystem.js +118 -72
  18. package/plugins/newgrounds.js +400 -199
  19. package/plugins/pathFinder.js +208 -175
  20. package/plugins/pluginExport.js +196 -115
  21. package/plugins/postProcess.js +284 -166
  22. package/plugins/render3d.js +4072 -0
  23. package/plugins/render3dDebug.js +109 -0
  24. package/plugins/render3dExtras.js +1754 -0
  25. package/plugins/render3dVoxels.js +502 -0
  26. package/plugins/textureSheet.js +86 -48
  27. package/plugins/threejs.js +183 -158
  28. package/plugins/tweakables.js +411 -0
  29. package/plugins/tweenSystem.js +646 -526
  30. package/plugins/uiSystem.js +2024 -1687
  31. package/plugins/zzfxm.js +8 -15
  32. package/src/engine.js +345 -194
  33. package/src/engineAudio.js +1022 -777
  34. package/src/engineBuild.mjs +289 -253
  35. package/src/engineDebug.js +912 -735
  36. package/src/engineDraw.js +1774 -1510
  37. package/src/engineEditor.js +2431 -0
  38. package/src/engineExport.js +419 -396
  39. package/src/engineInput.js +1462 -1293
  40. package/src/engineLogo.js +4 -3
  41. package/src/engineMath.js +1274 -1183
  42. package/src/engineObject.js +674 -551
  43. package/src/engineParticles.js +583 -532
  44. package/src/engineRelease.js +67 -45
  45. package/src/engineSettings.js +808 -726
  46. package/src/engineTileLayer.js +995 -741
  47. package/src/engineUtilities.js +32 -13
  48. package/src/engineWebGL.js +1162 -941
  49. package/src/jsconfig.json +17 -13
  50. package/plugins/box2d.wasm.js +0 -630
  51. package/plugins/box2d.wasm.wasm +0 -0
@@ -0,0 +1,911 @@
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
+ const cx = cos(euler.x), sx = sin(euler.x);
342
+ const cy = cos(euler.y), sy = sin(euler.y);
343
+ const cz = cos(euler.z), sz = sin(euler.z);
344
+ const m = matrix.m;
345
+ // R = Ry * Rx * Rz written out, column major, every element set so a reused matrix comes out clean
346
+ m[0] = cy*cz + sy*sx*sz; m[1] = cx*sz; m[2] = -sy*cz + cy*sx*sz; m[3] = 0;
347
+ m[4] = -cy*sz + sy*sx*cz; m[5] = cx*cz; m[6] = sy*sz + cy*sx*cz; m[7] = 0;
348
+ m[8] = sy*cx; m[9] = -sx; m[10] = cy*cx; m[11] = 0;
349
+ m[12] = m[13] = m[14] = 0; m[15] = 1;
350
+ return matrix;
351
+ }
352
+
353
+ /** Returns a new scale matrix
354
+ * @param {Vector3} v
355
+ * @return {Matrix4} */
356
+ static scaling(v)
357
+ {
358
+ ASSERT_VECTOR3_VALID(v);
359
+ const r = new Matrix4;
360
+ r.m[0] = v.x; r.m[5] = v.y; r.m[10] = v.z;
361
+ return r;
362
+ }
363
+
364
+ /** Returns a new perspective projection, camera looks down -Z
365
+ * @param {number} fov - Vertical field of view in radians
366
+ * @param {number} aspect - Width divided by height
367
+ * @param {number} near - Closest visible distance
368
+ * @param {number} far - Furthest visible distance, Infinity is allowed
369
+ * @return {Matrix4} */
370
+ static perspective(fov, aspect, near, far)
371
+ {
372
+ ASSERT(near > 0 && far > near, 'a perspective projection needs 0 < near < far, or nothing is visible', near, far);
373
+ const f = 1 / tan(fov/2);
374
+ const r = new Matrix4;
375
+ const m = r.m;
376
+ m[0] = f / aspect;
377
+ m[5] = f;
378
+ m[10] = far == Infinity ? -1 : (far + near) / (near - far); // the infinite case is the limit of the formula
379
+ m[11] = -1;
380
+ m[14] = far == Infinity ? -2 * near : 2 * far * near / (near - far);
381
+ m[15] = 0;
382
+ return r;
383
+ }
384
+
385
+ /** Returns a new orthographic projection, camera looks down -Z
386
+ * @param {number} left - Edge of the visible box
387
+ * @param {number} right - Edge of the visible box
388
+ * @param {number} bottom - Edge of the visible box
389
+ * @param {number} top - Edge of the visible box
390
+ * @param {number} near - Closest visible distance
391
+ * @param {number} far - Furthest visible distance, Infinity is not allowed here
392
+ * @return {Matrix4} */
393
+ static orthographic(left, right, bottom, top, near, far)
394
+ {
395
+ // an infinite far plane has no orthographic form: every depth would land on the near plane,
396
+ // and the formula below works out to NaN, which quietly clips the whole scene away
397
+ ASSERT(far > near && far != Infinity, 'an orthographic projection needs a real far plane past near, Infinity is perspective only', near, far);
398
+ const r = new Matrix4;
399
+ const m = r.m;
400
+ m[0] = 2 / (right - left);
401
+ m[5] = 2 / (top - bottom);
402
+ m[10] = -2 / (far - near);
403
+ m[12] = -(right + left) / (right - left);
404
+ m[13] = -(top + bottom) / (top - bottom);
405
+ m[14] = -(far + near) / (far - near);
406
+ return r;
407
+ }
408
+
409
+ /** Returns the transform of something at eye turned to face target
410
+ * - Invert it to get a view matrix for a camera there
411
+ * @param {Vector3} eye
412
+ * @param {Vector3} target
413
+ * @param {Vector3} [up]
414
+ * @return {Matrix4} */
415
+ static lookAt(eye, target, up=vec3(0, 1, 0))
416
+ {
417
+ let z = eye.subtract(target).normalize();
418
+ if (!z.lengthSquared())
419
+ z = vec3(0, 0, 1); // eye is on the target, face -Z
420
+ let x = up.cross(z).normalize();
421
+ // up is along the view direction, pick another; looking straight down or up keeps +X as the right axis,
422
+ // as three.js does and as a view tilting there from the +Z side ends up
423
+ if (!x.lengthSquared())
424
+ x = (abs(z.y) > .99 ? vec3(0, 0, z.y > 0 ? -1 : 1) : vec3(0, 1, 0)).cross(z).normalize();
425
+ const y = z.cross(x);
426
+ 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]);
427
+ }
428
+
429
+ /** Returns a new matrix that is a copy of this
430
+ * @return {Matrix4} */
431
+ copy() { return new Matrix4(this.m); }
432
+
433
+ /** Multiply this matrix by another and return this, the other happens first
434
+ * @param {Matrix4} matrix
435
+ * @return {Matrix4} */
436
+ multiply(matrix)
437
+ {
438
+ const a = this.m, b = matrix.m, r = matrix4Scratch;
439
+ for (let j = 0; j < 4; ++j)
440
+ for (let i = 0; i < 4; ++i)
441
+ 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];
442
+ this.m.set(r);
443
+ return this;
444
+ }
445
+
446
+ /** Append a translation, returns self
447
+ * @param {Vector3} v
448
+ * @return {Matrix4} */
449
+ translate(v) { return this.multiply(Matrix4.translation(v)); }
450
+
451
+ /** Append a rotation, returns self
452
+ * @param {Vector3} euler - vec3(pitch, yaw, roll) in radians
453
+ * @return {Matrix4} */
454
+ rotate(euler) { return this.multiply(Matrix4.rotation(euler)); }
455
+
456
+ /** Append a scale, returns self
457
+ * @param {Vector3} v
458
+ * @return {Matrix4} */
459
+ scale(v) { return this.multiply(Matrix4.scaling(v)); }
460
+
461
+ /** Transpose this matrix in place, returns self
462
+ * @return {Matrix4} */
463
+ transpose()
464
+ {
465
+ const m = this.m;
466
+ for (let i = 0; i < 4; ++i)
467
+ for (let j = i + 1; j < 4; ++j)
468
+ {
469
+ const t = m[i*4 + j];
470
+ m[i*4 + j] = m[j*4 + i];
471
+ m[j*4 + i] = t;
472
+ }
473
+ return this;
474
+ }
475
+
476
+ /** Flip this matrix so it undoes itself, returns this and does nothing if it cannot be inverted
477
+ * @return {Matrix4} */
478
+ invert()
479
+ {
480
+ const m = this.m;
481
+ const [a00, a01, a02, a03, a10, a11, a12, a13, a20, a21, a22, a23, a30, a31, a32, a33] = m;
482
+ const b00 = a00*a11 - a01*a10, b01 = a00*a12 - a02*a10, b02 = a00*a13 - a03*a10;
483
+ const b03 = a01*a12 - a02*a11, b04 = a01*a13 - a03*a11, b05 = a02*a13 - a03*a12;
484
+ const b06 = a20*a31 - a21*a30, b07 = a20*a32 - a22*a30, b08 = a20*a33 - a23*a30;
485
+ const b09 = a21*a32 - a22*a31, b10 = a21*a33 - a23*a31, b11 = a22*a33 - a23*a32;
486
+ let det = b00*b11 - b01*b10 + b02*b09 + b03*b08 - b04*b07 + b05*b06;
487
+ if (!det)
488
+ return this;
489
+ det = 1 / det;
490
+ m[0] = (a11*b11 - a12*b10 + a13*b09) * det;
491
+ m[1] = (a02*b10 - a01*b11 - a03*b09) * det;
492
+ m[2] = (a31*b05 - a32*b04 + a33*b03) * det;
493
+ m[3] = (a22*b04 - a21*b05 - a23*b03) * det;
494
+ m[4] = (a12*b08 - a10*b11 - a13*b07) * det;
495
+ m[5] = (a00*b11 - a02*b08 + a03*b07) * det;
496
+ m[6] = (a32*b02 - a30*b05 - a33*b01) * det;
497
+ m[7] = (a20*b05 - a22*b02 + a23*b01) * det;
498
+ m[8] = (a10*b10 - a11*b08 + a13*b06) * det;
499
+ m[9] = (a01*b08 - a00*b10 - a03*b06) * det;
500
+ m[10] = (a30*b04 - a31*b02 + a33*b00) * det;
501
+ m[11] = (a21*b02 - a20*b04 - a23*b00) * det;
502
+ m[12] = (a11*b07 - a10*b09 - a12*b06) * det;
503
+ m[13] = (a00*b09 - a01*b07 + a02*b06) * det;
504
+ m[14] = (a31*b01 - a30*b03 - a32*b00) * det;
505
+ m[15] = (a20*b03 - a21*b01 + a22*b00) * det;
506
+ return this;
507
+ }
508
+
509
+ /** Transform a point, translation included
510
+ * @param {Vector3} v
511
+ * @return {Vector3} */
512
+ transformPoint(v)
513
+ {
514
+ const m = this.m;
515
+ return new Vector3(
516
+ m[0]*v.x + m[4]*v.y + m[8]*v.z + m[12],
517
+ m[1]*v.x + m[5]*v.y + m[9]*v.z + m[13],
518
+ m[2]*v.x + m[6]*v.y + m[10]*v.z + m[14]);
519
+ }
520
+
521
+ /** Transform a direction, rotation and scale only
522
+ * @param {Vector3} v
523
+ * @return {Vector3} */
524
+ transformDirection(v)
525
+ {
526
+ const m = this.m;
527
+ return new Vector3(
528
+ m[0]*v.x + m[4]*v.y + m[8]*v.z,
529
+ m[1]*v.x + m[5]*v.y + m[9]*v.z,
530
+ m[2]*v.x + m[6]*v.y + m[10]*v.z);
531
+ }
532
+
533
+ /** Returns the translation part of this matrix
534
+ * @return {Vector3} */
535
+ getTranslation() { return new Vector3(this.m[12], this.m[13], this.m[14]); }
536
+
537
+ /** Returns the determinant of the rotation and scale part: negative when the matrix mirrors, 0 when it flattens a
538
+ * shape and has no inverse
539
+ * @return {number} */
540
+ determinant()
541
+ {
542
+ const m = this.m;
543
+ 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]);
544
+ }
545
+
546
+ /** Returns the scale part of this matrix, the length of each axis; a mirroring matrix shows as a negative x
547
+ * @return {Vector3} */
548
+ getScale()
549
+ {
550
+ const m = this.m;
551
+ 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]);
552
+ return new Vector3(this.determinant() < 0 ? -x : x, y, z);
553
+ }
554
+
555
+ /** Returns the rotation part of this matrix as vec3(pitch, yaw, roll), the angles Matrix4.rotation builds it from
556
+ * - The scale is divided out first, so it works on a full transform
557
+ * - A matrix with shear, from a scaled parent with a turned child, has no exact answer and gets the nearest
558
+ * @return {Vector3} */
559
+ getRotation()
560
+ {
561
+ // the axes at unit length, see Matrix4.rotation for which element is which
562
+ const m = this.m, s = this.getScale();
563
+ const sx = s.x || 1, sy = s.y || 1, sz = s.z || 1;
564
+ const m1 = m[1] / sx, m5 = m[5] / sy, m8 = m[8] / sz, m9 = m[9] / sz, m10 = m[10] / sz;
565
+ const pitch = Math.asin(clamp(-m9, -1, 1));
566
+ if (abs(m9) < 1 - 1e-6)
567
+ return new Vector3(pitch, atan2(m8, m10), atan2(m1, m5));
568
+ // straight up or down: yaw and roll turn about the same axis, so the roll is zero and yaw takes it all
569
+ return new Vector3(pitch, atan2(m[4] / sy * -m9, m[0] / sx), 0);
570
+ }
571
+
572
+ /** Returns a string representation of this matrix for debugging
573
+ * @return {string} */
574
+ toString()
575
+ {
576
+ const m = this.m, f = (i)=> m[i].toFixed(2).padStart(7);
577
+ let s = '';
578
+ for (let row = 0; row < 4; ++row)
579
+ s += `[${f(row)} ${f(4 + row)} ${f(8 + row)} ${f(12 + row)} ]\n`;
580
+ return s;
581
+ }
582
+ }
583
+
584
+ ///////////////////////////////////////////////////////////////////////////////
585
+
586
+ /**
587
+ * Build a transform for an object from its position, rotation and scale
588
+ * - A point is scaled first, then rotated, then moved, which is what you want for a game object
589
+ * @param {Vector3} [pos]
590
+ * @param {Vector3} [rotation] - vec3(pitch, yaw, roll) in radians
591
+ * @param {Vector3} [scale]
592
+ * @param {Matrix4} [matrix] - Written into instead of a new one, for a loop that builds many
593
+ * @return {Matrix4}
594
+ * @memberof Math3D
595
+ */
596
+ function buildMatrix(pos, rotation, scale, matrix=new Matrix4)
597
+ {
598
+ ASSERT(!pos || isVector3(pos), 'pos must be a Vector3', pos);
599
+ ASSERT(!scale || isVector3(scale), 'scale must be a Vector3', scale);
600
+ ASSERT(matrix instanceof Matrix4, 'the matrix to write into must be a Matrix4');
601
+ // scale the rotation columns and drop the position in, instead of multiplying three matrices
602
+ // an object that is not turned at all is most of a big scene, and identity is what the six
603
+ // trig calls would have worked out to anyway
604
+ const turned = rotation && (rotation.x || rotation.y || rotation.z), m = matrix.m;
605
+ turned ? Matrix4.rotation(rotation, matrix) : m.set(matrix4Identity);
606
+ if (scale)
607
+ {
608
+ m[0] *= scale.x; m[1] *= scale.x; m[2] *= scale.x;
609
+ m[4] *= scale.y; m[5] *= scale.y; m[6] *= scale.y;
610
+ m[8] *= scale.z; m[9] *= scale.z; m[10] *= scale.z;
611
+ }
612
+ if (pos)
613
+ m[12] = pos.x, m[13] = pos.y, m[14] = pos.z;
614
+ return matrix;
615
+ }
616
+
617
+ ///////////////////////////////////////////////////////////////////////////////
618
+ /**
619
+ * Ray3D - A start point and a direction, what screenToRay returns and the raycast helpers take
620
+ * - The direction need not be unit length, the distances that come back are in units of it
621
+ * @memberof Math3D
622
+ * @example
623
+ * const ray = render3D.screenToRay(mousePosScreen);
624
+ * const distance = raycastPlane(ray, vec3(), vec3(0, 1, 0));
625
+ * if (distance !== undefined)
626
+ * ball.pos3D = ray.getPosition(distance);
627
+ */
628
+ class Ray3D
629
+ {
630
+ /** Create a ray
631
+ * @param {Vector3} [origin]
632
+ * @param {Vector3} [direction] - Defaults to -Z, forward */
633
+ constructor(origin=vec3(), direction=vec3(0, 0, -1))
634
+ {
635
+ ASSERT_VECTOR3_VALID(origin);
636
+ ASSERT_VECTOR3_VALID(direction);
637
+ /** @property {Vector3} - Where the ray starts */
638
+ this.origin = origin;
639
+ /** @property {Vector3} - Which way it goes */
640
+ this.direction = direction;
641
+ }
642
+
643
+ /** Returns the point a distance along the ray
644
+ * @param {number} distance - What the raycast helpers return
645
+ * @return {Vector3} */
646
+ getPosition(distance) { return this.origin.add(this.direction.scale(distance)); }
647
+
648
+ /** Returns a new ray that is a copy of this
649
+ * @return {Ray3D} */
650
+ copy() { return new Ray3D(this.origin.copy(), this.direction.copy()); }
651
+ }
652
+
653
+ ///////////////////////////////////////////////////////////////////////////////
654
+ // 3D collision helpers, none of them change anything that is passed in
655
+ // Boxes sit centered on pos and take a full size, like drawRect
656
+ // Cylinders stand up the Y axis, centered on pos, with a full height
657
+ // Names that could be mistaken for 2D functions get a 3D suffix
658
+
659
+ /**
660
+ * Check if a point is inside an axis aligned box, boundary is inclusive
661
+ * @param {Vector3} point
662
+ * @param {Vector3} pos - Center of the box
663
+ * @param {Vector3} size - Full size of the box
664
+ * @return {boolean}
665
+ * @memberof Math3D
666
+ */
667
+ function isPointInBox3D(point, pos, size)
668
+ {
669
+ return abs(point.x - pos.x) <= size.x/2 &&
670
+ abs(point.y - pos.y) <= size.y/2 &&
671
+ abs(point.z - pos.z) <= size.z/2;
672
+ }
673
+
674
+ /**
675
+ * Check if two axis aligned boxes are overlapping, touching edges do not overlap
676
+ * @param {Vector3} posA
677
+ * @param {Vector3} sizeA - Full size of box A
678
+ * @param {Vector3} posB
679
+ * @param {Vector3} [sizeB] - Full size of box B, zero for a point
680
+ * @return {boolean}
681
+ * @memberof Math3D
682
+ */
683
+ function isOverlapping3D(posA, sizeA, posB, sizeB=vec3())
684
+ {
685
+ const d = posA.subtract(posB);
686
+ return abs(d.x) < (sizeA.x + sizeB.x)/2 &&
687
+ abs(d.y) < (sizeA.y + sizeB.y)/2 &&
688
+ abs(d.z) < (sizeA.z + sizeB.z)/2;
689
+ }
690
+
691
+ /**
692
+ * Returns the vector to move sphere A by so it no longer overlaps sphere B, or undefined
693
+ * @param {Vector3} posA
694
+ * @param {number} radiusA
695
+ * @param {Vector3} posB
696
+ * @param {number} radiusB
697
+ * @return {Vector3|undefined}
698
+ * @memberof Math3D
699
+ */
700
+ function collideSphereSphere(posA, radiusA, posB, radiusB)
701
+ {
702
+ const d = posA.subtract(posB);
703
+ const r = radiusA + radiusB;
704
+ const dist = d.length();
705
+ if (dist >= r)
706
+ return undefined;
707
+ if (!dist)
708
+ return vec3(0, r, 0); // coincident centers, push straight up
709
+ return d.normalize(r - dist);
710
+ }
711
+
712
+ /**
713
+ * Returns the vector to move a sphere out of an axis aligned box, or undefined
714
+ * @param {Vector3} pos - Sphere center
715
+ * @param {number} radius
716
+ * @param {Vector3} boxPos
717
+ * @param {Vector3} boxSize - Full size of the box
718
+ * @return {Vector3|undefined}
719
+ * @memberof Math3D
720
+ */
721
+ function collideSphereBox(pos, radius, boxPos, boxSize)
722
+ {
723
+ const h = boxSize.scale(.5);
724
+ const closest = vec3(
725
+ clamp(pos.x, boxPos.x - h.x, boxPos.x + h.x),
726
+ clamp(pos.y, boxPos.y - h.y, boxPos.y + h.y),
727
+ clamp(pos.z, boxPos.z - h.z, boxPos.z + h.z));
728
+ const d = pos.subtract(closest), distSq = d.lengthSquared();
729
+ if (distSq)
730
+ return distSq >= radius*radius ? undefined : d.normalize(radius - distSq**.5);
731
+
732
+ // center is inside the box, push out along the axis of least penetration
733
+ const offset = pos.subtract(boxPos);
734
+ return pushOutAxis3D(offset, h.x - abs(offset.x), h.y - abs(offset.y), h.z - abs(offset.z), radius);
735
+ }
736
+
737
+ /**
738
+ * Returns the vector to move a sphere back inside an axis aligned box, or undefined when it is all inside
739
+ * - The inside out twin of collideSphereBox, for keeping things in a room or an arena
740
+ * - A sphere too big for the box on some axis is held at the middle of it on that axis
741
+ * @param {Vector3} pos - Sphere center
742
+ * @param {number} radius
743
+ * @param {Vector3} boxPos
744
+ * @param {Vector3} boxSize - Full size of the box
745
+ * @return {Vector3|undefined}
746
+ * @memberof Math3D
747
+ */
748
+ function collideSphereInBox(pos, radius, boxPos, boxSize)
749
+ {
750
+ // the box shrunk by the radius is everywhere the center can be, so clamp the center into it
751
+ const x = max(0, boxSize.x/2 - radius), y = max(0, boxSize.y/2 - radius), z = max(0, boxSize.z/2 - radius);
752
+ const push = vec3(
753
+ clamp(pos.x, boxPos.x - x, boxPos.x + x) - pos.x,
754
+ clamp(pos.y, boxPos.y - y, boxPos.y + y) - pos.y,
755
+ clamp(pos.z, boxPos.z - z, boxPos.z + z) - pos.z);
756
+ return push.lengthSquared() ? push : undefined;
757
+ }
758
+
759
+ // the axis with the smallest penetration, pointing the way d does, with extra distance added
760
+ function pushOutAxis3D(d, penX, penY, penZ, extra=0)
761
+ {
762
+ const s = (v)=> v >= 0 ? 1 : -1; // sign() gives 0 on a tie, which would be no push
763
+ if (penX <= penY && penX <= penZ)
764
+ return vec3(s(d.x)*(penX + extra), 0, 0);
765
+ if (penY <= penZ)
766
+ return vec3(0, s(d.y)*(penY + extra), 0);
767
+ return vec3(0, 0, s(d.z)*(penZ + extra));
768
+ }
769
+
770
+ /**
771
+ * Returns the vector to move a sphere out of a vertical cylinder, or undefined
772
+ * @param {Vector3} pos - Sphere center
773
+ * @param {number} radius
774
+ * @param {Vector3} cylinderPos
775
+ * @param {number} cylinderRadius
776
+ * @param {number} cylinderHeight - Full height along Y
777
+ * @return {Vector3|undefined}
778
+ * @memberof Math3D
779
+ */
780
+ function collideSphereCylinder(pos, radius, cylinderPos, cylinderRadius, cylinderHeight)
781
+ {
782
+ const halfHeight = cylinderHeight/2;
783
+ const offsetX = pos.x - cylinderPos.x;
784
+ const offsetZ = pos.z - cylinderPos.z;
785
+ const offsetY = pos.y - cylinderPos.y;
786
+ const radialDist = (offsetX**2 + offsetZ**2)**.5;
787
+ const radialScale = radialDist ? min(radialDist, cylinderRadius)/radialDist : 0; // pull the point onto the wall, or the axis
788
+ const closest = vec3(
789
+ cylinderPos.x + offsetX*radialScale,
790
+ clamp(pos.y, cylinderPos.y - halfHeight, cylinderPos.y + halfHeight),
791
+ cylinderPos.z + offsetZ*radialScale);
792
+ const d = pos.subtract(closest), distSq = d.lengthSquared();
793
+ if (distSq)
794
+ return distSq >= radius*radius ? undefined : d.normalize(radius - distSq**.5);
795
+
796
+ // center is inside the cylinder, push out through the nearer surface
797
+ const sidePen = cylinderRadius - radialDist;
798
+ const capPen = halfHeight - abs(offsetY);
799
+ if (sidePen <= capPen)
800
+ {
801
+ const dir = radialDist ? vec3(offsetX/radialDist, 0, offsetZ/radialDist) : vec3(1, 0, 0);
802
+ return dir.scale(sidePen + radius);
803
+ }
804
+ return vec3(0, (offsetY >= 0 ? 1 : -1)*(capPen + radius), 0);
805
+ }
806
+
807
+ /**
808
+ * Returns the vector to move box A by so it no longer overlaps box B, the shortest way out, or undefined
809
+ * - The 3D twin of collideBoxBox
810
+ * @param {Vector3} posA
811
+ * @param {Vector3} sizeA - Full size of box A
812
+ * @param {Vector3} posB
813
+ * @param {Vector3} sizeB - Full size of box B
814
+ * @return {Vector3|undefined}
815
+ * @memberof Math3D
816
+ */
817
+ function collideBoxBox3D(posA, sizeA, posB, sizeB)
818
+ {
819
+ const d = posA.subtract(posB);
820
+ const overlapX = (sizeA.x + sizeB.x)/2 - abs(d.x);
821
+ const overlapY = (sizeA.y + sizeB.y)/2 - abs(d.y);
822
+ const overlapZ = (sizeA.z + sizeB.z)/2 - abs(d.z);
823
+ if (overlapX <= 0 || overlapY <= 0 || overlapZ <= 0)
824
+ return undefined;
825
+ return pushOutAxis3D(d, overlapX, overlapY, overlapZ);
826
+ }
827
+
828
+ /**
829
+ * Returns the distance along the ray to the first intersection with a sphere, or undefined
830
+ * - The hit is ray.getPosition(distance), a direction that is not unit length scales the distance
831
+ * - A ray starting inside the sphere is already there, so it gets back 0
832
+ * @param {Ray3D} ray
833
+ * @param {Vector3} pos - Sphere center
834
+ * @param {number} radius
835
+ * @return {number|undefined}
836
+ * @memberof Math3D
837
+ */
838
+ function raycastSphere(ray, pos, radius)
839
+ {
840
+ const {origin, direction} = ray;
841
+ const oc = origin.subtract(pos);
842
+ const c = oc.dot(oc) - radius*radius;
843
+ if (c < 0)
844
+ return 0; // origin is inside the sphere, even for a ray of no length, as raycastBox gives
845
+ const a = direction.dot(direction);
846
+ if (!a)
847
+ return undefined;
848
+ const b = 2*oc.dot(direction);
849
+ const discriminant = b*b - 4*a*c;
850
+ if (discriminant < 0)
851
+ return undefined;
852
+ const t = (-b - discriminant**.5)/(2*a);
853
+ return t >= 0 ? t : undefined;
854
+ }
855
+
856
+ /**
857
+ * Returns the distance along the ray to a plane, or undefined if parallel or behind
858
+ * - The hit is ray.getPosition(distance), a direction that is not unit length scales the distance
859
+ * @param {Ray3D} ray
860
+ * @param {Vector3} planePos
861
+ * @param {Vector3} planeNormal
862
+ * @return {number|undefined}
863
+ * @memberof Math3D
864
+ */
865
+ function raycastPlane(ray, planePos, planeNormal)
866
+ {
867
+ const {origin, direction} = ray;
868
+ const denominator = direction.dot(planeNormal);
869
+ if (abs(denominator) < 1e-9)
870
+ return undefined;
871
+ const t = planePos.subtract(origin).dot(planeNormal)/denominator;
872
+ return t < 0 ? undefined : t;
873
+ }
874
+
875
+ /**
876
+ * Returns the distance along the ray to the first intersection with an axis aligned box, or undefined
877
+ * - The hit is ray.getPosition(distance), a direction that is not unit length scales the distance
878
+ * - A ray starting inside the box is already there, so it gets back 0
879
+ * @param {Ray3D} ray
880
+ * @param {Vector3} pos - Center of the box
881
+ * @param {Vector3} size - Full size of the box
882
+ * @return {number|undefined}
883
+ * @memberof Math3D
884
+ */
885
+ function raycastBox(ray, pos, size)
886
+ {
887
+ const {origin, direction} = ray;
888
+ const h = size.scale(.5);
889
+ const boxMin = pos.subtract(h), boxMax = pos.add(h);
890
+ let tMin = 0, tMax = Infinity;
891
+ for (const axis of 'xyz')
892
+ {
893
+ const o = origin[axis], d = direction[axis];
894
+ const mn = boxMin[axis], mx = boxMax[axis];
895
+ if (!d)
896
+ {
897
+ if (o < mn || o > mx)
898
+ return undefined; // ray is parallel to this slab and outside it
899
+ continue;
900
+ }
901
+ let t0 = (mn - o)/d;
902
+ let t1 = (mx - o)/d;
903
+ if (t0 > t1)
904
+ [t0, t1] = [t1, t0];
905
+ tMin = max(tMin, t0);
906
+ tMax = min(tMax, t1);
907
+ if (tMin > tMax)
908
+ return undefined;
909
+ }
910
+ return tMin;
911
+ }