littlejsengine 1.24.3 → 1.26.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 (53) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/EDITOR.md +2 -2
  3. package/FAQ.md +55 -11
  4. package/README.md +6 -2
  5. package/REFERENCE.md +203 -57
  6. package/dist/littlejs.d.ts +933 -313
  7. package/dist/littlejs.esm.js +5143 -2419
  8. package/dist/littlejs.esm.min.js +38 -24
  9. package/dist/littlejs.js +5132 -2416
  10. package/dist/littlejs.min.js +37 -23
  11. package/dist/littlejs.release.js +4905 -2232
  12. package/package.json +4 -4
  13. package/plugins/box2d.js +141 -56
  14. package/plugins/drawUtilities.js +374 -380
  15. package/plugins/gltf.js +383 -60
  16. package/plugins/lightSystem.js +412 -30
  17. package/plugins/math3d.js +1015 -1014
  18. package/plugins/medalSystem.js +19 -7
  19. package/plugins/newgrounds.js +5 -6
  20. package/plugins/parallax.js +1 -0
  21. package/plugins/particleEffects.js +47 -14
  22. package/plugins/pathFinder.js +60 -38
  23. package/plugins/pluginExport.js +11 -0
  24. package/plugins/postProcess.js +200 -67
  25. package/plugins/render3d.js +493 -2391
  26. package/plugins/render3dDebug.js +5 -8
  27. package/plugins/render3dEditor.js +84 -9
  28. package/plugins/render3dExtras.js +122 -33
  29. package/plugins/render3dLevel.js +67 -8
  30. package/plugins/render3dMesh.js +1158 -0
  31. package/plugins/render3dObject.js +1451 -0
  32. package/plugins/render3dVoxels.js +15 -3
  33. package/plugins/textureSheet.js +9 -8
  34. package/plugins/threejs.js +1 -1
  35. package/plugins/tweenSystem.js +117 -87
  36. package/plugins/uiSystem.js +62 -28
  37. package/plugins/wavedash.js +229 -0
  38. package/src/engine.js +192 -40
  39. package/src/engineAudio.js +47 -27
  40. package/src/engineBuild.mjs +6 -1
  41. package/src/engineDebug.js +23 -162
  42. package/src/engineDraw.js +148 -43
  43. package/src/engineEditor.js +153 -40
  44. package/src/engineExport.js +0 -3
  45. package/src/engineInput.js +31 -25
  46. package/src/engineMath.js +38 -20
  47. package/src/engineObject.js +68 -50
  48. package/src/engineParticles.js +66 -18
  49. package/src/engineRelease.js +18 -15
  50. package/src/engineSettings.js +48 -9
  51. package/src/engineTileLayer.js +44 -18
  52. package/src/engineUtilities.js +56 -11
  53. package/src/engineWebGL.js +68 -22
