littlejsengine 1.18.28 → 1.19.3
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.
- package/FAQ.md +5 -0
- package/README.md +207 -199
- package/dist/littlejs.d.ts +2099 -48
- package/dist/littlejs.esm.js +16251 -10315
- package/dist/littlejs.esm.min.js +13 -1
- package/dist/littlejs.js +15747 -9888
- package/dist/littlejs.min.js +13 -1
- package/dist/littlejs.release.js +15719 -9867
- package/package.json +1 -1
- package/plugins/audioEffects.js +371 -0
- package/plugins/lightSystem.js +5 -3
- package/plugins/math3d.js +870 -0
- package/plugins/medalSystem.js +280 -280
- package/plugins/pluginExport.js +181 -115
- package/plugins/postProcess.js +241 -166
- package/plugins/render3d.js +4006 -0
- package/plugins/textureSheet.js +458 -458
- package/plugins/threejs.js +6 -1
- package/plugins/tweenSystem.js +528 -526
- package/plugins/zzfxm.js +159 -166
- package/src/engine.js +172 -137
- package/src/engineAudio.js +918 -775
- package/src/engineBuild.mjs +3 -0
- package/src/engineDebug.js +15 -8
- package/src/engineDraw.js +1672 -1510
- package/src/engineExport.js +407 -396
- package/src/engineInput.js +1 -1
- package/src/engineLogo.js +4 -3
- package/src/engineMath.js +69 -0
- package/src/engineObject.js +560 -551
- package/src/engineSettings.js +759 -725
- package/src/engineTileLayer.js +3 -3
- package/src/engineUtilities.js +13 -0
- package/src/engineWebGL.js +1005 -941
|
@@ -0,0 +1,870 @@
|
|
|
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
|
+
|
|
292
|
+
/**
|
|
293
|
+
* 4x4 transform matrix for moving, rotating and scaling points in 3D
|
|
294
|
+
* - Static builders like Matrix4.translation return a new matrix
|
|
295
|
+
* - Methods on a matrix change it in place and return it, so calls can chain
|
|
296
|
+
* - a.multiply(b) means b happens first, then a
|
|
297
|
+
* - Stored the way WebGL wants it, so it can be sent to a shader as is
|
|
298
|
+
* @memberof Math3D
|
|
299
|
+
* @example
|
|
300
|
+
* const m = buildMatrix(vec3(0, 1, 0), vec3(0, PI/2, 0)); // rotate then move up
|
|
301
|
+
* const p = m.transformPoint(vec3(1, 0, 0));
|
|
302
|
+
*/
|
|
303
|
+
class Matrix4
|
|
304
|
+
{
|
|
305
|
+
/** Create a matrix, identity by default
|
|
306
|
+
* @param {Float32Array|Array<number>} [m] - 16 column major values */
|
|
307
|
+
constructor(m)
|
|
308
|
+
{
|
|
309
|
+
/** @property {Float32Array} - The 16 column major values */
|
|
310
|
+
this.m = new Float32Array(16);
|
|
311
|
+
ASSERT(!m || m.length == 16, 'Matrix4 takes 16 values, use copy() to duplicate a matrix');
|
|
312
|
+
if (m)
|
|
313
|
+
this.m.set(m);
|
|
314
|
+
else
|
|
315
|
+
this.m[0] = this.m[5] = this.m[10] = this.m[15] = 1;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Returns a new identity matrix
|
|
319
|
+
* @return {Matrix4} */
|
|
320
|
+
static identity() { return new Matrix4; }
|
|
321
|
+
|
|
322
|
+
/** Returns a new translation matrix
|
|
323
|
+
* @param {Vector3} v
|
|
324
|
+
* @return {Matrix4} */
|
|
325
|
+
static translation(v)
|
|
326
|
+
{
|
|
327
|
+
ASSERT_VECTOR3_VALID(v);
|
|
328
|
+
const r = new Matrix4;
|
|
329
|
+
r.m[12] = v.x; r.m[13] = v.y; r.m[14] = v.z;
|
|
330
|
+
return r;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Returns a new rotation matrix, rolled first, then pitched, then yawed
|
|
334
|
+
* @param {Vector3} euler - vec3(pitch, yaw, roll) in radians
|
|
335
|
+
* @return {Matrix4} */
|
|
336
|
+
static rotation(euler)
|
|
337
|
+
{
|
|
338
|
+
ASSERT_VECTOR3_VALID(euler);
|
|
339
|
+
const cx = cos(euler.x), sx = sin(euler.x);
|
|
340
|
+
const cy = cos(euler.y), sy = sin(euler.y);
|
|
341
|
+
const cz = cos(euler.z), sz = sin(euler.z);
|
|
342
|
+
const r = new Matrix4;
|
|
343
|
+
const m = r.m;
|
|
344
|
+
// R = Ry * Rx * Rz written out, column major
|
|
345
|
+
m[0] = cy*cz + sy*sx*sz; m[1] = cx*sz; m[2] = -sy*cz + cy*sx*sz;
|
|
346
|
+
m[4] = -cy*sz + sy*sx*cz; m[5] = cx*cz; m[6] = sy*sz + cy*sx*cz;
|
|
347
|
+
m[8] = sy*cx; m[9] = -sx; m[10] = cy*cx;
|
|
348
|
+
return r;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Returns a new scale matrix
|
|
352
|
+
* @param {Vector3} v
|
|
353
|
+
* @return {Matrix4} */
|
|
354
|
+
static scaling(v)
|
|
355
|
+
{
|
|
356
|
+
ASSERT_VECTOR3_VALID(v);
|
|
357
|
+
const r = new Matrix4;
|
|
358
|
+
r.m[0] = v.x; r.m[5] = v.y; r.m[10] = v.z;
|
|
359
|
+
return r;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Returns a new perspective projection, camera looks down -Z
|
|
363
|
+
* @param {number} fov - Vertical field of view in radians
|
|
364
|
+
* @param {number} aspect - Width divided by height
|
|
365
|
+
* @param {number} near - Closest visible distance
|
|
366
|
+
* @param {number} far - Furthest visible distance, Infinity is allowed
|
|
367
|
+
* @return {Matrix4} */
|
|
368
|
+
static perspective(fov, aspect, near, far)
|
|
369
|
+
{
|
|
370
|
+
ASSERT(near > 0 && far > near, 'a perspective projection needs 0 < near < far, or nothing is visible', near, far);
|
|
371
|
+
const f = 1 / tan(fov/2);
|
|
372
|
+
const r = new Matrix4;
|
|
373
|
+
const m = r.m;
|
|
374
|
+
m[0] = f / aspect;
|
|
375
|
+
m[5] = f;
|
|
376
|
+
m[10] = far == Infinity ? -1 : (far + near) / (near - far); // the infinite case is the limit of the formula
|
|
377
|
+
m[11] = -1;
|
|
378
|
+
m[14] = far == Infinity ? -2 * near : 2 * far * near / (near - far);
|
|
379
|
+
m[15] = 0;
|
|
380
|
+
return r;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Returns a new orthographic projection, camera looks down -Z
|
|
384
|
+
* @param {number} left - Edge of the visible box
|
|
385
|
+
* @param {number} right - Edge of the visible box
|
|
386
|
+
* @param {number} bottom - Edge of the visible box
|
|
387
|
+
* @param {number} top - Edge of the visible box
|
|
388
|
+
* @param {number} near - Closest visible distance
|
|
389
|
+
* @param {number} far - Furthest visible distance, Infinity is not allowed here
|
|
390
|
+
* @return {Matrix4} */
|
|
391
|
+
static orthographic(left, right, bottom, top, near, far)
|
|
392
|
+
{
|
|
393
|
+
// an infinite far plane has no orthographic form: every depth would land on the near plane,
|
|
394
|
+
// and the formula below works out to NaN, which quietly clips the whole scene away
|
|
395
|
+
ASSERT(far > near && far != Infinity, 'an orthographic projection needs a real far plane past near, Infinity is perspective only', near, far);
|
|
396
|
+
const r = new Matrix4;
|
|
397
|
+
const m = r.m;
|
|
398
|
+
m[0] = 2 / (right - left);
|
|
399
|
+
m[5] = 2 / (top - bottom);
|
|
400
|
+
m[10] = -2 / (far - near);
|
|
401
|
+
m[12] = -(right + left) / (right - left);
|
|
402
|
+
m[13] = -(top + bottom) / (top - bottom);
|
|
403
|
+
m[14] = -(far + near) / (far - near);
|
|
404
|
+
return r;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** Returns the transform of something at eye turned to face target
|
|
408
|
+
* - Invert it to get a view matrix for a camera there
|
|
409
|
+
* @param {Vector3} eye
|
|
410
|
+
* @param {Vector3} target
|
|
411
|
+
* @param {Vector3} [up]
|
|
412
|
+
* @return {Matrix4} */
|
|
413
|
+
static lookAt(eye, target, up=vec3(0, 1, 0))
|
|
414
|
+
{
|
|
415
|
+
let z = eye.subtract(target).normalize();
|
|
416
|
+
if (!z.lengthSquared())
|
|
417
|
+
z = vec3(0, 0, 1); // eye is on the target, face -Z
|
|
418
|
+
let x = up.cross(z).normalize();
|
|
419
|
+
if (!x.lengthSquared()) // up is along the view direction, pick another
|
|
420
|
+
x = (abs(z.y) > .99 ? vec3(0, 0, 1) : vec3(0, 1, 0)).cross(z).normalize();
|
|
421
|
+
const y = z.cross(x);
|
|
422
|
+
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]);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Returns a new matrix that is a copy of this
|
|
426
|
+
* @return {Matrix4} */
|
|
427
|
+
copy() { return new Matrix4(this.m); }
|
|
428
|
+
|
|
429
|
+
/** Multiply this matrix by another and return this, the other happens first
|
|
430
|
+
* @param {Matrix4} matrix
|
|
431
|
+
* @return {Matrix4} */
|
|
432
|
+
multiply(matrix)
|
|
433
|
+
{
|
|
434
|
+
const a = this.m, b = matrix.m, r = matrix4Scratch;
|
|
435
|
+
for (let j = 0; j < 4; ++j)
|
|
436
|
+
for (let i = 0; i < 4; ++i)
|
|
437
|
+
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];
|
|
438
|
+
this.m.set(r);
|
|
439
|
+
return this;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Append a translation, returns self
|
|
443
|
+
* @param {Vector3} v
|
|
444
|
+
* @return {Matrix4} */
|
|
445
|
+
translate(v) { return this.multiply(Matrix4.translation(v)); }
|
|
446
|
+
|
|
447
|
+
/** Append a rotation, returns self
|
|
448
|
+
* @param {Vector3} euler - vec3(pitch, yaw, roll) in radians
|
|
449
|
+
* @return {Matrix4} */
|
|
450
|
+
rotate(euler) { return this.multiply(Matrix4.rotation(euler)); }
|
|
451
|
+
|
|
452
|
+
/** Append a scale, returns self
|
|
453
|
+
* @param {Vector3} v
|
|
454
|
+
* @return {Matrix4} */
|
|
455
|
+
scale(v) { return this.multiply(Matrix4.scaling(v)); }
|
|
456
|
+
|
|
457
|
+
/** Transpose this matrix in place, returns self
|
|
458
|
+
* @return {Matrix4} */
|
|
459
|
+
transpose()
|
|
460
|
+
{
|
|
461
|
+
const m = this.m;
|
|
462
|
+
for (let i = 0; i < 4; ++i)
|
|
463
|
+
for (let j = i + 1; j < 4; ++j)
|
|
464
|
+
{
|
|
465
|
+
const t = m[i*4 + j];
|
|
466
|
+
m[i*4 + j] = m[j*4 + i];
|
|
467
|
+
m[j*4 + i] = t;
|
|
468
|
+
}
|
|
469
|
+
return this;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** Flip this matrix so it undoes itself, returns this and does nothing if it cannot be inverted
|
|
473
|
+
* @return {Matrix4} */
|
|
474
|
+
invert()
|
|
475
|
+
{
|
|
476
|
+
const m = this.m;
|
|
477
|
+
const [a00, a01, a02, a03, a10, a11, a12, a13, a20, a21, a22, a23, a30, a31, a32, a33] = m;
|
|
478
|
+
const b00 = a00*a11 - a01*a10, b01 = a00*a12 - a02*a10, b02 = a00*a13 - a03*a10;
|
|
479
|
+
const b03 = a01*a12 - a02*a11, b04 = a01*a13 - a03*a11, b05 = a02*a13 - a03*a12;
|
|
480
|
+
const b06 = a20*a31 - a21*a30, b07 = a20*a32 - a22*a30, b08 = a20*a33 - a23*a30;
|
|
481
|
+
const b09 = a21*a32 - a22*a31, b10 = a21*a33 - a23*a31, b11 = a22*a33 - a23*a32;
|
|
482
|
+
let det = b00*b11 - b01*b10 + b02*b09 + b03*b08 - b04*b07 + b05*b06;
|
|
483
|
+
if (!det)
|
|
484
|
+
return this;
|
|
485
|
+
det = 1 / det;
|
|
486
|
+
m[0] = (a11*b11 - a12*b10 + a13*b09) * det;
|
|
487
|
+
m[1] = (a02*b10 - a01*b11 - a03*b09) * det;
|
|
488
|
+
m[2] = (a31*b05 - a32*b04 + a33*b03) * det;
|
|
489
|
+
m[3] = (a22*b04 - a21*b05 - a23*b03) * det;
|
|
490
|
+
m[4] = (a12*b08 - a10*b11 - a13*b07) * det;
|
|
491
|
+
m[5] = (a00*b11 - a02*b08 + a03*b07) * det;
|
|
492
|
+
m[6] = (a32*b02 - a30*b05 - a33*b01) * det;
|
|
493
|
+
m[7] = (a20*b05 - a22*b02 + a23*b01) * det;
|
|
494
|
+
m[8] = (a10*b10 - a11*b08 + a13*b06) * det;
|
|
495
|
+
m[9] = (a01*b08 - a00*b10 - a03*b06) * det;
|
|
496
|
+
m[10] = (a30*b04 - a31*b02 + a33*b00) * det;
|
|
497
|
+
m[11] = (a21*b02 - a20*b04 - a23*b00) * det;
|
|
498
|
+
m[12] = (a11*b07 - a10*b09 - a12*b06) * det;
|
|
499
|
+
m[13] = (a00*b09 - a01*b07 + a02*b06) * det;
|
|
500
|
+
m[14] = (a31*b01 - a30*b03 - a32*b00) * det;
|
|
501
|
+
m[15] = (a20*b03 - a21*b01 + a22*b00) * det;
|
|
502
|
+
return this;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** Transform a point, translation included
|
|
506
|
+
* @param {Vector3} v
|
|
507
|
+
* @return {Vector3} */
|
|
508
|
+
transformPoint(v)
|
|
509
|
+
{
|
|
510
|
+
const m = this.m;
|
|
511
|
+
return new Vector3(
|
|
512
|
+
m[0]*v.x + m[4]*v.y + m[8]*v.z + m[12],
|
|
513
|
+
m[1]*v.x + m[5]*v.y + m[9]*v.z + m[13],
|
|
514
|
+
m[2]*v.x + m[6]*v.y + m[10]*v.z + m[14]);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** Transform a direction, rotation and scale only
|
|
518
|
+
* @param {Vector3} v
|
|
519
|
+
* @return {Vector3} */
|
|
520
|
+
transformDirection(v)
|
|
521
|
+
{
|
|
522
|
+
const m = this.m;
|
|
523
|
+
return new Vector3(
|
|
524
|
+
m[0]*v.x + m[4]*v.y + m[8]*v.z,
|
|
525
|
+
m[1]*v.x + m[5]*v.y + m[9]*v.z,
|
|
526
|
+
m[2]*v.x + m[6]*v.y + m[10]*v.z);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** Returns the translation part of this matrix
|
|
530
|
+
* @return {Vector3} */
|
|
531
|
+
getTranslation() { return new Vector3(this.m[12], this.m[13], this.m[14]); }
|
|
532
|
+
|
|
533
|
+
/** Returns a string representation of this matrix for debugging
|
|
534
|
+
* @return {string} */
|
|
535
|
+
toString()
|
|
536
|
+
{
|
|
537
|
+
const m = this.m, f = (i)=> m[i].toFixed(2).padStart(7);
|
|
538
|
+
let s = '';
|
|
539
|
+
for (let row = 0; row < 4; ++row)
|
|
540
|
+
s += `[${f(row)} ${f(4 + row)} ${f(8 + row)} ${f(12 + row)} ]\n`;
|
|
541
|
+
return s;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Build a transform for an object from its position, rotation and scale
|
|
549
|
+
* - A point is scaled first, then rotated, then moved, which is what you want for a game object
|
|
550
|
+
* @param {Vector3} [pos]
|
|
551
|
+
* @param {Vector3} [rotation] - vec3(pitch, yaw, roll) in radians
|
|
552
|
+
* @param {Vector3} [scale]
|
|
553
|
+
* @return {Matrix4}
|
|
554
|
+
* @memberof Math3D
|
|
555
|
+
*/
|
|
556
|
+
function buildMatrix(pos, rotation, scale)
|
|
557
|
+
{
|
|
558
|
+
ASSERT(!pos || isVector3(pos), 'pos must be a Vector3', pos);
|
|
559
|
+
ASSERT(!scale || isVector3(scale), 'scale must be a Vector3', scale);
|
|
560
|
+
// scale the rotation columns and drop the position in, instead of multiplying three matrices
|
|
561
|
+
// an object that is not turned at all is most of a big scene, and identity is what the six
|
|
562
|
+
// trig calls would have worked out to anyway
|
|
563
|
+
const turned = rotation && (rotation.x || rotation.y || rotation.z);
|
|
564
|
+
const matrix = turned ? Matrix4.rotation(rotation) : new Matrix4, m = matrix.m;
|
|
565
|
+
if (scale)
|
|
566
|
+
{
|
|
567
|
+
m[0] *= scale.x; m[1] *= scale.x; m[2] *= scale.x;
|
|
568
|
+
m[4] *= scale.y; m[5] *= scale.y; m[6] *= scale.y;
|
|
569
|
+
m[8] *= scale.z; m[9] *= scale.z; m[10] *= scale.z;
|
|
570
|
+
}
|
|
571
|
+
if (pos)
|
|
572
|
+
m[12] = pos.x, m[13] = pos.y, m[14] = pos.z;
|
|
573
|
+
return matrix;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
577
|
+
/**
|
|
578
|
+
* Ray3D - A start point and a direction, what screenToRay returns and the raycast helpers take
|
|
579
|
+
* - The direction need not be unit length, the distances that come back are in units of it
|
|
580
|
+
* @memberof Math3D
|
|
581
|
+
* @example
|
|
582
|
+
* const ray = render3D.screenToRay(mousePosScreen);
|
|
583
|
+
* const distance = raycastPlane(ray, vec3(), vec3(0, 1, 0));
|
|
584
|
+
* if (distance !== undefined)
|
|
585
|
+
* ball.pos3D = ray.getPosition(distance);
|
|
586
|
+
*/
|
|
587
|
+
class Ray3D
|
|
588
|
+
{
|
|
589
|
+
/** Create a ray
|
|
590
|
+
* @param {Vector3} [origin]
|
|
591
|
+
* @param {Vector3} [direction] - Defaults to -Z, forward */
|
|
592
|
+
constructor(origin=vec3(), direction=vec3(0, 0, -1))
|
|
593
|
+
{
|
|
594
|
+
ASSERT_VECTOR3_VALID(origin);
|
|
595
|
+
ASSERT_VECTOR3_VALID(direction);
|
|
596
|
+
/** @property {Vector3} - Where the ray starts */
|
|
597
|
+
this.origin = origin;
|
|
598
|
+
/** @property {Vector3} - Which way it goes */
|
|
599
|
+
this.direction = direction;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** Returns the point a distance along the ray
|
|
603
|
+
* @param {number} distance - What the raycast helpers return
|
|
604
|
+
* @return {Vector3} */
|
|
605
|
+
getPosition(distance) { return this.origin.add(this.direction.scale(distance)); }
|
|
606
|
+
|
|
607
|
+
/** Returns a new ray that is a copy of this
|
|
608
|
+
* @return {Ray3D} */
|
|
609
|
+
copy() { return new Ray3D(this.origin.copy(), this.direction.copy()); }
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
613
|
+
// 3D collision helpers, none of them change anything that is passed in
|
|
614
|
+
// Boxes sit centered on pos and take a full size, like drawRect
|
|
615
|
+
// Cylinders stand up the Y axis, centered on pos, with a full height
|
|
616
|
+
// Names that could be mistaken for 2D functions get a 3D suffix
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Check if a point is inside an axis aligned box, boundary is inclusive
|
|
620
|
+
* @param {Vector3} point
|
|
621
|
+
* @param {Vector3} pos - Center of the box
|
|
622
|
+
* @param {Vector3} size - Full size of the box
|
|
623
|
+
* @return {boolean}
|
|
624
|
+
* @memberof Math3D
|
|
625
|
+
*/
|
|
626
|
+
function isPointInBox3D(point, pos, size)
|
|
627
|
+
{
|
|
628
|
+
return abs(point.x - pos.x) <= size.x/2 &&
|
|
629
|
+
abs(point.y - pos.y) <= size.y/2 &&
|
|
630
|
+
abs(point.z - pos.z) <= size.z/2;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Check if two axis aligned boxes are overlapping, touching edges do not overlap
|
|
635
|
+
* @param {Vector3} posA
|
|
636
|
+
* @param {Vector3} sizeA - Full size of box A
|
|
637
|
+
* @param {Vector3} posB
|
|
638
|
+
* @param {Vector3} [sizeB] - Full size of box B, zero for a point
|
|
639
|
+
* @return {boolean}
|
|
640
|
+
* @memberof Math3D
|
|
641
|
+
*/
|
|
642
|
+
function isOverlapping3D(posA, sizeA, posB, sizeB=vec3())
|
|
643
|
+
{
|
|
644
|
+
const d = posA.subtract(posB);
|
|
645
|
+
return abs(d.x) < (sizeA.x + sizeB.x)/2 &&
|
|
646
|
+
abs(d.y) < (sizeA.y + sizeB.y)/2 &&
|
|
647
|
+
abs(d.z) < (sizeA.z + sizeB.z)/2;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Returns the vector to move sphere A by so it no longer overlaps sphere B, or undefined
|
|
652
|
+
* @param {Vector3} posA
|
|
653
|
+
* @param {number} radiusA
|
|
654
|
+
* @param {Vector3} posB
|
|
655
|
+
* @param {number} radiusB
|
|
656
|
+
* @return {Vector3|undefined}
|
|
657
|
+
* @memberof Math3D
|
|
658
|
+
*/
|
|
659
|
+
function collideSphereSphere(posA, radiusA, posB, radiusB)
|
|
660
|
+
{
|
|
661
|
+
const d = posA.subtract(posB);
|
|
662
|
+
const r = radiusA + radiusB;
|
|
663
|
+
const dist = d.length();
|
|
664
|
+
if (dist >= r)
|
|
665
|
+
return undefined;
|
|
666
|
+
if (!dist)
|
|
667
|
+
return vec3(0, r, 0); // coincident centers, push straight up
|
|
668
|
+
return d.normalize(r - dist);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Returns the vector to move a sphere out of an axis aligned box, or undefined
|
|
673
|
+
* @param {Vector3} pos - Sphere center
|
|
674
|
+
* @param {number} radius
|
|
675
|
+
* @param {Vector3} boxPos
|
|
676
|
+
* @param {Vector3} boxSize - Full size of the box
|
|
677
|
+
* @return {Vector3|undefined}
|
|
678
|
+
* @memberof Math3D
|
|
679
|
+
*/
|
|
680
|
+
function collideSphereBox(pos, radius, boxPos, boxSize)
|
|
681
|
+
{
|
|
682
|
+
const h = boxSize.scale(.5);
|
|
683
|
+
const closest = vec3(
|
|
684
|
+
clamp(pos.x, boxPos.x - h.x, boxPos.x + h.x),
|
|
685
|
+
clamp(pos.y, boxPos.y - h.y, boxPos.y + h.y),
|
|
686
|
+
clamp(pos.z, boxPos.z - h.z, boxPos.z + h.z));
|
|
687
|
+
const d = pos.subtract(closest), distSq = d.lengthSquared();
|
|
688
|
+
if (distSq)
|
|
689
|
+
return distSq >= radius*radius ? undefined : d.normalize(radius - distSq**.5);
|
|
690
|
+
|
|
691
|
+
// center is inside the box, push out along the axis of least penetration
|
|
692
|
+
const offset = pos.subtract(boxPos);
|
|
693
|
+
return pushOutAxis3D(offset, h.x - abs(offset.x), h.y - abs(offset.y), h.z - abs(offset.z), radius);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Returns the vector to move a sphere back inside an axis aligned box, or undefined when it is all inside
|
|
698
|
+
* - The inside out twin of collideSphereBox, for keeping things in a room or an arena
|
|
699
|
+
* - A sphere too big for the box on some axis is held at the middle of it on that axis
|
|
700
|
+
* @param {Vector3} pos - Sphere center
|
|
701
|
+
* @param {number} radius
|
|
702
|
+
* @param {Vector3} boxPos
|
|
703
|
+
* @param {Vector3} boxSize - Full size of the box
|
|
704
|
+
* @return {Vector3|undefined}
|
|
705
|
+
* @memberof Math3D
|
|
706
|
+
*/
|
|
707
|
+
function collideSphereInBox(pos, radius, boxPos, boxSize)
|
|
708
|
+
{
|
|
709
|
+
// the box shrunk by the radius is everywhere the center can be, so clamp the center into it
|
|
710
|
+
const x = max(0, boxSize.x/2 - radius), y = max(0, boxSize.y/2 - radius), z = max(0, boxSize.z/2 - radius);
|
|
711
|
+
const push = vec3(
|
|
712
|
+
clamp(pos.x, boxPos.x - x, boxPos.x + x) - pos.x,
|
|
713
|
+
clamp(pos.y, boxPos.y - y, boxPos.y + y) - pos.y,
|
|
714
|
+
clamp(pos.z, boxPos.z - z, boxPos.z + z) - pos.z);
|
|
715
|
+
return push.lengthSquared() ? push : undefined;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// the axis with the smallest penetration, pointing the way d does, with extra distance added
|
|
719
|
+
function pushOutAxis3D(d, penX, penY, penZ, extra=0)
|
|
720
|
+
{
|
|
721
|
+
const s = (v)=> v >= 0 ? 1 : -1; // sign() gives 0 on a tie, which would be no push
|
|
722
|
+
if (penX <= penY && penX <= penZ)
|
|
723
|
+
return vec3(s(d.x)*(penX + extra), 0, 0);
|
|
724
|
+
if (penY <= penZ)
|
|
725
|
+
return vec3(0, s(d.y)*(penY + extra), 0);
|
|
726
|
+
return vec3(0, 0, s(d.z)*(penZ + extra));
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Returns the vector to move a sphere out of a vertical cylinder, or undefined
|
|
731
|
+
* @param {Vector3} pos - Sphere center
|
|
732
|
+
* @param {number} radius
|
|
733
|
+
* @param {Vector3} cylinderPos
|
|
734
|
+
* @param {number} cylinderRadius
|
|
735
|
+
* @param {number} cylinderHeight - Full height along Y
|
|
736
|
+
* @return {Vector3|undefined}
|
|
737
|
+
* @memberof Math3D
|
|
738
|
+
*/
|
|
739
|
+
function collideSphereCylinder(pos, radius, cylinderPos, cylinderRadius, cylinderHeight)
|
|
740
|
+
{
|
|
741
|
+
const halfHeight = cylinderHeight/2;
|
|
742
|
+
const offsetX = pos.x - cylinderPos.x;
|
|
743
|
+
const offsetZ = pos.z - cylinderPos.z;
|
|
744
|
+
const offsetY = pos.y - cylinderPos.y;
|
|
745
|
+
const radialDist = (offsetX**2 + offsetZ**2)**.5;
|
|
746
|
+
const radialScale = radialDist ? min(radialDist, cylinderRadius)/radialDist : 0; // pull the point onto the wall, or the axis
|
|
747
|
+
const closest = vec3(
|
|
748
|
+
cylinderPos.x + offsetX*radialScale,
|
|
749
|
+
clamp(pos.y, cylinderPos.y - halfHeight, cylinderPos.y + halfHeight),
|
|
750
|
+
cylinderPos.z + offsetZ*radialScale);
|
|
751
|
+
const d = pos.subtract(closest), distSq = d.lengthSquared();
|
|
752
|
+
if (distSq)
|
|
753
|
+
return distSq >= radius*radius ? undefined : d.normalize(radius - distSq**.5);
|
|
754
|
+
|
|
755
|
+
// center is inside the cylinder, push out through the nearer surface
|
|
756
|
+
const sidePen = cylinderRadius - radialDist;
|
|
757
|
+
const capPen = halfHeight - abs(offsetY);
|
|
758
|
+
if (sidePen <= capPen)
|
|
759
|
+
{
|
|
760
|
+
const dir = radialDist ? vec3(offsetX/radialDist, 0, offsetZ/radialDist) : vec3(1, 0, 0);
|
|
761
|
+
return dir.scale(sidePen + radius);
|
|
762
|
+
}
|
|
763
|
+
return vec3(0, (offsetY >= 0 ? 1 : -1)*(capPen + radius), 0);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Returns the vector to move box A by so it no longer overlaps box B, the shortest way out, or undefined
|
|
768
|
+
* - The 3D twin of collideBoxBox
|
|
769
|
+
* @param {Vector3} posA
|
|
770
|
+
* @param {Vector3} sizeA - Full size of box A
|
|
771
|
+
* @param {Vector3} posB
|
|
772
|
+
* @param {Vector3} sizeB - Full size of box B
|
|
773
|
+
* @return {Vector3|undefined}
|
|
774
|
+
* @memberof Math3D
|
|
775
|
+
*/
|
|
776
|
+
function collideBoxBox3D(posA, sizeA, posB, sizeB)
|
|
777
|
+
{
|
|
778
|
+
const d = posA.subtract(posB);
|
|
779
|
+
const overlapX = (sizeA.x + sizeB.x)/2 - abs(d.x);
|
|
780
|
+
const overlapY = (sizeA.y + sizeB.y)/2 - abs(d.y);
|
|
781
|
+
const overlapZ = (sizeA.z + sizeB.z)/2 - abs(d.z);
|
|
782
|
+
if (overlapX <= 0 || overlapY <= 0 || overlapZ <= 0)
|
|
783
|
+
return undefined;
|
|
784
|
+
return pushOutAxis3D(d, overlapX, overlapY, overlapZ);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Returns the distance along the ray to the first intersection with a sphere, or undefined
|
|
789
|
+
* - The hit is ray.getPosition(distance), a direction that is not unit length scales the distance
|
|
790
|
+
* - A ray starting inside the sphere is already there, so it gets back 0
|
|
791
|
+
* @param {Ray3D} ray
|
|
792
|
+
* @param {Vector3} pos - Sphere center
|
|
793
|
+
* @param {number} radius
|
|
794
|
+
* @return {number|undefined}
|
|
795
|
+
* @memberof Math3D
|
|
796
|
+
*/
|
|
797
|
+
function raycastSphere(ray, pos, radius)
|
|
798
|
+
{
|
|
799
|
+
const {origin, direction} = ray;
|
|
800
|
+
const oc = origin.subtract(pos);
|
|
801
|
+
const a = direction.dot(direction);
|
|
802
|
+
if (!a)
|
|
803
|
+
return undefined;
|
|
804
|
+
const c = oc.dot(oc) - radius*radius;
|
|
805
|
+
if (c < 0)
|
|
806
|
+
return 0; // origin is inside the sphere
|
|
807
|
+
const b = 2*oc.dot(direction);
|
|
808
|
+
const discriminant = b*b - 4*a*c;
|
|
809
|
+
if (discriminant < 0)
|
|
810
|
+
return undefined;
|
|
811
|
+
const t = (-b - discriminant**.5)/(2*a);
|
|
812
|
+
return t >= 0 ? t : undefined;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Returns the distance along the ray to a plane, or undefined if parallel or behind
|
|
817
|
+
* - The hit is ray.getPosition(distance), a direction that is not unit length scales the distance
|
|
818
|
+
* @param {Ray3D} ray
|
|
819
|
+
* @param {Vector3} planePos
|
|
820
|
+
* @param {Vector3} planeNormal
|
|
821
|
+
* @return {number|undefined}
|
|
822
|
+
* @memberof Math3D
|
|
823
|
+
*/
|
|
824
|
+
function raycastPlane(ray, planePos, planeNormal)
|
|
825
|
+
{
|
|
826
|
+
const {origin, direction} = ray;
|
|
827
|
+
const denominator = direction.dot(planeNormal);
|
|
828
|
+
if (abs(denominator) < 1e-9)
|
|
829
|
+
return undefined;
|
|
830
|
+
const t = planePos.subtract(origin).dot(planeNormal)/denominator;
|
|
831
|
+
return t < 0 ? undefined : t;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Returns the distance along the ray to the first intersection with an axis aligned box, or undefined
|
|
836
|
+
* - The hit is ray.getPosition(distance), a direction that is not unit length scales the distance
|
|
837
|
+
* - A ray starting inside the box is already there, so it gets back 0
|
|
838
|
+
* @param {Ray3D} ray
|
|
839
|
+
* @param {Vector3} pos - Center of the box
|
|
840
|
+
* @param {Vector3} size - Full size of the box
|
|
841
|
+
* @return {number|undefined}
|
|
842
|
+
* @memberof Math3D
|
|
843
|
+
*/
|
|
844
|
+
function raycastBox(ray, pos, size)
|
|
845
|
+
{
|
|
846
|
+
const {origin, direction} = ray;
|
|
847
|
+
const h = size.scale(.5);
|
|
848
|
+
const boxMin = pos.subtract(h), boxMax = pos.add(h);
|
|
849
|
+
let tMin = 0, tMax = Infinity;
|
|
850
|
+
for (const axis of 'xyz')
|
|
851
|
+
{
|
|
852
|
+
const o = origin[axis], d = direction[axis];
|
|
853
|
+
const mn = boxMin[axis], mx = boxMax[axis];
|
|
854
|
+
if (!d)
|
|
855
|
+
{
|
|
856
|
+
if (o < mn || o > mx)
|
|
857
|
+
return undefined; // ray is parallel to this slab and outside it
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
let t0 = (mn - o)/d;
|
|
861
|
+
let t1 = (mx - o)/d;
|
|
862
|
+
if (t0 > t1)
|
|
863
|
+
[t0, t1] = [t1, t0];
|
|
864
|
+
tMin = max(tMin, t0);
|
|
865
|
+
tMax = min(tMax, t1);
|
|
866
|
+
if (tMin > tMax)
|
|
867
|
+
return undefined;
|
|
868
|
+
}
|
|
869
|
+
return tMin;
|
|
870
|
+
}
|