@@ -0,0 +1,1451 @@
1
+ /*
2
+ * LittleJS 3D Object Plugin
3
+ * - EngineObject3D, an EngineObject with a 3D transform and a mesh, its collision and its level collision
4
+ * - InstancedMesh3D, one mesh drawn many times in one call, and Light3D and DirectionalLight3D
5
+ * - Goes after the Render3D Mesh plugin, everything here is part of the Render3D namespace
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ ///////////////////////////////////////////////////////////////////////////////
11
+
12
+ /**
13
+ * EngineObject3D - An EngineObject with a 3D transform and a mesh
14
+ * - Set pos3D, rotation3D and scale3D instead of the 2D pos, size and angle
15
+ * - Gets update, children, timers, destroy and renderOrder from EngineObject
16
+ * - velocity3D is added to pos3D each frame, slowed by damping, and render3D.gravity pulls it once it has a mass
17
+ * - Objects face -Z, the same way the camera does, so lookAt turns them to face a point
18
+ * - The 2D pos and velocity are still there but nothing draws them
19
+ * - These inherited fields are 2D only and do nothing here: angle, angleVelocity, additiveColor, drawSize and mirror;
20
+ * damping, angleDamping, clampSpeed, friction and groundObject work as in 2D, on velocity3D and angleVelocity3D
21
+ * - The inherited shader works here as in 2D, and with emissive at 1 its snippet does its own lighting
22
+ * - Set sync2D for a 2D game with 3D looks, pos and angle then drive pos3D and rotation3D,
23
+ * which is the one way those 2D fields reach a 3D object
24
+ * - setCollision takes the same flags as in 2D, but the solid collision happens in 3D against size3D
25
+ * - The solid box turns with rotation3D, so a turned wall or a ramp collides as it looks; resting on one no steeper
26
+ * than groundAngle stands there
27
+ * - Its tile and raycast halves are 2D only so they default off here, and a child sits solid collision out
28
+ * - A sync2D object collides in 2D instead, which needs the 2D size set as well as size3D
29
+ * - setMesh swaps the mesh and frees the old one, for text and terrain that get built again
30
+ * - addChild attaches the 3D transform, and pos3D becomes an offset from the parent; attach keeps the child where
31
+ * it is and works the offset out, and removeChild leaves it where it was in the world
32
+ * - The 2D offset arguments of addChild do nothing here, set the child's pos3D
33
+ * @extends EngineObject
34
+ * @memberof Render3D
35
+ * @example
36
+ * class Spinner extends EngineObject3D
37
+ * {
38
+ * constructor(pos) { super(pos, buildBox(), undefined, RED); }
39
+ * update() { this.rotation3D.y += .02; }
40
+ * }
41
+ */
42
+ class EngineObject3D extends EngineObject
43
+ {
44
+ /** Create a 3D object and add it to the object list
45
+ * @param {Vector3} [pos3D] - World space position
46
+ * @param {Mesh} [mesh] - Mesh to draw, undefined draws nothing
47
+ * @param {TileInfo|TextureInfo} [tileInfo] - Texture, mesh uvs map across the tile; a whole TextureInfo becomes
48
+ * the tile that covers it
49
+ * @param {Color} [color] - Tint */
50
+ constructor(pos3D=vec3(), mesh, tileInfo, color=WHITE)
51
+ {
52
+ ASSERT(!tileInfo || tileInfo instanceof TileInfo || tileInfo instanceof TextureInfo,
53
+ 'tileInfo must be a TileInfo or TextureInfo, it comes before color');
54
+ // a whole texture is stored as the tile that covers it, with no padding or bleed to trim
55
+ // the edges, so this is always a TileInfo like the 2D one and the object stays an EngineObject
56
+ if (tileInfo instanceof TextureInfo)
57
+ render3DWholeTiles.add(tileInfo = new TileInfo(vec2(), tileInfo.size, tileInfo, 0, 0));
58
+ // the 2D pos starts where the object is, so turning on sync2D keeps it there
59
+ super(vec2(pos3D.x, pos3D.y), vec2(), tileInfo, 0, color);
60
+ ASSERT(isVector3(pos3D), 'pos3D must be a vec3');
61
+ ASSERT(!mesh || mesh instanceof Mesh, 'mesh must be a Mesh or undefined');
62
+ this.mass = 0; // static: no 2D physics, and no 3D gravity until a mass is set
63
+
64
+ /** @property {Vector3} - World space position, local to the parent when attached to an EngineObject3D */
65
+ this.pos3D = pos3D.copy();
66
+ /** @property {Vector3} - Rotation vec3(pitch, yaw, roll) in radians, local to the parent when attached to an
67
+ * EngineObject3D */
68
+ this.rotation3D = vec3();
69
+ /** @property {Vector3} - Scale, local to the parent when attached to an EngineObject3D */
70
+ this.scale3D = vec3(1);
71
+ /** @property {Vector3} - Added to pos3D each frame by the engine before update, like the 2D velocity, no super
72
+ * call needed; damping and render3D.gravity act on it once the object has a mass */
73
+ this.velocity3D = vec3();
74
+ /** @property {Vector3} - Added to rotation3D each frame by the engine before update, slowed by angleDamping */
75
+ this.angleVelocity3D = vec3();
76
+ /** @property {Mesh|undefined} - Mesh to draw
77
+ * @type {Mesh|undefined} */
78
+ this.mesh = mesh;
79
+ /** @property {Vector3} - Size for solid collision and the collect and callback helpers, and of the sprite when
80
+ * there is a tileInfo and no mesh; starts at the size of the mesh's box, or 1 with no mesh, and setMesh leaves
81
+ * it as it is; the box is centered on pos3D, so center() a mesh whose origin is not its middle, like a model
82
+ * standing on its feet, or set size3D; scale3D and any parent's scale grow it, so drawing and picking agree */
83
+ // a shared mesh measured since it last changed is not walked again for each object made from it
84
+ const bounds = mesh && mesh.points.length ? !mesh.dirty && mesh.bounds || mesh.getBounds() : undefined;
85
+ this.size3D = bounds ? bounds.max.subtract(bounds.min) : vec3(1);
86
+ /** @property {number} - Diameter of a soft shadow drawn under the object on render3D.softShadowHeight, 0 for
87
+ * none; scale3D and a parent's scale grow it, so set it once for the unscaled object */
88
+ this.softShadow = 0;
89
+ /** @property {boolean} - A sprite stands on world up instead of tilting toward the camera */
90
+ this.upright = false;
91
+ /** @property {boolean} - Keep this object's texture pixels hard edged, for pixel art that should not blur or bleed */
92
+ this.pixelated = false;
93
+ /** @property {boolean} - Copy the 2D pos and angle into pos3D and rotation3D each frame, for 2D games with 3D
94
+ * looks; set mass to use 2D physics, and pos3D.z stays yours to set or move with velocity3D.z */
95
+ this.sync2D = false;
96
+ /** @property {boolean} - Draw in the transparent stage, blended and sorted far to near with depth writes off;
97
+ * on for a sprite */
98
+ this.transparent = !mesh && !!tileInfo;
99
+ /** @property {boolean} - Additive blending, in the transparent stage */
100
+ this.additive = false;
101
+ /** @property {number} - How much it lights itself: 0 is lit as normal, 1 is its own color with no shading, for
102
+ * lamps and glowing things, between is partly self lit, and above 1 is brighter than its color, for bloom */
103
+ this.emissive = 0;
104
+ /** @property {number} - Strength of the highlight where the sun and the Light3D objects reflect, 0 is none and
105
+ * 1 adds a light's full color at its brightest; shininess sets its size */
106
+ this.specular = 0;
107
+ /** @property {number} - The highlight's exponent, how small and sharp it is: 4 is broad like rubber, 16 the
108
+ * default, 100 sharp like polished metal; shows only with specular above 0 */
109
+ this.shininess = 16;
110
+ /** @property {TextureInfo|undefined} - A normal map that bends the surface at each texel so it catches the
111
+ * light like bumps and grooves, green pointing up the image as OpenGL and glTF have it; read at the color
112
+ * texture's coordinates, see normalMapFromHeight to make one in code
113
+ * @type {TextureInfo|undefined} */
114
+ this.normalMap = undefined;
115
+ /** @property {number} - How strongly the normal map bends the surface, 0 turns it off, as glTF's scale */
116
+ this.normalScale = 1;
117
+ /** @property {number} - How much it reflects the sky, 0 none and 1 a mirror of it; the edges seen at a
118
+ * glancing angle reflect more either way, as water and glass do */
119
+ this.reflectivity = 0;
120
+ /** @property {TextureInfo|undefined} - A texture of where it glows, added on top of the lit surface so it
121
+ * shows in the dark, like lit windows; read at the color texture's coordinates
122
+ * @type {TextureInfo|undefined} */
123
+ this.emissiveMap = undefined;
124
+ /** @property {Color} - Multiplies the emissive map, as glTF's emissiveFactor */
125
+ this.emissiveMapColor = WHITE;
126
+ /** @property {CubeMap|undefined} - What it reflects in place of render3D.environment, as a mirror captures
127
+ * the scene from its own middle
128
+ * @type {CubeMap|undefined} */
129
+ this.environment = undefined;
130
+ /** @property {boolean} - Draw into the shadow map when render3D.shadows is on; sprites and cut out textures
131
+ * cast their outline, an object faded below half its alpha casts nothing, a see through one casts only when
132
+ * textured, and additive objects never cast */
133
+ this.castShadow = true;
134
+ /** @property {boolean} - Collide as the sphere that fits size3D instead of as the size3D box, so it rolls
135
+ * around corners */
136
+ this.collideAsSphere3D = false;
137
+ /** @property {number} - The steepest slope it stands on, in radians from level, PI/4 by default: resting on
138
+ * a solid within this of flat sets groundObject and holds it still, steeper it slides down */
139
+ this.groundAngle = PI / 4;
140
+ /** @property {boolean} - Darkened by the shadow map when render3D.shadows is on */
141
+ this.receiveShadow = true;
142
+ /** @property {boolean|undefined} - Draw this object over the 2D scene, undefined uses render3D.renderAfter2D
143
+ * @type {boolean|undefined} */
144
+ this.renderAfter2D = undefined;
145
+ /** @property {Matrix4|undefined} - The transform from its parent, used in place of pos3D, rotation3D and
146
+ * scale3D when set, for one they cannot hold like a glTF pose with shear; read every frame it is set
147
+ * @type {Matrix4|undefined} */
148
+ this.localMatrix = undefined;
149
+ // the world transform, kept up to date by render3DObjectMatrix; getMatrix returns a copy
150
+ this.worldMatrix = new Matrix4;
151
+ this.matrixBuilt = new Float64Array(9).fill(NaN); // the position, rotation and scale it was built from
152
+ this.matrixVersion = 0; // counts the rebuilds, so a child knows when its parent's changed
153
+ /** @type {EngineObject3D|undefined} */
154
+ this.matrixParent = undefined; // the parent it was built under, and that parent's version then
155
+ this.matrixParentVersion = 0;
156
+ this.movePass = engineObjectsUpdateCount; // the engine pass a child last moved in, so a refresh is not a step
157
+ this.collideFrom = vec3(); // where it was when the pass it collided in began, see render3DCollideFrom
158
+ this.collideFromPass = -1;
159
+ /** @type {EngineObject3D|undefined} */
160
+ this.drawOwner = undefined; // the object this one is drawn as a part of, see render3DShareSettings
161
+ }
162
+
163
+ /** Move by the 3D velocities and push out of solids, called automatically each frame before update, like the 2D physics
164
+ * - update runs once every object has moved and collided, so bounce off anything else there, it lands before the draw
165
+ * - Override this and call super to change how the object moves itself
166
+ * - A sync2D object runs the 2D physics as well, and collides there instead */
167
+ updatePhysics()
168
+ {
169
+ // a sync2D object collides in 2D, which measures the 2D size, and that starts at zero on a 3D object
170
+ ASSERT(!this.sync2D || !this.collideSolidObjects || (this.size.x && this.size.y),
171
+ 'a sync2D object collides in 2D, so give it a 2D size as well as a size3D', this.size);
172
+ if (this.sync2D)
173
+ super.updatePhysics();
174
+ ASSERT(this.damping >= 0 && this.damping <= 1, 'damping must be 0 to 1, the fraction of velocity kept each frame',
175
+ this.damping);
176
+ ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1,
177
+ 'angleDamping must be 0 to 1, the fraction of angular velocity kept each frame', this.angleDamping);
178
+ ASSERT(isNumber(this.groundAngle) && this.groundAngle >= 0 && this.groundAngle < PI / 2,
179
+ 'groundAngle must be 0 to less than PI/2, a slope from level', this.groundAngle);
180
+ // what it stands on is found again each frame, by the level and by the solids it rests on; a sync2D
181
+ // object's is the 2D physics'
182
+ const ground = this.groundObject;
183
+ this.sync2D || (this.groundObject = undefined);
184
+ // each axis within objectMaxSpeed, as in 2D, after gravity so no move is past it; with the push back to the
185
+ // side it came from, a move that overlaps a solid at all is stopped on its side, so a fast object goes through
186
+ // only one thinner than its move less its own size, as in 2D, while neither is turned; only for what collides,
187
+ // anything else moves as fast as it is told
188
+ // nothing of collision while setEnablePhysicsSolver is off, as in 2D, the game moves them its own way
189
+ const solve = enablePhysicsSolver && !this.sync2D;
190
+ const cap = this.clampSpeed && solve && (this.collideSolidObjects || this.collideLevel && this.mass) ?
191
+ objectMaxSpeed : undefined; // 0 holds it still, as in 2D
192
+ // a moving object keeps out of the level, the height maps and voxel maps, from where it was before it moved,
193
+ // and a solid it hits sends it back to the side it came from
194
+ const oldPos = (this.collideLevel || this.collideSolidObjects) && this.mass && !this.sync2D ?
195
+ this.pos3D.copy() : undefined;
196
+ this.collideSolidObjects && !this.sync2D && render3DCollideFrom(this);
197
+ render3DMove(this, cap);
198
+ if (ground && this.mass && !this.sync2D)
199
+ {
200
+ // sliding on what it stood on slows by friction, the less grippy of the two, relative to that one's own
201
+ // speed so a moving platform carries it, as in 2D
202
+ const friction = max(this.friction, ground.friction), v = this.velocity3D;
203
+ const moving = ground instanceof EngineObject3D ? ground.velocity3D : undefined;
204
+ const gx = moving?.x ?? 0, gz = moving?.z ?? 0;
205
+ v.x = gx + (v.x - gx) * friction, v.z = gz + (v.z - gz) * friction;
206
+ }
207
+ oldPos && this.collideLevel && solve && render3DCollideLevel(this, oldPos, ground);
208
+ // the engine only runs this for objects that own where they are, a child rides along with its parent
209
+ if (this.collideSolidObjects && solve)
210
+ render3DCollideSolid(this);
211
+ }
212
+
213
+ /** Move a child by its own velocities, bring a sync2D object's pos3D up to its 2D pos, then update the children,
214
+ * called automatically each frame
215
+ * @param {boolean} [updateChildren] - Also update the children's transforms */
216
+ updateTransforms(updateChildren=true)
217
+ {
218
+ if (!paused)
219
+ {
220
+ // a child is never given updatePhysics, so it moves here, as an offset from its parent: once per
221
+ // engine pass, since addChild, attach and a game bring the transforms up to date too
222
+ if (this.parent && this.movePass !== engineObjectsUpdateCount)
223
+ {
224
+ this.movePass = engineObjectsUpdateCount;
225
+ render3DMove(this);
226
+ }
227
+ }
228
+
229
+ // placed from its parent first, so a sync2D object copies where it is now, then its children from it
230
+ super.updateTransforms(false);
231
+ if (!paused && this.sync2D)
232
+ this.pos3D.x = this.pos.x, this.pos3D.y = this.pos.y, this.rotation3D.z = -this.angle;
233
+ if (updateChildren)
234
+ for (const child of this.children)
235
+ child.updateTransforms();
236
+ }
237
+
238
+ /** Set how this object collides, the same flags as in 2D
239
+ * - Solid collision happens in 3D here, against size3D boxes or spheres; a child sits it out
240
+ * - The boxes turn with rotation3D, against height maps and voxel maps an object is still its upright box
241
+ * - A sync2D object collides in 2D instead, against the 2D size, so set that as well as size3D
242
+ * @param {boolean} [collideSolidObjects] - Take part in solid collision
243
+ * @param {boolean} [isSolid] - Block other objects, a pair where neither one blocks passes through;
244
+ * blocking needs collideSolidObjects, so isSolid on its own is not allowed
245
+ * @param {boolean} [collideLevel] - Collide with the level, the height maps and voxel maps, or the 2D tile layers
246
+ * for a sync2D object
247
+ * @param {boolean} [collideRaycast] - Raycasts, 2D only; 3D has render3D.pick and engineObjectsRaycast3D */
248
+ setCollision(collideSolidObjects=true, isSolid=true, collideLevel=true, collideRaycast=false)
249
+ { super.setCollision(collideSolidObjects, isSolid, collideLevel, collideRaycast); }
250
+
251
+ /** Called by a VoxelMap to ask whether a block stops this object, a hook to let one through or react to it
252
+ * @param {number} type - The block's type, 1 to 255
253
+ * @param {Vector3} cell - The block's cell in the map
254
+ * @return {boolean} - true to be stopped by it, every block stops it by default */
255
+ collideWithVoxel(type, cell) { return true; }
256
+
257
+ /** Returns the world position
258
+ * @return {Vector3} */
259
+ getWorldPos3D() { return render3DObjectMatrix(this).getTranslation(); }
260
+
261
+ /** Returns the direction the object faces, its -Z axis in the world
262
+ * @return {Vector3} */
263
+ getForward3D() { return render3DAxis(render3DObjectMatrix(this).m, 8).normalize(-1); }
264
+
265
+ /** Returns the object's right axis in the world
266
+ * @return {Vector3} */
267
+ getRight3D() { return render3DAxis(render3DObjectMatrix(this).m, 0).normalize(); }
268
+
269
+ /** Returns the object's up axis in the world
270
+ * @return {Vector3} */
271
+ getUp3D() { return render3DAxis(render3DObjectMatrix(this).m, 4).normalize(); }
272
+
273
+ /** Returns a copy of the object's world transform, the parent's included when attached to an EngineObject3D
274
+ * - The object keeps its matrix and rebuilds it only when its position, rotation or scale changed, so this is
275
+ * cheap to call
276
+ * @return {Matrix4} */
277
+ getMatrix() { return render3DObjectMatrix(this).copy(); }
278
+
279
+ /** How rough the surface is, 0 a mirror to 1 matte, as glTF and three.js's MeshStandardMaterial have it: another
280
+ * way to set shininess, which is what is kept, shininess = 2 / roughness^4 - 2 and at least 1; it sets both the
281
+ * highlight and how blurred a reflection is; read back it is the same up to about .9, above which shininess is 1
282
+ * and it reads .9
283
+ * @return {number} */
284
+ get roughness() { return (2 / (this.shininess + 2)) ** .25; }
285
+ set roughness(roughness)
286
+ {
287
+ ASSERT(isNumber(roughness) && roughness >= 0, 'roughness must be a number, 0 or more', roughness);
288
+ this.shininess = max(1, 2 / max(roughness, 1e-3) ** 4 - 2); // none is a mirror, still a finite number
289
+ }
290
+
291
+ /** Turn the object so its -Z axis points at a world space target, sets pitch and yaw and clears roll
292
+ * @param {Vector3} target */
293
+ lookAt(target)
294
+ {
295
+ // rotation3D is local to the parent, so a child has to aim at the target from the parent's point of view
296
+ const parent = this.parent instanceof EngineObject3D ? this.parent : undefined;
297
+ const local = parent ? parent.getMatrix().invert().transformPoint(target) : target;
298
+ this.rotation3D = render3DLookRotation(local.subtract(this.pos3D), this.rotation3D);
299
+ }
300
+
301
+ /** Attaches a child, its pos3D, rotation3D and scale3D taken as the offset from this one; returns child for chaining
302
+ * - The 2D offset arguments do nothing for an EngineObject3D child, set its pos3D
303
+ * @param {EngineObject} child
304
+ * @param {Vector2} [localPos]
305
+ * @param {number} [localAngle]
306
+ * @return {EngineObject} The child object attached */
307
+ addChild(child, localPos, localAngle)
308
+ {
309
+ // addChild brings the child's transform up to date, which is not a step: it already moved this pass as
310
+ // a root, or moves in the next one, so it must not move by its velocity again here
311
+ if (child instanceof EngineObject3D)
312
+ child.movePass = engineObjectsUpdateCount;
313
+ return super.addChild(child, localPos, localAngle);
314
+ }
315
+
316
+ /** Attaches a child without moving it: its pos3D, rotation3D and scale3D become what they have to be under this
317
+ * parent to keep its world transform, where addChild takes them as the offset; returns child for chaining
318
+ * - A parent scaled unevenly and a child turned under it make a shear, which those three values cannot hold,
319
+ * so the child comes out as close as they can get; a uniform scale is exact
320
+ * @param {EngineObject} child
321
+ * @return {EngineObject} The child object attached */
322
+ attach(child)
323
+ {
324
+ if (!(child instanceof EngineObject3D))
325
+ return super.attach(child); // a 2D child only has the 2D transform to keep
326
+ child.parent?.removeChild(child); // keeps its world values, so its own matrix is its world matrix
327
+ const local = render3DObjectMatrix(this).copy().invert().multiply(render3DObjectMatrix(child));
328
+ super.attach(child);
329
+ child.pos3D = local.getTranslation();
330
+ child.rotation3D = local.getRotation();
331
+ child.scale3D = local.getScale();
332
+ if (child.localMatrix)
333
+ child.localMatrix = local; // a matrix given whole stays whole, shear and all
334
+ return child;
335
+ }
336
+
337
+ /** Removes a child from this one, it stays where it is in the world: its pos3D, rotation3D and scale3D become
338
+ * its world values, with the same shear caveat as attach; a child being destroyed is let go as it is
339
+ * @param {EngineObject} child */
340
+ removeChild(child)
341
+ {
342
+ if (child instanceof EngineObject3D && !child.destroyed)
343
+ render3DTakeWorld(child, render3DObjectMatrix(child));
344
+ if (child instanceof EngineObject3D && child.drawOwner === this)
345
+ child.drawOwner = undefined; // a part taken off draws with its own settings
346
+ super.removeChild(child);
347
+ }
348
+
349
+ /** Draw a different mesh and free the GPU buffer of the one it replaces
350
+ * - For a mesh built again when something changes, like a score, a rebuilt terrain or a loaded model
351
+ * - A mesh another object is still drawing is left alone, since builders are often shared
352
+ * - Freeing one held somewhere else only costs it an upload, the points it was built from stay
353
+ * @param {Mesh} [mesh] - The mesh to draw from now on, undefined to draw nothing
354
+ * @return {Mesh|undefined} - The mesh passed in */
355
+ setMesh(mesh)
356
+ {
357
+ ASSERT(!mesh || mesh instanceof Mesh, 'mesh must be a Mesh or undefined');
358
+ const old = this.mesh;
359
+ this.mesh = mesh;
360
+ // nothing to free and nothing to look for when it was never uploaded
361
+ if (old && old !== mesh && old.buffer && !engineObjects.some(o=> /** @type {EngineObject3D} */ (o).mesh === old))
362
+ old.dispose();
363
+ return mesh;
364
+ }
365
+
366
+ /** 2D rendering is skipped, the mesh is drawn by render3D during the 3D pass */
367
+ render() {}
368
+
369
+ /** Draw the object in 3D, called by the 3D pass with the draw state set from this object's flags, draws the mesh by default
370
+ * @return {void} */
371
+ render3D()
372
+ {
373
+ // an opaque draw comes out solid however low its alpha is, so a fade with no flag looks like nothing happened
374
+ ASSERT(this.transparent || render3DSetting(this, 'additive') || this.color.a >= 1, // a part, its owner's
375
+ 'an object that fades needs its transparent flag, an opaque draw ignores the color alpha', this.color);
376
+ // the matrix the object keeps, rebuilt only when it moved, the same one for the shadow pass and the main pass
377
+ const matrix = render3DObjectMatrix(this);
378
+ if (this.mesh)
379
+ render3D.drawMesh(this.mesh, matrix, this.tileInfo, this.color);
380
+ else if (this.tileInfo)
381
+ {
382
+ // a sprite, at the world size the collect, pick and solid collision helpers measure it at
383
+ const m = matrix.m, size = render3DWorldSize(this, m);
384
+ render3D.drawBillboard(vec3(m[12], m[13], m[14]), vec2(size.x, size.y), this.tileInfo, this.color,
385
+ this.rotation3D.z, this.upright);
386
+ }
387
+ }
388
+ }
389
+
390
+ // the draw settings an object hands to the parts it draws through, each with its default: a glTF model's parts, an
391
+ // attached prefab's, a voxel map's see through blocks
392
+ const RENDER3D_PART_SETTINGS = {emissive: 0, additive: false, specular: 0, shininess: 16, reflectivity: 0,
393
+ normalMap: undefined, normalScale: 1, emissiveMap: undefined, emissiveMapColor: WHITE, receiveShadow: true,
394
+ castShadow: true, pixelated: false, shader: undefined, environment: undefined, renderAfter2D: undefined,
395
+ renderOrder: 0};
396
+
397
+ // make a part draw with its owner's settings: it draws with the owner's where the owner set it away from the default,
398
+ // and its own otherwise, so a model's unlit or rough part keeps what its file gave it; a link the renderer follows
399
+ // where it reads them (render3DSetting), so a setting changed later is seen, before the stage is chosen too, and the
400
+ // part keeps its own values as plain properties, which keeps its shape as fast as any object's
401
+ function render3DShareSettings(part, owner) { part.drawOwner = owner; }
402
+
403
+ // the value of a draw setting an object draws with: for a part, its owner's where that is set, through every owner
404
+ // up, and its own otherwise
405
+ function render3DSetting(o, name)
406
+ {
407
+ const owner = o.drawOwner;
408
+ if (owner)
409
+ {
410
+ const value = render3DSetting(owner, name);
411
+ if (value !== RENDER3D_PART_SETTINGS[name])
412
+ return value;
413
+ }
414
+ return o[name];
415
+ }
416
+
417
+ // a part's draw settings gathered for the draw state, in one object kept for it, so nothing is made per draw
418
+ const render3DPartScratch = {...RENDER3D_PART_SETTINGS};
419
+ function render3DPartSettings(o)
420
+ {
421
+ for (const name in RENDER3D_PART_SETTINGS)
422
+ render3DPartScratch[name] = render3DSetting(o, name);
423
+ return render3DPartScratch;
424
+ }
425
+
426
+ // make an object's own transform a world one, for an object leaving its parent: its position, rotation and scale
427
+ // from the world matrix, and a matrix given whole, shear and all
428
+ function render3DTakeWorld(o, world)
429
+ {
430
+ o.pos3D = world.getTranslation();
431
+ o.rotation3D = world.getRotation();
432
+ o.scale3D = world.getScale();
433
+ if (o.localMatrix)
434
+ o.localMatrix = world.copy();
435
+ }
436
+
437
+ // an object's world matrix, the one it keeps: rebuilt only when its position, rotation or scale changed since the
438
+ // last build, or its parent's matrix did, so an object that stands still costs nine compares a frame instead of
439
+ // the trig and a new matrix; the matrix returned is the object's own, read it and never change it
440
+ const render3DLocalMatrix = new Matrix4;
441
+ function render3DObjectMatrix(o)
442
+ {
443
+ const parent = o.parent instanceof EngineObject3D ? o.parent : undefined;
444
+ const parentMatrix = parent && render3DObjectMatrix(parent); // the parent first, so its version is current
445
+ const p = o.pos3D, r = o.rotation3D, s = o.scale3D, k = o.matrixBuilt, local = o.localMatrix;
446
+ if (local)
447
+ {
448
+ // a matrix given whole is taken as it is each time, it can change in place
449
+ if (parent)
450
+ {
451
+ o.worldMatrix.m.set(parentMatrix.m);
452
+ o.worldMatrix.multiply(local);
453
+ o.matrixParentVersion = parent.matrixVersion;
454
+ }
455
+ else
456
+ o.worldMatrix.m.set(local.m);
457
+ k[0] = NaN; // built from the matrix, so going back to pos3D builds again
458
+ o.matrixParent = parent;
459
+ ++o.matrixVersion;
460
+ }
461
+ else if (k[0] !== p.x || k[1] !== p.y || k[2] !== p.z || k[3] !== r.x || k[4] !== r.y || k[5] !== r.z
462
+ || k[6] !== s.x || k[7] !== s.y || k[8] !== s.z || o.matrixParent !== parent
463
+ || parent && o.matrixParentVersion !== parent.matrixVersion)
464
+ {
465
+ k[0] = p.x, k[1] = p.y, k[2] = p.z, k[3] = r.x, k[4] = r.y, k[5] = r.z, k[6] = s.x, k[7] = s.y, k[8] = s.z;
466
+ if (parent)
467
+ {
468
+ // the parent's world matrix times the local one
469
+ buildMatrix(p, r, s, render3DLocalMatrix);
470
+ o.worldMatrix.m.set(parentMatrix.m);
471
+ o.worldMatrix.multiply(render3DLocalMatrix);
472
+ o.matrixParentVersion = parent.matrixVersion;
473
+ }
474
+ else
475
+ buildMatrix(p, r, s, o.worldMatrix);
476
+ o.matrixParent = parent;
477
+ ++o.matrixVersion;
478
+ }
479
+ return o.worldMatrix;
480
+ }
481
+
482
+ // move an object by its 3D velocities, each slowed by its damping, an object with mass falling with render3D.gravity
483
+ function render3DMove(o, cap)
484
+ {
485
+ // the vectors change in place, as the 2D object's do: this runs for every object every frame
486
+ // a sync2D object's damping is the 2D physics', its 3D velocities are its own to set
487
+ const p = o.pos3D, v = o.velocity3D, r = o.rotation3D, a = o.angleVelocity3D;
488
+ const d = o.sync2D ? 1 : o.damping, e = o.sync2D ? 1 : o.angleDamping;
489
+ // damped first and gravity added after, the order EngineObject.updatePhysics uses,
490
+ // so the same mass, damping and gravity fall the same way in both
491
+ v.x *= d, v.y *= d, v.z *= d;
492
+ if (o.mass && !o.sync2D) // a 2D driven object gets the 2D gravity instead
493
+ {
494
+ const g = render3D.gravity, s = o.gravityScale;
495
+ v.x += g.x * s, v.y += g.y * s, v.z += g.z * s;
496
+ }
497
+ if (cap !== undefined) // the speed cap of what collides, after gravity, as in 2D
498
+ v.x = clamp(v.x, -cap, cap), v.y = clamp(v.y, -cap, cap), v.z = clamp(v.z, -cap, cap);
499
+ p.x += v.x, p.y += v.y, p.z += v.z;
500
+ r.x += a.x *= e, r.y += a.y *= e, r.z += a.z *= e;
501
+ }
502
+
503
+ // keep an object that moved out of the level's solid geometry, clearing what it stood on for the level to set again
504
+ // ground is what it stood on last frame, which a height map keeps it on going down a slope
505
+ function render3DCollideLevel(o, oldPos, ground)
506
+ {
507
+ for (const level of render3DLevel)
508
+ level.destroyed || level.levelCollide3D(o, oldPos, level === ground);
509
+ }
510
+
511
+ // where a solid object is in the world and what it collides as: the sphere that fits size3D, or the size3D box,
512
+ // each grown by the object's scale
513
+ // only objects that own where they are take part, so pos3D is already world space, and however the object is
514
+ // turned its axes come out as long as its scale makes them; building the transform to read that back off it
515
+ // costs six trig calls and a matrix for every pair tested, which is the whole cost of a crowded scene
516
+ function render3DSolidShape(o)
517
+ {
518
+ ASSERT(!o.parent, 'a child rides along with its parent, it has no world pos3D of its own to collide with');
519
+ const s = o.size3D, k = o.scale3D;
520
+ const kx = abs(k.x), ky = abs(k.y), kz = abs(k.z);
521
+ if (o.collideAsSphere3D)
522
+ return {pos: o.pos3D.copy(), radius: max(s.x, s.y, s.z) / 2 * max(kx, ky, kz)};
523
+ // a turned box has its axes, a solid is never a child so its rotation is the world's; an upright one has none, and
524
+ // neither does a sprite, whose rotation turns how it faces the camera, not its box
525
+ const axes = !render3DIsSprite(o) && isTurned3D(o.rotation3D) ? render3DSolidAxes(o) : undefined;
526
+ return {pos: o.pos3D.copy(), size: vec3(s.x * kx, s.y * ky, s.z * kz), axes};
527
+ }
528
+
529
+ // a turned solid's three axes, worked out when its rotation changes and kept until then, so the pairs it is tested
530
+ // against each frame do not each work them out again
531
+ const render3DSolidAxesCache = new WeakMap;
532
+ function render3DSolidAxes(o)
533
+ {
534
+ const r = o.rotation3D, kept = render3DSolidAxesCache.get(o);
535
+ if (kept && kept.x === r.x && kept.y === r.y && kept.z === r.z)
536
+ return kept.axes;
537
+ const axes = boxAxes3D(r);
538
+ render3DSolidAxesCache.set(o, {x: r.x, y: r.y, z: r.z, axes});
539
+ return axes;
540
+ }
541
+
542
+ // how far a solid shape can reach from its own center, for a quick reject before the exact test
543
+ // it has to be the shape's own radius, or a wider one: a box reaches to its corner, and a sphere
544
+ // takes the largest scale the same way render3DSolidShape does, or the reject would skip real touches
545
+ function render3DSolidReach(o)
546
+ {
547
+ const s = o.size3D, k = o.scale3D;
548
+ const kx = abs(k.x), ky = abs(k.y), kz = abs(k.z);
549
+ if (o.collideAsSphere3D)
550
+ return max(s.x, s.y, s.z) / 2 * max(kx, ky, kz);
551
+ const x = s.x * kx, y = s.y * ky, z = s.z * kz;
552
+ return (x*x + y*y + z*z) ** .5 / 2; // written out, it is asked for every pair each frame
553
+ }
554
+
555
+ // what it takes to move shape a clear of shape b, whichever pair of shapes they are, or undefined for no touch
556
+ function render3DSolidPush(a, b)
557
+ {
558
+ // a turned box is tested by its kept axes, the upright pairs as they always were
559
+ const sphereBox = (sphere, box)=> box.axes ? collideSphereOrientedBox3D(sphere.pos, sphere.radius, box.pos,
560
+ box.size, box.axes) : collideSphereBox(sphere.pos, sphere.radius, box.pos, box.size);
561
+ if (!a.size) // a is a sphere
562
+ return b.size ? sphereBox(a, b) : collideSphereSphere(a.pos, a.radius, b.pos, b.radius);
563
+ if (!b.size) // only b is, so push b out of a and turn it around
564
+ {
565
+ const push = sphereBox(b, a);
566
+ return push && push.scale(-1);
567
+ }
568
+ return a.axes || b.axes ? collideOrientedBoxes3D(a.pos, a.size, a.axes ?? BOX_WORLD_AXES, b.pos, b.size,
569
+ b.axes ?? BOX_WORLD_AXES) : collideBoxBox3D(a.pos, a.size, b.pos, b.size);
570
+ }
571
+
572
+ // whether a touching pair resolves: both hear about it, and the answer is kept for the frame, so a pair met again as
573
+ // one object is settled after being pushed is not asked twice; an object's own turn asks again, as it always has,
574
+ // unless a settle asked that pair already this pass (render3DSettleAsked), so a wall hears about it once a frame
575
+ function render3DCollideAsk(a, b, push, useKept=true)
576
+ {
577
+ const kept = useKept || render3DSettleAsked(a, b) ? engineObjectsCollidePairAnswer(a, b) : undefined;
578
+ if (kept !== undefined) return kept;
579
+ const resolveA = a.collideWithObject(b, push), resolveB = b.collideWithObject(a, push.scale(-1));
580
+ const resolve = !!(resolveA && resolveB);
581
+ engineObjectsCollidePairAdd(a, b, resolve);
582
+ engineObjectsCollidePairAdd(b, a, resolve);
583
+ useKept && render3DSettleAsked(a, b, true);
584
+ return resolve;
585
+ }
586
+
587
+ // the pairs a settle asked this pass, each object to the others; asked with mark set, it marks the pair
588
+ const render3DSettleAskedPairs = {pass: -1, pairs: new Map};
589
+ function render3DSettleAsked(a, b, mark=false)
590
+ {
591
+ const kept = render3DSettleAskedPairs;
592
+ if (kept.pass !== engineObjectsUpdateCount)
593
+ {
594
+ if (!mark) return false;
595
+ kept.pass = engineObjectsUpdateCount;
596
+ kept.pairs.clear();
597
+ }
598
+ if (mark)
599
+ {
600
+ for (const [one, other] of [[a, b], [b, a]])
601
+ {
602
+ const set = kept.pairs.get(one);
603
+ set ? set.add(other) : kept.pairs.set(one, new Set([other]));
604
+ }
605
+ return true;
606
+ }
607
+ return !!kept.pairs.get(a)?.has(b);
608
+ }
609
+
610
+ // where a solid was when this pass of the engine began, taken at the start of its own updatePhysics, or the first time
611
+ // a pair needs it if that comes first, as for one pushed or a mass 0 one not updated yet, so a
612
+ // pair resolved in the turn of either one knows where both came from
613
+ function render3DCollideFrom(o)
614
+ {
615
+ if (o.collideFromPass !== engineObjectsUpdateCount)
616
+ {
617
+ o.collideFromPass = engineObjectsUpdateCount;
618
+ o.collideFrom.set(o.pos3D.x, o.pos3D.y, o.pos3D.z);
619
+ }
620
+ return o.collideFrom;
621
+ }
622
+
623
+ // a push that would carry a on the way it is going relative to b, out the far side past b's middle, as a fast object
624
+ // goes through a thin wall or two movers through each other, is turned back to the side a came from, as 2D resolves
625
+ // from where an object was; both go by where they were when the pass began, so it is the same in either one's turn
626
+ function render3DPushFrom(shapeA, shapeB, a, b, push)
627
+ {
628
+ const aFrom = render3DCollideFrom(a), bFrom = render3DCollideFrom(b), pa = a.pos3D, pb = b.pos3D;
629
+ const mx = pa.x - aFrom.x - pb.x + bFrom.x, my = pa.y - aFrom.y - pb.y + bFrom.y, mz = pa.z - aFrom.z - pb.z + bFrom.z;
630
+ if (push.x * mx + push.y * my + push.z * mz <= 0)
631
+ return push;
632
+ return render3DSolidPushBack(shapeA, shapeB, aFrom, bFrom) ?? push;
633
+ }
634
+
635
+ // the solids that do not move, mass 0, in the order of the engine's list, gathered once a pass for the settles, which
636
+ // would otherwise walk every collider for each push between two movers
637
+ const render3DFixed = {pass: -1, list: []};
638
+ function render3DFixedSolids()
639
+ {
640
+ if (render3DFixed.pass !== engineObjectsUpdateCount)
641
+ {
642
+ render3DFixed.pass = engineObjectsUpdateCount;
643
+ render3DFixed.list = engineObjectsCollide.filter((b)=> !b.mass && b instanceof EngineObject3D &&
644
+ !b.parent && !b.sync2D);
645
+ }
646
+ return render3DFixed.list;
647
+ }
648
+
649
+ // push an object pushed by another out of the solids that do not move, mass 0, back to the side it came from;
650
+ // they take the touch as any pair does, so a one way platform still lets it through; not the one that pushed it, which
651
+ // already did, and a push of a rounding error is none, or a box on a turned ramp would be pushed along its slant;
652
+ // from is where it was before the push
653
+ function render3DSettleFixed(o, pusher, from)
654
+ {
655
+ if (o.destroyed || !o.collideSolidObjects) return;
656
+ let shape = render3DSolidShape(o);
657
+ const reachO = render3DSolidReach(o);
658
+ for (const b of render3DFixedSolids())
659
+ {
660
+ // a pair where neither is solid passes through, as in the pair loop; a solid against one that is not still meets
661
+ if (b === o || b === pusher || b.mass || b.destroyed || !o.isSolid && !b.isSolid)
662
+ continue;
663
+ const p = shape.pos, q = b.pos3D, reach = reachO + render3DSolidReach(b);
664
+ const dx = p.x - q.x, dy = p.y - q.y, dz = p.z - q.z;
665
+ if (dx*dx + dy*dy + dz*dz > reach*reach) continue;
666
+ const shapeB = render3DSolidShape(b);
667
+ let push = render3DSolidPush(shape, shapeB);
668
+ if (!push || push.lengthSquared() < 1e-12) continue;
669
+ push = render3DPushFrom(shape, shapeB, o, b, push);
670
+ if (!render3DCollideAsk(o, b, push)) continue;
671
+ // standing as in a pair: a push off a face within its groundAngle of up holds it there, turned straight up
672
+ const lengthSquared = push.lengthSquared(), up = push.y / lengthSquared ** .5;
673
+ if (up > 0 && o.mass && up >= cos(o.groundAngle) && render3DOnFace(push, shapeB))
674
+ {
675
+ o.groundObject = b;
676
+ if (push.x || push.z)
677
+ push = vec3(0, lengthSquared / push.y, 0);
678
+ }
679
+ o.pos3D = o.pos3D.add(push);
680
+ shape = render3DSolidShape(o);
681
+ const normal = push.normalize();
682
+ if (o.velocity3D.dot(normal) < 0)
683
+ o.velocity3D = o.velocity3D.reflect(normal, o.restitution);
684
+ }
685
+ // and out of the level, the voxel and height maps, from where it was before the push, so a move of the game's own,
686
+ // as a collision callback that sends it away, is not swept back across the level
687
+ if (o.collideLevel && o.mass)
688
+ render3DCollideLevel(o, from, undefined);
689
+ }
690
+
691
+ // what moves shape a back clear of shape b to the side it came from: along an axis the two were clear on where they
692
+ // came from, the least of those, as 2D resolves it from where the object was; undefined when they overlapped on every
693
+ // axis there too, or a box is turned, which keeps the least push
694
+ function render3DSolidPushBack(a, b, aFrom, bFrom)
695
+ {
696
+ if (a.axes || b.axes) return;
697
+ const half = (shape)=> shape.size ? shape.size.scale(.5) : vec3(shape.radius);
698
+ const halfA = half(a), halfB = half(b);
699
+ let axis, amount = Infinity, side = 0;
700
+ for (const k of ['x', 'y', 'z'])
701
+ {
702
+ const reach = halfA[k] + halfB[k], was = aFrom[k] - bFrom[k];
703
+ // it overlapped on this axis before it moved as well; one resting against it touches, which is clear, give or
704
+ // take the rounding of the push that put it there
705
+ if (abs(was) < reach - 1e-6) continue;
706
+ const s = sign(was), need = reach - s * (a.pos[k] - b.pos[k]);
707
+ if (need > 0 && need < amount)
708
+ axis = k, amount = need, side = s;
709
+ }
710
+ if (!axis) return;
711
+ const push = vec3();
712
+ push[axis] = side * amount;
713
+ return push;
714
+ }
715
+
716
+ // a sprite, a tile with no mesh, which faces the camera however it is turned
717
+ const render3DIsSprite = (o)=> !o.mesh && !!o.tileInfo;
718
+
719
+ // whether a push leaves a box by one of its faces, along one of its axes, not by an edge or a corner; a sphere has
720
+ // no face to stand on
721
+ function render3DOnFace(push, shape)
722
+ {
723
+ if (!shape.size) return false;
724
+ const n = push.normalize(), axes = shape.axes ?? BOX_WORLD_AXES;
725
+ return max(abs(n.dot(axes[0])), abs(n.dot(axes[1])), abs(n.dot(axes[2]))) > 1 - 1e-6;
726
+ }
727
+
728
+ // push a solid object out of the solids before it in the engine's list of them, so each pair is resolved once:
729
+ // the ones after it update later and test against it then, and an object that is not in the list yet, because it
730
+ // turned collision on this frame, tests them all itself and is not tested back
731
+ // one pair per test is half the work of the 2D solver, which tests both directions; the difference only shows
732
+ // when a collideWithObject destroys some third object, whose own turn then finds the pair already gone
733
+ function render3DCollideSolid(a)
734
+ {
735
+ let shapeA = render3DSolidShape(a);
736
+ const reachA = render3DSolidReach(a);
737
+ for (const b of engineObjectsCollide)
738
+ {
739
+ if (b === a) break;
740
+ // a child is part of its parent
741
+ if (b.destroyed || !(b instanceof EngineObject3D) || b.parent || b.sync2D) continue;
742
+ if (!a.isSolid && !b.isSolid) continue; // neither one blocks, so they pass through each other
743
+
744
+ // the pairs nowhere near each other are almost all of them in a scene of any size, so
745
+ // settle those with one distance check instead of building a shape for each
746
+ const p = shapeA.pos, q = b.pos3D, reach = reachA + render3DSolidReach(b);
747
+ const dx = p.x - q.x, dy = p.y - q.y, dz = p.z - q.z;
748
+ if (dx*dx + dy*dy + dz*dz > reach*reach)
749
+ continue;
750
+
751
+ const shapeB = render3DSolidShape(b);
752
+ let push = render3DSolidPush(shapeA, shapeB);
753
+ if (!push) continue;
754
+ push = render3DPushFrom(shapeA, shapeB, a, b, push);
755
+
756
+ // both objects hear about it, and either one can take the touch over
757
+ if (!render3DCollideAsk(a, b, push, false)) continue;
758
+
759
+ // standing: resting on a box's face within the upper one's groundAngle of level holds it there, the push
760
+ // turned straight up, as far as it takes to leave the surface, so it does not creep down a ramp; only what
761
+ // moves stands, and a sphere, an edge or a corner is nothing to stand on, what rests there rolls off
762
+ const lengthSquared = push.lengthSquared(), up = push.y / lengthSquared ** .5;
763
+ const aStands = up > 0 && a.mass && up >= cos(a.groundAngle) && render3DOnFace(push, shapeB);
764
+ const bStands = up < 0 && b.mass && -up >= cos(b.groundAngle) && render3DOnFace(push, shapeA);
765
+ if (aStands || bStands)
766
+ {
767
+ aStands ? a.groundObject = b : b.groundObject = a;
768
+ if (push.x || push.z) // one straight up already is used as it is
769
+ push = vec3(0, lengthSquared / push.y, 0);
770
+ }
771
+
772
+ // heavier objects move less, mass 0 stays put; then bounce apart when moving toward each other
773
+ const total = a.mass + b.mass;
774
+ const weightA = !a.mass ? 0 : !b.mass ? 1 : b.mass / total;
775
+ const weightB = !b.mass ? 0 : !a.mass ? 1 : a.mass / total;
776
+ const fromA = a.pos3D.copy(), fromB = b.pos3D.copy(); // where the push moves them from, for the settles
777
+ a.pos3D = a.pos3D.add(push.scale(weightA));
778
+ b.pos3D = b.pos3D.subtract(push.scale(weightB));
779
+ // mass 0 keeps its velocity too, so a moving platform keeps moving, and what hits it bounces by its own
780
+ // restitution as it would off a static wall
781
+ const normal = push.normalize();
782
+ if (weightA && a.velocity3D.dot(normal) < 0)
783
+ a.velocity3D = a.velocity3D.reflect(normal, a.restitution);
784
+ if (weightB && b.velocity3D.dot(normal) > 0)
785
+ b.velocity3D = b.velocity3D.reflect(normal, b.restitution);
786
+
787
+ // the settles come after the bounce, so a wall's push is the last word on the velocity
788
+ if (weightA && weightB)
789
+ {
790
+ // a met the solids before it in the list already, the fixed ones among them, so a push from something
791
+ // that moves is settled against those now, and b takes what a could not move, as if a were fixed
792
+ const pushed = a.pos3D.copy();
793
+ render3DSettleFixed(a, b, fromA);
794
+ b.pos3D = b.pos3D.add(a.pos3D.subtract(pushed));
795
+ }
796
+ if (weightB)
797
+ {
798
+ // b had its turn already, so a push into a wall is settled against the fixed solids now, or it would end
799
+ // the frame in the wall, and a hard enough shove would carry it through; then a takes what b could not
800
+ // move, as a pusher stops against a crate held by a wall, and is settled itself, squeezed at worst
801
+ const pushed = b.pos3D.copy();
802
+ render3DSettleFixed(b, a, fromB);
803
+ const backX = b.pos3D.x - pushed.x, backY = b.pos3D.y - pushed.y, backZ = b.pos3D.z - pushed.z;
804
+ if (weightA && (backX || backY || backZ))
805
+ {
806
+ a.pos3D = a.pos3D.add(vec3(backX, backY, backZ));
807
+ render3DSettleFixed(a, b, fromA);
808
+ }
809
+ }
810
+ if (weightA)
811
+ shapeA = render3DSolidShape(a); // it moved, so the next solid must be tested against where it is now
812
+ }
813
+ }
814
+
815
+ /**
816
+ * Collect the EngineObject3D objects whose boxes overlap a sphere or a box, the 3D twin of engineObjectsCollect
817
+ * - Each object is its size3D box at its world position, turned as it is turned; lights, emitters and trails have no
818
+ * size and are never collected
819
+ * @param {Vector3} pos - Center of the area
820
+ * @param {Vector3|number} size - Diameter of a sphere if a number, 0 for a point, full size of a box if a Vector3
821
+ * @param {Array<EngineObject>} [objects] - Defaults to every object
822
+ * @param {boolean} [testCenters] - Test only each object's center, a little faster, and ignores object sizes
823
+ * @return {Array<EngineObject3D>}
824
+ * @memberof Render3D
825
+ */
826
+ function engineObjectsCollect3D(pos, size, objects=engineObjects, testCenters=false)
827
+ {
828
+ // a size of 0 is a point, tested against each box, since a sphere of no size could never hit
829
+ const radiusSquared = typeof size === 'number' && size > 0 ? (size/2)**2 : undefined;
830
+ const box = radiusSquared ? undefined : typeof size === 'number' ? vec3() : render3DSize3(size);
831
+ const collected = [];
832
+ for (const o of objects)
833
+ {
834
+ if (!(o instanceof EngineObject3D) || o.destroyed) continue;
835
+ // the box in world space, scaled by the object and its parents
836
+ const m = render3DObjectMatrix(o).m, s = o.size3D;
837
+ if (!(s.x || s.y || s.z)) continue;
838
+ const center = vec3(m[12], m[13], m[14]);
839
+ const worldSize = testCenters ? vec3() : render3DWorldSize(o, m);
840
+ // a turned box is tested as turned, its axes read off the matrix, its columns with the scale taken out
841
+ const turned = !testCenters && !render3DIsSprite(o) && (m[1] || m[2] || m[4] || m[6] || m[8] || m[9]);
842
+ const axis = (i)=> vec3(m[i], m[i+1], m[i+2]).normalize();
843
+ let axes = turned ? [axis(0), axis(4), axis(8)] : undefined;
844
+ if (axes && (abs(axes[0].dot(axes[1])) > 1e-6 || abs(axes[0].dot(axes[2])) > 1e-6 ||
845
+ abs(axes[1].dot(axes[2])) > 1e-6))
846
+ {
847
+ // a turned child of an unevenly scaled parent is sheared, its edges no longer square, which the box
848
+ // tests do not take; the upright box around it stands in, which may take a little more but never misses
849
+ const e = axes.map((a, i)=> a.scale(i ? i > 1 ? worldSize.z / 2 : worldSize.y / 2 : worldSize.x / 2));
850
+ const reach = (c)=> 2 * (abs(e[0][c]) + abs(e[1][c]) + abs(e[2][c]));
851
+ worldSize.set(reach('x'), reach('y'), reach('z'));
852
+ axes = undefined;
853
+ }
854
+ let hit;
855
+ if (box && axes)
856
+ hit = !!collideOrientedBoxes3D(pos, box, BOX_WORLD_AXES, center, worldSize, axes);
857
+ else if (box)
858
+ hit = isOverlapping3D(pos, box, center, worldSize);
859
+ else if (axes)
860
+ hit = !!collideSphereOrientedBox3D(pos, radiusSquared ** .5, center, worldSize, axes);
861
+ else
862
+ {
863
+ // a sphere against the nearest point of the box
864
+ const dx = max(abs(pos.x - center.x) - worldSize.x/2, 0);
865
+ const dy = max(abs(pos.y - center.y) - worldSize.y/2, 0);
866
+ const dz = max(abs(pos.z - center.z) - worldSize.z/2, 0);
867
+ hit = dx*dx + dy*dy + dz*dz < radiusSquared;
868
+ }
869
+ hit && collected.push(o);
870
+ }
871
+ return collected;
872
+ }
873
+
874
+ // how far along a ray an object is hit, or undefined for a miss; a mesh is hit on its triangles, in its own space,
875
+ // and a sprite as the quad it draws
876
+ const render3DRaycastCenter = vec3();
877
+ function render3DRaycastObject(ray, o)
878
+ {
879
+ if (o.destroyed || !(o instanceof EngineObject3D)) return;
880
+ // a height map or voxel map is hit on its surface, not its box
881
+ if (o instanceof HeightMap || o instanceof VoxelMap) return o.levelRaycast3D(ray);
882
+ if (!(o.mesh || o.tileInfo)) return;
883
+ if (o instanceof InstancedMesh3D) return; // its instances are not objects, and its one sphere is not a thing to hit
884
+ const matrix = render3DObjectMatrix(o), mesh = o.mesh;
885
+ if (!mesh) return render3DRaycastSprite(ray, o, matrix);
886
+ // a mesh that changed since it was measured is measured again, an upload may not have come yet
887
+ const radius = (mesh.dirty || !mesh.radius ? mesh.computeRadius() : mesh.radius) * render3DMaxStretch(matrix.m);
888
+ if (!(radius > 0)) return; // nothing to hit
889
+ // its place read from the matrix into a vector kept for this, a pick tests every object and most miss
890
+ const m = matrix.m, center = render3DRaycastCenter.set(m[12], m[13], m[14]);
891
+ const distance = raycastSphere(ray, center, radius);
892
+ if (distance === undefined) return;
893
+
894
+ // the sphere and then the box in the mesh's own space are quick rejects, and a ray that meets the box is tested
895
+ // against the triangles; the direction is not made unit length, so the distance holds in the world;
896
+ // a mesh flattened to nothing on an axis has no inverse, it is hit as a disc like a sprite
897
+ if (!matrix.determinant()) return render3DRaycastDisc(ray, center, radius);
898
+ const inverse = matrix.copy().invert(), bounds = mesh.bounds || mesh.getBounds();
899
+ const local = new Ray3D(inverse.transformPoint(ray.origin), inverse.transformDirection(ray.direction));
900
+ const hit = raycastBox(local, bounds.min.add(bounds.max).scale(.5), bounds.max.subtract(bounds.min));
901
+ if (hit === undefined || !ray.direction.lengthSquared())
902
+ return hit; // a ray of no length is where it starts, inside the box
903
+ return render3DRaycastMesh(local, mesh);
904
+ }
905
+
906
+ // how far along a ray, in the mesh's own space, its nearest triangle is hit, or undefined: the triangles that face
907
+ // the ray as the pass draws them, clockwise from the front, and the back faces too of a doubleSided mesh; a strip's
908
+ // triangle i is (i-2, i-1, i), the odd ones read the other way, and a join between its pieces has no area and is
909
+ // never hit; nothing is kept between calls, so a mesh that changed is hit as it is now
910
+ function render3DRaycastMesh(ray, mesh)
911
+ {
912
+ const points = mesh.points, indices = mesh.indices, both = mesh.doubleSided;
913
+ const ox = ray.origin.x, oy = ray.origin.y, oz = ray.origin.z;
914
+ const dx = ray.direction.x, dy = ray.direction.y, dz = ray.direction.z;
915
+ const count = indices ? indices.length / 3 | 0 : points.length - 2;
916
+ let nearest;
917
+ for (let i = 0; i < count; ++i)
918
+ {
919
+ // the corners clockwise from the front: an indexed list is written the other way round
920
+ let a, b, c;
921
+ if (indices)
922
+ a = points[indices[i*3]], b = points[indices[i*3+2]], c = points[indices[i*3+1]];
923
+ else
924
+ a = points[i & 1 ? i + 1 : i], b = points[i & 1 ? i : i + 1], c = points[i + 2];
925
+
926
+ // Moller and Trumbore: the determinant is the ray along the face's normal, below zero from the front
927
+ const e1x = b.x - a.x, e1y = b.y - a.y, e1z = b.z - a.z;
928
+ const e2x = c.x - a.x, e2y = c.y - a.y, e2z = c.z - a.z;
929
+ const hx = dy * e2z - dz * e2y, hy = dz * e2x - dx * e2z, hz = dx * e2y - dy * e2x;
930
+ const det = e1x * hx + e1y * hy + e1z * hz;
931
+ if (both ? !det : !(det < 0)) continue;
932
+ const sx = ox - a.x, sy = oy - a.y, sz = oz - a.z;
933
+ const u = (sx * hx + sy * hy + sz * hz) / det;
934
+ if (!(u >= 0 && u <= 1)) continue;
935
+ const qx = sy * e1z - sz * e1y, qy = sz * e1x - sx * e1z, qz = sx * e1y - sy * e1x;
936
+ const v = (dx * qx + dy * qy + dz * qz) / det;
937
+ if (!(v >= 0 && u + v <= 1)) continue;
938
+ const t = (e2x * qx + e2y * qy + e2z * qz) / det;
939
+ if (t >= 0 && !(t >= nearest))
940
+ nearest = t;
941
+ }
942
+ return nearest;
943
+ }
944
+
945
+ // a sprite is the quad drawBillboard draws, its size3D grown by the x and y scale as the draw does, facing the camera
946
+ // as it was last drawn; where the ray meets its plane, inside its half axes
947
+ function render3DRaycastSprite(ray, o, matrix)
948
+ {
949
+ const center = matrix.getTranslation(), worldSize = render3DWorldSize(o, matrix.m), size = vec2(worldSize.x, worldSize.y);
950
+ if (!(size.x > 0 && size.y > 0)) return; // nothing to hit
951
+ if (!ray.direction.lengthSquared()) // a ray of no length is where it starts
952
+ return render3DRaycastDisc(ray, center, hypot(size.x, size.y) / 2);
953
+ const a = render3DBillboardAxes(size, o.rotation3D.z, o.upright);
954
+ const right = vec3(a[0], a[1], a[2]), up = vec3(a[3], a[4], a[5]);
955
+ const t = raycastPlane(ray, center, right.cross(up));
956
+ if (t === undefined) return; // behind, or seen edge on
957
+ const p = ray.getPosition(t).subtract(center);
958
+ return abs(p.dot(right)) <= right.lengthSquared() && abs(p.dot(up)) <= up.lengthSquared() ? t : undefined;
959
+ }
960
+
961
+ // a mesh flattened to nothing, or a sprite for a ray of no length: a disc facing the ray at its center's depth
962
+ function render3DRaycastDisc(ray, center, radius)
963
+ {
964
+ const d = ray.direction, oc = center.subtract(ray.origin), dd = d.dot(d);
965
+ const t = dd ? oc.dot(d) / dd : 0; // its depth along the ray, in the ray's own units
966
+ return t >= 0 && oc.subtract(d.scale(t)).lengthSquared() <= radius*radius ? t : undefined;
967
+ }
968
+
969
+ /**
970
+ * Collect every EngineObject3D a ray passes through, nearest first, the 3D twin of engineObjectsRaycast
971
+ * - The ray has no end, so everything along it counts however far away it is
972
+ * - Use render3D.pick for the nearest one on its own, with the distance to it
973
+ * @param {Ray3D} ray - From render3D.screenToRay, or any ray
974
+ * @param {Array<EngineObject>} [objects] - Defaults to every object; only those with a mesh or a sprite count
975
+ * @return {Array<EngineObject3D>}
976
+ * @memberof Render3D
977
+ */
978
+ function engineObjectsRaycast3D(ray, objects=engineObjects)
979
+ {
980
+ const hits = [];
981
+ for (const o of objects)
982
+ {
983
+ const distance = render3DRaycastObject(ray, o);
984
+ if (distance !== undefined) // only a 3D object has a distance
985
+ hits.push({o: /** @type {EngineObject3D} */ (o), distance});
986
+ }
987
+ return hits.sort((a, b)=> a.distance - b.distance).map(hit=> hit.o);
988
+ }
989
+
990
+ /**
991
+ * Call a function for each EngineObject3D whose box overlaps a sphere or a box
992
+ * - An object destroyed by an earlier callback is skipped
993
+ * @param {Vector3} pos - Center of the area
994
+ * @param {Vector3|number} size - Diameter of a sphere if a number, 0 for a point, full size of a box if a Vector3
995
+ * @param {function(EngineObject3D): void} callback
996
+ * @param {Array<EngineObject>} [objects] - Defaults to every object
997
+ * @param {boolean} [testCenters] - Test only each object's center, see engineObjectsCollect3D
998
+ * @memberof Render3D
999
+ */
1000
+ function engineObjectsCallback3D(pos, size, callback, objects=engineObjects, testCenters=false)
1001
+ {
1002
+ for (const o of engineObjectsCollect3D(pos, size, objects, testCenters))
1003
+ o.destroyed || callback(o);
1004
+ }
1005
+
1006
+ ///////////////////////////////////////////////////////////////////////////////
1007
+ /**
1008
+ * InstancedMesh3D - Many copies of one mesh drawn as one call, with their transforms kept on the GPU
1009
+ * - For big sets that mostly stay put: an instance costs nothing per frame until it changes, so a hundred thousand
1010
+ * trees cost what one tree does; objects and drawMesh batch by themselves too, but rebuild their batch every frame
1011
+ * - setTransformAt, setMatrixAt and setColorAt change one instance, and only the changed range uploads before the
1012
+ * next draw; the matrices and the colors are kept apart, so moving instances uploads 64 bytes each
1013
+ * - For many instances moving every frame, setTransforms places a run of them from arrays of positions, rotations
1014
+ * and scales in one loop, the fastest way; or write matrixData directly and call markDirtyRange
1015
+ * - The instances are in world space; the object's own pos3D, rotation3D and scale3D do not move them
1016
+ * - The whole set is culled by one bounding sphere around the origin, worked out when culling reads it, or set by hand
1017
+ * with radius; it casts and receives shadows like any object
1018
+ * - The object's flags cover the whole set, one emissive, one tileInfo, one shader; only the colors are per instance
1019
+ * - A mirrored instance, one with a negative scale, shows its inside unless the mesh is doubleSided
1020
+ * - A transparent set draws in one go in the transparent stage, its instances are not sorted against each other;
1021
+ * the set sorts against other transparent draws by the object's position, so put pos3D at its middle
1022
+ * - pick, the raycast and the collect helpers do not see the instances, test them yourself from matrixData
1023
+ * @extends EngineObject3D
1024
+ * @memberof Render3D
1025
+ * @example
1026
+ * const forest = new InstancedMesh3D(treeMesh, 1000);
1027
+ * for (let i = 0; i < 1000; ++i)
1028
+ * forest.setTransformAt(i, randomGroundPos(), vec3(0, rand(2*PI), 0));
1029
+ */
1030
+ class InstancedMesh3D extends EngineObject3D
1031
+ {
1032
+ /** Create a set of instances of a mesh, each at the origin in the object's color until it is set
1033
+ * @param {Mesh} mesh
1034
+ * @param {number} count - How many instances there is room for, all of them draw until count is lowered
1035
+ * @param {TileInfo|TextureInfo} [tileInfo] - Texture for all of them
1036
+ * @param {Color} [color] - The color they start with */
1037
+ constructor(mesh, count, tileInfo, color=WHITE)
1038
+ {
1039
+ super(vec3(), mesh, tileInfo, color);
1040
+ ASSERT(mesh instanceof Mesh, 'an InstancedMesh3D needs a Mesh');
1041
+ ASSERT(count >= 1, 'an InstancedMesh3D needs room for at least one instance');
1042
+ this.size3D = vec3(); // not a solid thing to pick or collect
1043
+ /** @property {number} - How many instances draw, the first ones, up to the count it was made with */
1044
+ this.count = count;
1045
+ /** @property {number} - How many instances it was made with */
1046
+ this.maxCount = count;
1047
+ /** @property {Float32Array} - Each instance's matrix, 16 floats, what moving them uploads; edit it directly and
1048
+ * call markDirty for the instances changed */
1049
+ this.matrixData = new Float32Array(count * RENDER3D_MATRIX_FLOATS);
1050
+ /** @property {Float32Array} - Each instance's color and uv rect, 8 floats, uploaded only when they change; edit
1051
+ * it directly and call markColorDirty for the instances changed */
1052
+ this.colorData = new Float32Array(count * RENDER3D_COLOR_FLOATS);
1053
+ /** @property {number} - First instance whose matrix uploads before the next draw */
1054
+ this.dirtyStart = 0;
1055
+ /** @property {number} - One past the last instance whose matrix uploads, nothing when it is not past dirtyStart */
1056
+ this.dirtyEnd = count;
1057
+ /** @property {number} - First instance whose color uploads before the next draw */
1058
+ this.colorDirtyStart = 0;
1059
+ /** @property {number} - One past the last instance whose color uploads */
1060
+ this.colorDirtyEnd = count;
1061
+ // the farthest any instance's position has been from the origin, and the largest scale any has had, both
1062
+ // squared, and the instances set since radius last took them in: setting one does no bounds work at all
1063
+ this.reachSquared = 0;
1064
+ this.scaleSquared = 0;
1065
+ this.boundsStart = 0;
1066
+ this.boundsEnd = count;
1067
+ /** @type {number|undefined} */
1068
+ this.fixedRadius = undefined; // a radius set by hand, see radius
1069
+ this.buffer = undefined; // the GPU copy of matrixData
1070
+ /** @type {WebGLBuffer|undefined} */
1071
+ this.colorBuffer = undefined; // and of colorData
1072
+ this.bufferGeneration = -1; // the context they were made under
1073
+ this.uvTileInfo = tileInfo; // the tile the uv rects were written for
1074
+ const uv = render3DGetTileUVs(tileInfo), matrices = this.matrixData, colors = this.colorData;
1075
+ for (let i = 0; i < count; ++i)
1076
+ {
1077
+ matrices.set(RENDER3D_IDENTITY.m, i * RENDER3D_MATRIX_FLOATS);
1078
+ const k = i * RENDER3D_COLOR_FLOATS;
1079
+ colors[k] = color.r; colors[k+1] = color.g; colors[k+2] = color.b; colors[k+3] = color.a;
1080
+ colors[k+4] = uv.x; colors[k+5] = uv.y; colors[k+6] = uv.w; colors[k+7] = uv.h;
1081
+ }
1082
+ }
1083
+
1084
+ /** Place an instance by its position, rotation and scale, what buildMatrix takes, written straight in with no
1085
+ * matrix made; in world space
1086
+ * @param {number} i
1087
+ * @param {Vector3} [pos]
1088
+ * @param {Vector3} [rotation] - Euler angles in radians
1089
+ * @param {Vector3} [scale] */
1090
+ setTransformAt(i, pos, rotation, scale)
1091
+ {
1092
+ ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
1093
+ matrix4Compose(this.matrixData, i * RENDER3D_MATRIX_FLOATS, pos, rotation, scale);
1094
+ this.markDirty(i);
1095
+ }
1096
+
1097
+ /** Place a run of instances from arrays indexed by instance, plain or typed, as a game keeps them: each is placed
1098
+ * as setTransformAt places it, all in one loop with nothing made, the fastest way to move many every frame;
1099
+ * in world space
1100
+ * @param {number} start - First instance
1101
+ * @param {number} count - How many
1102
+ * @param {ArrayLike<number>} x - Positions
1103
+ * @param {ArrayLike<number>} y
1104
+ * @param {ArrayLike<number>} z
1105
+ * @param {ArrayLike<number>} [rx] - Euler angles in radians, all three or none for upright
1106
+ * @param {ArrayLike<number>} [ry]
1107
+ * @param {ArrayLike<number>} [rz]
1108
+ * @param {ArrayLike<number>} [sx] - Scales, all three or none for 1
1109
+ * @param {ArrayLike<number>} [sy]
1110
+ * @param {ArrayLike<number>} [sz] */
1111
+ setTransforms(start, count, x, y, z, rx, ry, rz, sx, sy, sz)
1112
+ {
1113
+ ASSERT(start >= 0 && count >= 0 && start + count <= this.maxCount, 'instance range out of range');
1114
+ ASSERT(!rx === !ry && !rx === !rz && !sx === !sy && !sx === !sz, 'rotations and scales come in threes');
1115
+ const m = this.matrixData, end = start + count;
1116
+ for (let i = start; i < end; ++i)
1117
+ {
1118
+ // matrix4Compose's expressions, so a run placed here is the same as each placed by setTransformAt
1119
+ const k = i * RENDER3D_MATRIX_FLOATS;
1120
+ const scaleX = sx ? sx[i] : 1, scaleY = sy ? sy[i] : 1, scaleZ = sz ? sz[i] : 1;
1121
+ if (rx)
1122
+ {
1123
+ const cx = cos(rx[i]), sinX = sin(rx[i]);
1124
+ const cy = cos(ry[i]), sinY = sin(ry[i]);
1125
+ const cz = cos(rz[i]), sinZ = sin(rz[i]);
1126
+ m[k] = (cy*cz + sinY*sinX*sinZ) * scaleX; m[k+1] = cx*sinZ * scaleX;
1127
+ m[k+2] = (-sinY*cz + cy*sinX*sinZ) * scaleX;
1128
+ m[k+4] = (-cy*sinZ + sinY*sinX*cz) * scaleY; m[k+5] = cx*cz * scaleY;
1129
+ m[k+6] = (sinY*sinZ + cy*sinX*cz) * scaleY;
1130
+ m[k+8] = sinY*cx * scaleZ; m[k+9] = -sinX * scaleZ; m[k+10] = cy*cx * scaleZ;
1131
+ }
1132
+ else
1133
+ {
1134
+ m[k] = scaleX; m[k+1] = 0; m[k+2] = 0;
1135
+ m[k+4] = 0; m[k+5] = scaleY; m[k+6] = 0;
1136
+ m[k+8] = 0; m[k+9] = 0; m[k+10] = scaleZ;
1137
+ }
1138
+ m[k+3] = m[k+7] = m[k+11] = 0; m[k+15] = 1;
1139
+ m[k+12] = x[i], m[k+13] = y[i], m[k+14] = z[i];
1140
+ }
1141
+ count && this.markDirtyRange(start, end);
1142
+ }
1143
+
1144
+ /** Place an instance, in world space
1145
+ * @param {number} i
1146
+ * @param {Matrix4} matrix */
1147
+ setMatrixAt(i, matrix)
1148
+ {
1149
+ ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
1150
+ const data = this.matrixData, k = i * RENDER3D_MATRIX_FLOATS, m = matrix.m;
1151
+ for (let j = 0; j < 16; ++j)
1152
+ data[k+j] = m[j]; // for 16 floats a loop beats set's call
1153
+ this.markDirty(i);
1154
+ }
1155
+
1156
+ /** The matrix of an instance
1157
+ * @param {number} i
1158
+ * @return {Matrix4} */
1159
+ getMatrixAt(i)
1160
+ {
1161
+ ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
1162
+ const k = i * RENDER3D_MATRIX_FLOATS, matrix = new Matrix4;
1163
+ matrix.m.set(this.matrixData.subarray(k, k + 16));
1164
+ return matrix;
1165
+ }
1166
+
1167
+ /** Color an instance
1168
+ * @param {number} i
1169
+ * @param {Color} color */
1170
+ setColorAt(i, color)
1171
+ {
1172
+ ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
1173
+ ASSERT(isColor(color), 'color must be a Color');
1174
+ const data = this.colorData, k = i * RENDER3D_COLOR_FLOATS;
1175
+ data[k] = color.r; data[k+1] = color.g; data[k+2] = color.b; data[k+3] = color.a;
1176
+ this.markColorDirty(i);
1177
+ }
1178
+
1179
+ /** Note that an instance's matrix changed, so it uploads before the next draw and the bounds take it in when
1180
+ * they are read; setTransformAt and setMatrixAt call this, and so must an edit made straight to matrixData
1181
+ * @param {number} i */
1182
+ markDirty(i)
1183
+ {
1184
+ if (i < this.dirtyStart) this.dirtyStart = i;
1185
+ if (i >= this.dirtyEnd) this.dirtyEnd = i + 1;
1186
+ if (i < this.boundsStart) this.boundsStart = i;
1187
+ if (i >= this.boundsEnd) this.boundsEnd = i + 1;
1188
+ }
1189
+
1190
+ /** Note that a run of instances' matrices changed, for code that writes matrixData directly, the lowest level
1191
+ * way to move many: they upload before the next draw and the bounds take them in when they are read
1192
+ * @param {number} start - First instance
1193
+ * @param {number} end - One past the last */
1194
+ markDirtyRange(start, end)
1195
+ {
1196
+ ASSERT(start >= 0 && end <= this.maxCount && start <= end, 'instance range out of range');
1197
+ if (start < this.dirtyStart) this.dirtyStart = start;
1198
+ if (end > this.dirtyEnd) this.dirtyEnd = end;
1199
+ if (start < this.boundsStart) this.boundsStart = start;
1200
+ if (end > this.boundsEnd) this.boundsEnd = end;
1201
+ }
1202
+
1203
+ /** Note that an instance's color changed, so it uploads before the next draw; setColorAt calls this, and so must
1204
+ * an edit made straight to colorData
1205
+ * @param {number} i */
1206
+ markColorDirty(i)
1207
+ {
1208
+ if (i < this.colorDirtyStart) this.colorDirtyStart = i;
1209
+ if (i >= this.colorDirtyEnd) this.colorDirtyEnd = i + 1;
1210
+ }
1211
+
1212
+ /** Radius of the sphere around the origin that holds every instance set so far, for culling: the farthest
1213
+ * instance, and the mesh's size at the largest scale; it only grows, and takes in the instances set since it
1214
+ * was last read, so with frustum culling off it is never worked out. Set it to fix the sphere, which a big set
1215
+ * that is culled can use to skip that work, and set it to undefined to go back to the bounds
1216
+ * @return {number} */
1217
+ get radius()
1218
+ {
1219
+ if (this.fixedRadius !== undefined) return this.fixedRadius;
1220
+ if (this.boundsEnd > this.boundsStart)
1221
+ {
1222
+ // the bounds grow to hold where each is now and how big, read back from its matrix, squared
1223
+ const d = this.matrixData;
1224
+ let reachSquared = this.reachSquared, scaleSquared = this.scaleSquared;
1225
+ for (let i = this.boundsStart; i < this.boundsEnd; ++i)
1226
+ {
1227
+ const k = i * RENDER3D_MATRIX_FLOATS, x = d[k+12], y = d[k+13], z = d[k+14];
1228
+ const reach = x*x + y*y + z*z, scale = render3DMaxStretchSquared(d, k);
1229
+ if (reach > reachSquared) reachSquared = reach;
1230
+ if (scale > scaleSquared) scaleSquared = scale;
1231
+ }
1232
+ this.reachSquared = reachSquared, this.scaleSquared = scaleSquared;
1233
+ this.boundsStart = Infinity, this.boundsEnd = 0;
1234
+ }
1235
+ const mesh = this.mesh, meshRadius = mesh.radius || mesh.computeRadius(); // the mesh measured once
1236
+ return this.reachSquared ** .5 + meshRadius * this.scaleSquared ** .5;
1237
+ }
1238
+ set radius(radius) { this.fixedRadius = radius; }
1239
+
1240
+ /** Draws every instance as one call, uploading the ones that changed first
1241
+ * @return {void} */
1242
+ render3D()
1243
+ {
1244
+ const r = render3D, gl = glContext, mesh = this.mesh;
1245
+ ASSERT(this.count >= 0 && this.count <= this.maxCount, 'count must be within the count it was made with');
1246
+ if (r.transparentQueue) // sorted as one thing with the other transparent draws
1247
+ return r.queueTransparent(this.getWorldPos3D(), ()=> this.render3D());
1248
+ if (!mesh || !this.count || !render3DCanDraw()) return;
1249
+ if (r.shadowPass && !r.lighting) return; // unlit things cast no shadow
1250
+ render3DMeshUpload(mesh);
1251
+ if (!mesh.bufferCount) return;
1252
+ if (r.frustumCulling && !render3DSphereVisible(0, 0, 0, this.radius)) return;
1253
+
1254
+ // the uv rect is the object's tile for every instance, rewritten when the tile changes
1255
+ if (this.uvTileInfo !== this.tileInfo)
1256
+ {
1257
+ const uv = render3DGetTileUVs(this.tileInfo), data = this.colorData;
1258
+ for (let k = 4; k < data.length; k += RENDER3D_COLOR_FLOATS)
1259
+ data[k] = uv.x, data[k+1] = uv.y, data[k+2] = uv.w, data[k+3] = uv.h;
1260
+ this.uvTileInfo = this.tileInfo;
1261
+ this.colorDirtyStart = 0, this.colorDirtyEnd = this.maxCount;
1262
+ }
1263
+
1264
+ // the GPU copies: all of each under a fresh context, otherwise just the changed ranges
1265
+ if (!this.buffer || this.bufferGeneration !== r.contextGeneration)
1266
+ {
1267
+ this.bufferGeneration = r.contextGeneration;
1268
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer = gl.createBuffer());
1269
+ gl.bufferData(gl.ARRAY_BUFFER, this.matrixData, gl.DYNAMIC_DRAW);
1270
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer = gl.createBuffer());
1271
+ gl.bufferData(gl.ARRAY_BUFFER, this.colorData, gl.DYNAMIC_DRAW);
1272
+ }
1273
+ else
1274
+ {
1275
+ const upload = (buffer, data, floats, start, end)=>
1276
+ {
1277
+ if (end <= start) return;
1278
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
1279
+ gl.bufferSubData(gl.ARRAY_BUFFER, start * floats * 4, data, start * floats, (end - start) * floats);
1280
+ };
1281
+ upload(this.buffer, this.matrixData, RENDER3D_MATRIX_FLOATS, this.dirtyStart, this.dirtyEnd);
1282
+ upload(this.colorBuffer, this.colorData, RENDER3D_COLOR_FLOATS, this.colorDirtyStart, this.colorDirtyEnd);
1283
+ }
1284
+ this.dirtyStart = this.colorDirtyStart = Infinity, this.dirtyEnd = this.colorDirtyEnd = 0;
1285
+
1286
+ // one draw under the object's state, the mesh setting the culling as drawMesh does
1287
+ r.flush();
1288
+ render3DFlushBeforeOverlay(); // as in drawMesh, what was drawn before goes under it
1289
+ const cullBackFaces = r.cullBackFaces, tileInfo = this.tileInfo;
1290
+ r.cullBackFaces = !mesh.doubleSided;
1291
+ render3DDrawInstanced(mesh, this.buffer, this.count, render3DTextureOf(tileInfo), r, this.colorBuffer);
1292
+ r.cullBackFaces = cullBackFaces;
1293
+ }
1294
+
1295
+ /** Destroy the set and free its GPU buffer
1296
+ * @param {boolean} [immediate] */
1297
+ destroy(immediate)
1298
+ {
1299
+ if (this.buffer && this.bufferGeneration === render3D?.contextGeneration)
1300
+ glContext?.deleteBuffer(this.buffer), glContext?.deleteBuffer(this.colorBuffer);
1301
+ this.buffer = this.colorBuffer = undefined;
1302
+ super.destroy(immediate);
1303
+ }
1304
+ }
1305
+
1306
+ ///////////////////////////////////////////////////////////////////////////////
1307
+ /**
1308
+ * Light3D - A light that is an EngineObject3D, so it can move, follow a parent or be destroyed like anything else
1309
+ * - A point light: it lights what is near it and fades out by its radius, DirectionalLight3D shines from far away
1310
+ * - A coneAngle makes it a spotlight: it shines along its own forward, turned by rotation3D or by what it is
1311
+ * attached to, inside the cone, fading over the part of it coneSoftness says
1312
+ * - The sun, render3D.sunDirection, casts the shadows; a spotlight can cast them in its place, see
1313
+ * render3D.shadowLight; the other lights light and make highlights without one
1314
+ * - Only the 8 lights nearest the camera are used each frame
1315
+ * - radius is where the light fades out, and it fades fast, so a small radius wants a higher intensity
1316
+ * - intensity multiplies the color, above 1 for a light brighter than white
1317
+ * - radius is a world distance, so scale3D does not change it
1318
+ * - An alpha, an intensity or a radius of 0 switches it off, and a light that is off takes none of those slots
1319
+ * - Draws nothing but its glow, when it has one; add a small emissive mesh if the lamp itself should be seen
1320
+ * - addFlare gives it a lens flare, see LensFlare3D
1321
+ * @extends EngineObject3D
1322
+ * @memberof Render3D
1323
+ * @example
1324
+ * const torch = new Light3D(vec3(0, 3, 0), 10, hsl(.1, 1, .65));
1325
+ * torch.addFlare(); // light in the lens when the torch is in view
1326
+ */
1327
+ class Light3D extends EngineObject3D
1328
+ {
1329
+ /** Create a point light
1330
+ * @param {Vector3} [pos3D] - Where it is
1331
+ * @param {number} [radius] - Distance where the light fades to nothing
1332
+ * @param {Color} [color] - Light color, its alpha fades it
1333
+ * @param {number} [intensity] - Brightness, multiplies the color, above 1 is brighter than white */
1334
+ constructor(pos3D=vec3(), radius=5, color=WHITE, intensity=1)
1335
+ {
1336
+ super(pos3D, undefined, undefined, color);
1337
+ ASSERT(radius >= 0, 'light radius cannot be negative, 0 is an off switch like an alpha of 0');
1338
+ ASSERT(intensity >= 0, 'light intensity cannot be negative, 0 is an off switch');
1339
+ this.size3D = vec3(); // not a solid thing to pick or collect
1340
+ /** @property {number} - Distance where the light fades to nothing */
1341
+ this.radius = radius;
1342
+ /** @property {number} - Brightness, multiplies the color, above 1 is brighter than white */
1343
+ this.intensity = intensity;
1344
+ /** @property {boolean} - Shine from far away, from its position toward the origin, instead of out from its
1345
+ * position with a falloff; DirectionalLight3D sets it */
1346
+ this.directional = false;
1347
+ /** @property {number} - Makes it a spotlight: the angle in radians from its forward out to the edge of its
1348
+ * cone, so the beam is twice this across; 0 for a light that shines every way */
1349
+ this.coneAngle = 0;
1350
+ /** @property {number} - How much of the cone is its fading edge: 0 a hard edge, .2 by default, the outer
1351
+ * fifth, 1 fading all the way from the middle of the beam */
1352
+ this.coneSoftness = .2;
1353
+ /** @property {number} - Size of a soft hazy glow drawn over the light, like a lamp at night, 0 for none; it
1354
+ * is added onto what is behind it, and what is in front of the light hides it */
1355
+ this.glow = 0;
1356
+ /** @property {number} - How fast the glow fades from its middle: 1 by default, .5 a wide haze, 2 a tight
1357
+ * bright core */
1358
+ this.glowFalloff = 1;
1359
+ /** @property {TextureInfo|TileInfo|undefined} - A gel: a picture the light shines through, like a stained glass
1360
+ * window or the leaves of a tree, cast along its cone in its colors, upright as the light looks out, the whole
1361
+ * texture of a TileInfo; only the spotlight that casts the shadows has one, with render3D.shadows on and it
1362
+ * as render3D.shadowLight, and only on what takes its shadows: an object with receiveShadow off is lit
1363
+ * without the gel; its alpha is not read, see through panes are dark
1364
+ * @type {TextureInfo|TileInfo|undefined} */
1365
+ this.gel = undefined;
1366
+ this.additive = true; // the glow is added on, in the transparent stage; a light with none draws nothing
1367
+ /** @type {LensFlare3D|undefined} */
1368
+ this.flareObject = undefined;
1369
+ }
1370
+
1371
+ /** Give the light a lens flare, made with the arguments of LensFlare3D, in place of the one it had
1372
+ * @param {number} [size] - Scales every part of it
1373
+ * @param {number} [count] - How many ghosts there are, besides the glow at the light
1374
+ * @param {number} [intensity] - How bright it is
1375
+ * @param {number} [saturation] - How colorful the ghosts are
1376
+ * @param {Color} [color] - Tints the flare, with the light's own color
1377
+ * @return {LensFlare3D} - The flare, to change: light.addFlare().shapes = ['hex'] */
1378
+ addFlare(size, count, intensity, saturation, color)
1379
+ {
1380
+ const flare = new LensFlare3D(size, count, intensity, saturation, color);
1381
+ this.flare = flare;
1382
+ return flare;
1383
+ }
1384
+
1385
+ /** The light's lens flare, undefined for none: addFlare makes it, or set a LensFlare3D of your own, and
1386
+ * destroying the flare takes it away
1387
+ * - The flare is the light's: it is attached to the light as its child, so it stays through a scene change
1388
+ * when the light does, and is destroyed with the light or when another takes its place
1389
+ * @type {LensFlare3D|undefined} */
1390
+ get flare() { return this.flareObject && !this.flareObject.destroyed ? this.flareObject : undefined; }
1391
+
1392
+ set flare(flare)
1393
+ {
1394
+ const old = this.flare; // a flare that was destroyed on its own is no flare
1395
+ if (flare === old) return;
1396
+ old && old.destroy();
1397
+ if (flare)
1398
+ {
1399
+ flare.light = this;
1400
+ flare.parent || this.addChild(flare);
1401
+ }
1402
+ this.flareObject = flare || undefined;
1403
+ }
1404
+
1405
+ /** Destroy the light, and its flare with it
1406
+ * @param {boolean} [immediate] */
1407
+ destroy(immediate)
1408
+ {
1409
+ this.flare = undefined;
1410
+ super.destroy(immediate);
1411
+ }
1412
+
1413
+ /** Draw the glow, a quad facing the camera with a soft round glow on it, pulled toward the camera by half its
1414
+ * size so a lamp at the light does not cut into it, but never past the camera */
1415
+ render3D()
1416
+ {
1417
+ if (!(this.glow > 0) || this.directional) return;
1418
+ const r = render3D, c = this.color, pos = render3DObjectMatrix(this).getTranslation();
1419
+ const toCamera = r.camera.pos.subtract(pos), distance = toCamera.length();
1420
+ const at = distance ? pos.add(toCamera.scale(min(this.glow, distance) / 2 / distance)) : pos;
1421
+ const color = rgb(c.r, c.g, c.b, c.a * min(this.intensity, 1)), texture = engineGlowTexture(this.glowFalloff);
1422
+ // a browser that can not make the glow's texture draws no glow, not a plain square
1423
+ if (texture || !glContext)
1424
+ r.drawBillboard(at, vec2(this.glow), texture, color);
1425
+ }
1426
+ }
1427
+
1428
+ ///////////////////////////////////////////////////////////////////////////////
1429
+ /**
1430
+ * DirectionalLight3D - A Light3D that shines from far away with no falloff, like sunlight
1431
+ * - It shines from its position toward the origin, like a three.js DirectionalLight: only the direction to it
1432
+ * counts, so moving it or its parent swings the light around; parent it to a sun in the sky and it follows
1433
+ * - It cannot sit on the origin, since that leaves no direction
1434
+ * - It casts no shadow: the sun, render3D.sunDirection, does, or a spotlight set as render3D.shadowLight
1435
+ * @extends Light3D
1436
+ * @memberof Render3D
1437
+ * @example
1438
+ * const fill = new DirectionalLight3D(vec3(-1, 1, 1), hsl(.6, .5, .3)); // from the back left and above
1439
+ */
1440
+ class DirectionalLight3D extends Light3D
1441
+ {
1442
+ /** Create a directional light
1443
+ * @param {Vector3} [pos3D] - Where it shines from, toward the origin
1444
+ * @param {Color} [color] - Light color, its alpha fades it
1445
+ * @param {number} [intensity] - Brightness, multiplies the color, above 1 is brighter than white */
1446
+ constructor(pos3D=vec3(0, 1, 0), color=WHITE, intensity=1)
1447
+ {
1448
+ super(pos3D, 0, color, intensity);
1449
+ this.directional = true;
1450
+ }
1451
+ }