littlejsengine 1.18.29 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/FAQ.md +841 -815
  2. package/README.md +211 -199
  3. package/dist/littlejs.d.ts +4383 -812
  4. package/dist/littlejs.esm.js +31848 -16680
  5. package/dist/littlejs.esm.min.js +14 -1
  6. package/dist/littlejs.js +31230 -16166
  7. package/dist/littlejs.min.js +13 -1
  8. package/dist/littlejs.release.js +27419 -15457
  9. package/package.json +10 -5
  10. package/plugins/audioEffects.js +375 -0
  11. package/plugins/box2d.js +2516 -2073
  12. package/plugins/drawUtilities.js +206 -31
  13. package/plugins/gltf.js +740 -0
  14. package/plugins/jsconfig.json +19 -0
  15. package/plugins/lightSystem.js +719 -330
  16. package/plugins/math3d.js +911 -0
  17. package/plugins/medalSystem.js +118 -72
  18. package/plugins/newgrounds.js +400 -199
  19. package/plugins/pathFinder.js +208 -175
  20. package/plugins/pluginExport.js +196 -115
  21. package/plugins/postProcess.js +284 -166
  22. package/plugins/render3d.js +4072 -0
  23. package/plugins/render3dDebug.js +109 -0
  24. package/plugins/render3dExtras.js +1754 -0
  25. package/plugins/render3dVoxels.js +502 -0
  26. package/plugins/textureSheet.js +86 -48
  27. package/plugins/threejs.js +183 -158
  28. package/plugins/tweakables.js +411 -0
  29. package/plugins/tweenSystem.js +646 -526
  30. package/plugins/uiSystem.js +2024 -1687
  31. package/plugins/zzfxm.js +8 -15
  32. package/src/engine.js +345 -194
  33. package/src/engineAudio.js +1022 -777
  34. package/src/engineBuild.mjs +289 -253
  35. package/src/engineDebug.js +912 -735
  36. package/src/engineDraw.js +1774 -1510
  37. package/src/engineEditor.js +2431 -0
  38. package/src/engineExport.js +419 -396
  39. package/src/engineInput.js +1462 -1293
  40. package/src/engineLogo.js +4 -3
  41. package/src/engineMath.js +1274 -1183
  42. package/src/engineObject.js +674 -551
  43. package/src/engineParticles.js +583 -532
  44. package/src/engineRelease.js +67 -45
  45. package/src/engineSettings.js +808 -726
  46. package/src/engineTileLayer.js +995 -741
  47. package/src/engineUtilities.js +32 -13
  48. package/src/engineWebGL.js +1162 -941
  49. package/src/jsconfig.json +17 -13
  50. package/plugins/box2d.wasm.js +0 -630
  51. package/plugins/box2d.wasm.wasm +0 -0
@@ -0,0 +1,4072 @@
1
+ /**
2
+ * LittleJS 3D Rendering Plugin
3
+ * - Adds a 3D scene that draws into the same WebGL canvas as the 2D game
4
+ * - Call new Render3DPlugin() in gameInit, then move render3D.camera and make EngineObject3D objects
5
+ * - EngineObject3D is an EngineObject with a 3D position, rotation and mesh
6
+ * - The 3D scene draws under the 2D sprites, so HUD and text land on top
7
+ * - Lighting is the sun plus ambient, with optional extra lights, fog and shadows
8
+ * - Any object or draw can bring its own Shader, a mainImage snippet the lighting then applies to
9
+ * - Build shapes with buildBox, buildSphere, buildGrid and buildLathe; the other builders, terrain, particles,
10
+ * camera controls and the OBJ loader are in the Render3D Extras plugin, which goes after this one
11
+ * - Requires the Math3D plugin
12
+ * @namespace Render3D
13
+ */
14
+
15
+ 'use strict';
16
+
17
+ ///////////////////////////////////////////////////////////////////////////////
18
+
19
+ /** Global Render3D plugin object
20
+ * @type {Render3DPlugin}
21
+ * @memberof Render3D */
22
+ let render3D;
23
+
24
+ // vertex format: position xyz, normal xyz, uv, rgba bytes
25
+ const RENDER3D_VERTEX_FLOATS = 9;
26
+ const RENDER3D_VERTEX_BYTES = RENDER3D_VERTEX_FLOATS * 4;
27
+
28
+ // per draw values the shaders read as vertex attributes: the model matrix columns (4-7), the tint (11) and the
29
+ // uv rect (12); constants for one draw, one per instance for a batch; the shader derives the normal matrix
30
+ const RENDER3D_INSTANCE_FLOATS = 24;
31
+ const RENDER3D_INSTANCE_BYTES = RENDER3D_INSTANCE_FLOATS * 4;
32
+ const RENDER3D_INSTANCE_ATTRIBS = [[4, 4, 0], [5, 4, 16], [6, 4, 32], [7, 4, 48], [11, 4, 64], [12, 4, 80]];
33
+ const RENDER3D_VERTEX_INPUTS =
34
+ 'layout(location=0) in vec3 p;layout(location=1) in vec3 n;layout(location=2) in vec2 t;layout(location=3) in vec4 c;' +
35
+ 'layout(location=4) in vec4 m0;layout(location=5) in vec4 m1;layout(location=6) in vec4 m2;layout(location=7) in vec4 m3;' +
36
+ 'layout(location=11) in vec4 tint;layout(location=12) in vec4 uvRect;';
37
+ const RENDER3D_MAX_STREAM_VERTS = 32768;
38
+ const RENDER3D_MAX_LIGHTS = 8; // Light3D objects per frame, the shader loops over this many
39
+ // strip order, frozen, and typed as the plain array the uv parameters take
40
+ const RENDER3D_QUAD_UVS = /** @type {Array<Vector2>} */
41
+ (Object.freeze([vec2(0, 0), vec2(0, 1), vec2(1, 0), vec2(1, 1)].map(uv=> Object.freeze(uv))));
42
+ const RENDER3D_FULL_UV_RECT = Object.freeze({x:0, y:0, w:1, h:1});
43
+ const RENDER3D_DEFAULT_NORMAL = Object.freeze(vec3(0, 1, 0));
44
+ const RENDER3D_DEFAULT_UV = Object.freeze(vec2());
45
+ const RENDER3D_SHADOW_COLOR = Object.freeze(hsl(0, 0, 0, .5));
46
+ const RENDER3D_IDENTITY = new Matrix4; // never modified
47
+ let render3DShadowCut; // the cut last sent to the shadow shader, see render3DSetDrawUniforms
48
+ ///////////////////////////////////////////////////////////////////////////////
49
+ // Private helpers
50
+
51
+ // outward normal of a triangle or a quad given its corners in loop order,
52
+ // from the diagonals so a collapsed corner still works
53
+ function render3DFaceNormal(a, b, c, d=a)
54
+ {
55
+ const n = c.subtract(a).cross(d.subtract(b));
56
+ return n.lengthSquared() ? n.normalize() : RENDER3D_DEFAULT_NORMAL;
57
+ }
58
+
59
+ // a quad's corners in loop order as a strip, the one place that knows the order
60
+ function render3DQuadStrip(a, b, c, d) { return [a, b, d, c]; }
61
+
62
+ // per corner values (colors, uvs) into strip order, a single value passes through
63
+ function render3DQuadValues(v) { return isArray(v) ? render3DQuadStrip(...v) : v; }
64
+
65
+ // 3D draws are only valid during the pass with a live shader
66
+ function render3DCanDraw()
67
+ {
68
+ if (!render3D.program) return false;
69
+ ASSERT(render3D.isRendering,
70
+ '3D draws are only valid during the 3D pass, draw from an EngineObject3D or render3D.onRenderOpaque');
71
+ return render3D.isRendering;
72
+ }
73
+
74
+ // the draw state fields a batch is drawn under; lights and fog are not captured, they are read live at flush
75
+ // the three functions below write them out by hand for speed, so a new field goes in all four
76
+ const RENDER3D_STATE_FIELDS = ['blend', 'additive', 'depthTest', 'depthWrite', 'cullBackFaces', 'mirrored', 'lighting',
77
+ 'emissive', 'receiveShadow', 'specular', 'pixelated', 'shader'];
78
+
79
+ // a copy of the draw state in one fixed shape, the fields of RENDER3D_STATE_FIELDS written out so the
80
+ // compare below stays a handful of direct reads, it runs for every instance drawn
81
+ function render3DCaptureBatchState()
82
+ {
83
+ const r = render3D;
84
+ return {blend: r.blend, additive: r.additive, depthTest: r.depthTest, depthWrite: r.depthWrite,
85
+ cullBackFaces: r.cullBackFaces, mirrored: r.mirrored, lighting: r.lighting, emissive: r.emissive,
86
+ receiveShadow: r.receiveShadow, specular: r.specular, pixelated: r.pixelated, shader: r.shader};
87
+ }
88
+
89
+ // put a captured draw state back, written out the same way; the transparent stage does this for every queued draw
90
+ function render3DApplyBatchState(s)
91
+ {
92
+ const r = render3D;
93
+ r.blend = s.blend, r.additive = s.additive, r.depthTest = s.depthTest, r.depthWrite = s.depthWrite,
94
+ r.cullBackFaces = s.cullBackFaces, r.mirrored = s.mirrored, r.lighting = s.lighting, r.emissive = s.emissive,
95
+ r.receiveShadow = s.receiveShadow, r.specular = s.specular, r.pixelated = s.pixelated, r.shader = s.shader;
96
+ }
97
+
98
+ // true when the current draw state differs from a captured one, so a pending batch must flush first
99
+ function render3DStateChanged(s)
100
+ {
101
+ const r = render3D;
102
+ return r.blend !== s.blend || r.additive !== s.additive || r.depthTest !== s.depthTest
103
+ || r.depthWrite !== s.depthWrite || r.cullBackFaces !== s.cullBackFaces || r.mirrored !== s.mirrored
104
+ || r.lighting !== s.lighting || r.emissive !== s.emissive || r.receiveShadow !== s.receiveShadow
105
+ || r.specular !== s.specular || r.pixelated !== s.pixelated || r.shader !== s.shader;
106
+ }
107
+
108
+ // whether a sphere is inside the view, or the shadow map's box during the shadow pass, without a vector
109
+ function render3DSphereVisible(x, y, z, radius)
110
+ {
111
+ const planes = render3D.shadowPass ? render3D.shadowPlanes : render3D.frustumPlanes;
112
+ for (let i = 0; i < planes.length; ++i)
113
+ {
114
+ const p = planes[i];
115
+ if (p[0]*x + p[1]*y + p[2]*z + p[3] < -radius)
116
+ return false;
117
+ }
118
+ return true;
119
+ }
120
+
121
+ // run a function with some draw state fields overridden, restored afterward even on a throw
122
+ function render3DWithState(fields, fn)
123
+ {
124
+ const r = render3D, saved = {};
125
+ for (const key in fields)
126
+ saved[key] = r[key], r[key] = fields[key];
127
+ try { return fn(); }
128
+ finally { Object.assign(r, saved); }
129
+ }
130
+
131
+ // which side of the 2D scene an object draws on, its own flag or the plugin default
132
+ function render3DIsAfter2D(o) { return !!(o.renderAfter2D ?? render3D.renderAfter2D); }
133
+
134
+ // a size given as a number or a vec3
135
+ /** @param {Vector3|number} size
136
+ * @return {Vector3} */
137
+ function render3DSize3(size)
138
+ { return isNumber(size) ? vec3(/** @type {number} */ (size)) : /** @type {Vector3} */ (size); }
139
+
140
+ // a size given as a number or a vec2
141
+ /** @param {Vector2|number} size
142
+ * @return {Vector2} */
143
+ function render3DSize2(size)
144
+ { return isNumber(size) ? vec2(/** @type {number} */ (size)) : /** @type {Vector2} */ (size); }
145
+
146
+ // a transform given as a matrix, or as a vec3 for one that only moves there
147
+ /** @param {Matrix4|Vector3} matrix
148
+ * @return {Matrix4} */
149
+ function render3DMatrix(matrix)
150
+ {
151
+ if (matrix instanceof Vector3)
152
+ return buildMatrix(matrix);
153
+ ASSERT(matrix instanceof Matrix4, 'takes a Matrix4, or a Vector3 for a position');
154
+ return matrix;
155
+ }
156
+
157
+ // the matrix that keeps normals pointing out when an object is scaled unevenly
158
+ function render3DNormalMatrix(matrix) { return matrix.copy().invert().transpose(); }
159
+
160
+ // whether a matrix mirrors, its determinant negative, so what it moves reads the other way round
161
+ function render3DMirrors(matrix) { return matrix.determinant() < 0; }
162
+
163
+
164
+ // a column of a matrix as a direction: 0 is the right axis, 4 up, 8 back
165
+ function render3DAxis(m, i) { return vec3(m[i], m[i+1], m[i+2]); }
166
+
167
+ // a matrix turning by angle around a unit axis through the origin, counter clockwise when the axis points at you
168
+ function render3DAxisRotation(axis, angle)
169
+ {
170
+ const x = vec3(1, 0, 0).rotate(axis, angle), y = vec3(0, 1, 0).rotate(axis, angle), z = vec3(0, 0, 1).rotate(axis, angle);
171
+ return new Matrix4([x.x, x.y, x.z, 0, y.x, y.y, y.z, 0, z.x, z.y, z.z, 0, 0, 0, 0, 1]);
172
+ }
173
+
174
+ // the largest axis scale of a matrix, how much it grows a bounding sphere
175
+ function render3DMaxScale(m)
176
+ {
177
+ return max(m[0]*m[0] + m[1]*m[1] + m[2]*m[2], m[4]*m[4] + m[5]*m[5] + m[6]*m[6], m[8]*m[8] + m[9]*m[9] + m[10]*m[10]) ** .5;
178
+ }
179
+
180
+ // how far a matrix at offset k can move a point that is one unit from its origin, for a bounding sphere: the
181
+ // longest axis when the axes are square to each other, more when they are not, as a turned child under a parent
182
+ // scaled on one axis leaves them; the axes' dot products bound the largest stretch by their largest row sum
183
+ function render3DMaxStretch(m, k=0)
184
+ {
185
+ const xx = m[k]*m[k] + m[k+1]*m[k+1] + m[k+2]*m[k+2];
186
+ const yy = m[k+4]*m[k+4] + m[k+5]*m[k+5] + m[k+6]*m[k+6];
187
+ const zz = m[k+8]*m[k+8] + m[k+9]*m[k+9] + m[k+10]*m[k+10];
188
+ const xy = abs(m[k]*m[k+4] + m[k+1]*m[k+5] + m[k+2]*m[k+6]);
189
+ const xz = abs(m[k]*m[k+8] + m[k+1]*m[k+9] + m[k+2]*m[k+10]);
190
+ const yz = abs(m[k+4]*m[k+8] + m[k+5]*m[k+9] + m[k+6]*m[k+10]);
191
+ return max(xx + xy + xz, yy + xy + yz, zz + xz + yz) ** .5;
192
+ }
193
+
194
+ // a quad as a strip from its center and half axes, the same corner order as render3DQuadStrip
195
+ function render3DQuadAxes(center, right, up)
196
+ {
197
+ return [center.subtract(right).add(up), center.subtract(right).subtract(up),
198
+ center.add(right).add(up), center.add(right).subtract(up)];
199
+ }
200
+
201
+ // set the draw state for an object's render3D, or the defaults for the stage callbacks
202
+ function render3DSetObjectState(o)
203
+ {
204
+ const r = render3D;
205
+ const emissive = o?.emissive || 0;
206
+ ASSERT(isNumber(emissive) && emissive >= 0, 'emissive must be a number, 0 or more', emissive);
207
+ r.lighting = true;
208
+ r.emissive = emissive;
209
+ r.additive = !!o?.additive;
210
+ r.specular = o?.specular || 0;
211
+ r.receiveShadow = !o || o.receiveShadow;
212
+ r.cullBackFaces = r.mirrored = false; // each mesh sets these as it draws
213
+ r.pixelated = !!o?.pixelated;
214
+ ASSERT(!o?.shader || o.shader instanceof Shader, 'shader must be a Shader, not the snippet itself');
215
+ r.shader = o?.shader || undefined; // null is no shader too, so it batches with none
216
+ r.depthTest = true;
217
+ if (r.shadowPass)
218
+ r.blend = !!o?.transparent; // the depth shader cuts a see through caster by the alpha it would blend with
219
+ }
220
+
221
+ // draw objects each with the draw state set from its own flags, then reset to the defaults
222
+ function render3DDrawObjects(objects)
223
+ {
224
+ for (const o of objects)
225
+ {
226
+ render3DSetObjectState(o);
227
+ o.render3D();
228
+ }
229
+ render3DSetObjectState();
230
+ }
231
+
232
+ // add a draw of a mesh to its batch; a batch is one mesh under one texture and draw state, so a change flushes it
233
+ function render3DInstance(mesh, matrix, tileInfo, color)
234
+ {
235
+ const k = render3DInstanceSlot(mesh, render3DTextureOf(tileInfo)), data = mesh.instanceData;
236
+ data.set(matrix.m, k);
237
+ // the tint; this batch is never blended, so in the shadow pass the depth shader cuts it by its texture alpha
238
+ // alone, a see through caster draws blended instead and is cut by its tint alpha too
239
+ data[k+16] = color.r, data[k+17] = color.g, data[k+18] = color.b, data[k+19] = color.a;
240
+ const uv = render3DGetTileUVs(tileInfo);
241
+ data[k+20] = uv.x; data[k+21] = uv.y; data[k+22] = uv.w; data[k+23] = uv.h;
242
+ }
243
+
244
+ // make room for one more instance of a mesh under a texture and the current draw state, flushing a batch that
245
+ // differs first, and return where its 24 floats go in mesh.instanceData: the matrix, the tint and the uv rect
246
+ function render3DInstanceSlot(mesh, textureInfo)
247
+ {
248
+ const r = render3D;
249
+ if (mesh.instanceCount && (mesh.instanceTextureInfo !== textureInfo || render3DStateChanged(mesh.instanceState)))
250
+ render3DFlushInstances(mesh);
251
+ if (!mesh.instanceCount)
252
+ {
253
+ mesh.instanceTextureInfo = textureInfo;
254
+ mesh.instanceState = render3DCaptureBatchState();
255
+ r.instanceMeshes.push(mesh);
256
+ }
257
+
258
+ // room for one more, doubling as the batch grows
259
+ const data = mesh.instanceData, k = mesh.instanceCount++ * RENDER3D_INSTANCE_FLOATS;
260
+ if (!data || data.length < k + RENDER3D_INSTANCE_FLOATS)
261
+ {
262
+ const grown = new Float32Array(max(64 * RENDER3D_INSTANCE_FLOATS, data ? data.length * 2 : 0));
263
+ data && grown.set(data);
264
+ mesh.instanceData = grown;
265
+ }
266
+ return k;
267
+ }
268
+
269
+ // draw the pending batches, or just one mesh's, each as a single instanced call
270
+ function render3DFlushInstances(only)
271
+ {
272
+ const r = render3D, gl = glContext;
273
+ for (const mesh of only ? [only] : r.instanceMeshes)
274
+ {
275
+ const count = mesh.instanceCount;
276
+ mesh.instanceCount = 0;
277
+ if (!count || !mesh.buffer) continue;
278
+
279
+ // the per instance values on top of the constant attributes, then the mesh under them
280
+ // a ring of buffers with fresh storage each time, so the driver never waits for a draw still reading one
281
+ const buffers = r.instanceBuffers, buffer = buffers[r.instanceBufferIndex = (r.instanceBufferIndex + 1) % buffers.length];
282
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
283
+ gl.bufferData(gl.ARRAY_BUFFER, mesh.instanceData, gl.DYNAMIC_DRAW, 0, count * RENDER3D_INSTANCE_FLOATS);
284
+ render3DDrawInstanced(mesh, buffer, count, mesh.instanceTextureInfo, mesh.instanceState);
285
+ }
286
+ if (!only)
287
+ r.instanceMeshes.length = 0;
288
+ else
289
+ {
290
+ const i = r.instanceMeshes.indexOf(only);
291
+ i < 0 || r.instanceMeshes.splice(i, 1);
292
+ }
293
+ }
294
+
295
+ // draw a mesh count times from a buffer that holds the per instance values, under a draw state
296
+ // the arrays are turned on with their instance divisor for this one call and both are turned off after: a single
297
+ // draw reads these slots as constant attributes, and in Firefox a draw that reads a constant through a slot whose
298
+ // divisor is set leaves the next batch on that slot reading the wrong values
299
+ function render3DDrawInstanced(mesh, buffer, count, textureInfo, state)
300
+ {
301
+ const gl = glContext;
302
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
303
+ for (const [location, size, offset] of RENDER3D_INSTANCE_ATTRIBS)
304
+ {
305
+ gl.vertexAttribPointer(location, size, gl.FLOAT, false, RENDER3D_INSTANCE_BYTES, offset);
306
+ gl.enableVertexAttribArray(location);
307
+ gl.vertexAttribDivisor(location, 1);
308
+ }
309
+ render3DSetDrawUniforms(RENDER3D_IDENTITY, textureInfo, WHITE, RENDER3D_FULL_UV_RECT, state);
310
+ render3DBindMesh(mesh);
311
+ gl.drawElementsInstanced(gl.TRIANGLES, mesh.bufferCount, mesh.indexType, 0, count);
312
+ for (const [location] of RENDER3D_INSTANCE_ATTRIBS)
313
+ {
314
+ gl.disableVertexAttribArray(location);
315
+ gl.vertexAttribDivisor(location, 0);
316
+ }
317
+ ++drawCount;
318
+ primitiveCount += mesh.bufferCount / 3 * count;
319
+ }
320
+
321
+ // forget the pending batches, for a frame that threw or a lost context
322
+ function render3DClearInstances()
323
+ {
324
+ for (const mesh of render3D.instanceMeshes)
325
+ mesh.instanceCount = 0;
326
+ render3D.instanceMeshes.length = 0;
327
+ }
328
+
329
+ // the live objects drawn on one side of the 2D scene
330
+ function render3DLayerObjects(after2D)
331
+ {
332
+ return /** @type {Array<EngineObject3D>} */ (engineObjects.filter(o=>
333
+ !o.destroyed && o instanceof EngineObject3D && render3DIsAfter2D(o) === after2D));
334
+ }
335
+
336
+ // the Light3D objects the shader gets this frame: directional lights light the whole scene so they come first,
337
+ // then the point lights nearest the camera
338
+ function render3DCollectLights()
339
+ {
340
+ // a light switched off by its radius or its alpha is left out, so it cannot take one of the few slots
341
+ const lights = /** @type {Array<Light3D>} */ (engineObjects.filter(o=> !o.destroyed && o instanceof Light3D &&
342
+ o.color.a > 0 && o.intensity > 0 && (o.directional || o.radius > 0)));
343
+ if (lights.length > RENDER3D_MAX_LIGHTS)
344
+ {
345
+ // distances cached once, getWorldPos3D walks the parent chain and the sort asks many times
346
+ const cameraPos = render3D.camera.pos, distances = new Map;
347
+ for (const light of lights)
348
+ distances.set(light, light.directional ? -1 : light.getWorldPos3D().distanceSquared(cameraPos));
349
+ lights.sort((a, b)=> distances.get(a) - distances.get(b));
350
+ lights.length = RENDER3D_MAX_LIGHTS;
351
+ }
352
+ return lights;
353
+ }
354
+
355
+ // unit circle directions for a number of sides, [cos, sin, cos, sin, ...] including the closing point, cached
356
+ const render3DCircleCache = new Map;
357
+ function render3DCircle(sides)
358
+ {
359
+ sides |= 0;
360
+ let circle = render3DCircleCache.get(sides);
361
+ if (!circle)
362
+ {
363
+ circle = new Float32Array(sides * 2 + 2);
364
+ for (let i = 0; i <= sides; ++i)
365
+ {
366
+ const a = i / sides * 2 * PI;
367
+ circle[i*2] = cos(a), circle[i*2 + 1] = sin(a);
368
+ }
369
+ render3DCircleCache.set(sides, circle);
370
+ }
371
+ return circle;
372
+ }
373
+
374
+ // the rotation that points -Z along a direction, as vec3(pitch, yaw, 0); a zero direction keeps the current one
375
+ function render3DLookRotation(direction, current)
376
+ {
377
+ const d = direction.normalize();
378
+ if (!d.lengthSquared()) return current;
379
+ if (abs(d.x) + abs(d.z) < 1e-9) // straight up or down has no yaw of its own
380
+ return vec3(d.y > 0 ? PI / 2 : -PI / 2, current.y, 0);
381
+ return vec3(Math.asin(clamp(d.y, -1, 1)), atan2(-d.x, -d.z), 0);
382
+ }
383
+
384
+ ///////////////////////////////////////////////////////////////////////////////
385
+ /**
386
+ * Render3D Plugin - The 3D renderer, camera, lights, shadows and fog
387
+ * - There is one of these, in the global render3D
388
+ * - It draws the 3D scene before gameRender, so 2D drawing lands on top
389
+ * - Set renderAfter2D to draw the 3D scene over the 2D scene instead
390
+ * - Settings like lighting and specular are read as each thing draws
391
+ * - Every object sets them from its own flags, so you rarely touch them
392
+ * @memberof Render3D
393
+ * @example
394
+ * new Render3DPlugin;
395
+ * render3D.camera.pos = vec3(0, 5, 10);
396
+ * render3D.camera.lookAt(vec3());
397
+ * new EngineObject3D(vec3(), buildBox());
398
+ */
399
+ class Render3DPlugin
400
+ {
401
+ /** Create the global 3D renderer, call in gameInit */
402
+ constructor()
403
+ {
404
+ ASSERT(!render3D, 'Render3D plugin already initialized');
405
+ render3D = this;
406
+ ASSERT(Object.keys(render3DCaptureBatchState()).join() === RENDER3D_STATE_FIELDS.join(),
407
+ 'the batch state functions must list RENDER3D_STATE_FIELDS');
408
+
409
+ /** @property {Camera3D} - The camera */
410
+ this.camera = new Camera3D;
411
+
412
+ // lights and fog
413
+ /** @property {Vector3} - Direction toward the sun, where its light comes from, like a directional Light3D;
414
+ * read at each draw, and any length will do, the shading and the shadows normalize it themselves;
415
+ * the sun is the one light that casts shadows */
416
+ this.sunDirection = vec3(-.3, 1, .5);
417
+ /** @property {Color} - Sunlight color */
418
+ this.sunColor = WHITE.copy();
419
+ /** @property {Color} - Ambient light color, from above when ambientGroundColor is set */
420
+ this.ambientColor = hsl(0, 0, .3);
421
+ /** @property {Color|undefined} - Ambient light from below: set, the ambient blends from this on faces pointing
422
+ * down to ambientColor on faces pointing up, the way a sky and a ground light a scene; setSky sets both from
423
+ * its colors
424
+ * @type {Color|undefined} */
425
+ this.ambientGroundColor = undefined;
426
+ /** @property {Color|undefined} - Fog color, uses canvasClearColor when undefined
427
+ * @type {Color|undefined} */
428
+ this.fogColor = undefined;
429
+ /** @property {number} - Distance from the camera where fog starts */
430
+ this.fogStart = 0;
431
+ /** @property {number} - Distance from the camera where fog is total, 0 disables fog */
432
+ this.fogEnd = 0;
433
+ /** @property {Vector3} - Added to the velocity3D of every object with a mass each frame, scaled by its
434
+ * gravityScale; sync2D objects use the 2D gravity */
435
+ this.gravity = vec3();
436
+ /** @property {number|HeightMap|function(number, number): number} - Floor for objects with a softShadow: a
437
+ * height, a HeightMap, or (x, z) => y
438
+ * @type {number|HeightMap|function(number, number): number} */
439
+ this.softShadowHeight = 0;
440
+ /** @property {boolean} - Default for every builder's smooth argument: true for smooth vertex normals, false for
441
+ * flat faces */
442
+ this.smoothShading = false;
443
+
444
+ // shadows
445
+ /** @property {boolean} - Cast real shadows from the sun, off by default and free when off */
446
+ this.shadows = false;
447
+ /** @property {number} - Size of the shadow map in pixels, bigger is sharper and slower */
448
+ this.shadowMapSize = 1024;
449
+ /** @property {number} - World size the shadow map covers around shadowCenter, smaller is sharper; it is a
450
+ * square facing the light, so it turns as the light does, and about 1.5 times an area's width covers it from
451
+ * any angle */
452
+ this.shadowRange = 40;
453
+ /** @property {Vector3|undefined} - Center of the shadowed area, read each frame, undefined follows the camera
454
+ * @type {Vector3|undefined} */
455
+ this.shadowCenter = undefined;
456
+ /** @property {number} - Stops surfaces shadowing themselves, raise for speckles, lower if shadows drift off */
457
+ this.shadowBias = .003;
458
+ /** @property {number} - How much to blur the shadow edges */
459
+ this.shadowSoftness = 1;
460
+
461
+ // draw state, read at each draw
462
+ /** @property {boolean} - Apply lighting, when false draws plain vertex color times texture and casts no shadow;
463
+ * off for billboards, lines, ribbons and soft discs, an object sets emissive instead */
464
+ this.lighting = true;
465
+ /** @property {number} - How much a surface lights itself, set per object by its emissive */
466
+ this.emissive = 0;
467
+ /** @property {boolean} - Additive blending instead of alpha, in the transparent stage */
468
+ this.additive = false;
469
+ /** @property {boolean} - Test against the depth buffer, reset to true before each object and callback; a draw
470
+ * with it off goes over what was drawn before it and under what is drawn after, by render order, which
471
+ * ends the batch of meshes before it, so one per object costs a draw per object */
472
+ this.depthTest = true;
473
+ /** @property {boolean} - Write to the depth buffer, owned by the stages: on for opaque, off for transparent */
474
+ this.depthWrite = true;
475
+ // batch state set by drawMesh from each mesh: whether its back faces are skipped, off for strips so they
476
+ // show from both sides, and whether its transform mirrors it so the other winding is the front
477
+ this.cullBackFaces = false;
478
+ this.mirrored = false;
479
+ /** @property {number} - Strength of the highlight where the sun and the Light3D objects reflect, 0 is none and
480
+ * 1 adds a light's full color at its brightest; its size is fixed */
481
+ this.specular = 0;
482
+ /** @property {Shader|undefined} - Custom Shader for the next draws, set from each object's shader; undefined
483
+ * draws with the plugin's own
484
+ * @type {Shader|undefined} */
485
+ this.shader = undefined;
486
+ /** @property {boolean} - Darken by the shadow map when shadows are on, turn it off for things that should stay
487
+ * lit inside a shadow */
488
+ this.receiveShadow = true;
489
+
490
+ // the pass
491
+ /** @property {Function|undefined} - Draw solid world here, it runs again for shadows so only draw in it
492
+ * @type {Function|undefined} */
493
+ this.onRenderOpaque = undefined;
494
+ /** @property {Function|undefined} - Draw see through things here, like glows, billboards and soft shadows
495
+ * @type {Function|undefined} */
496
+ this.onRenderTransparent = undefined;
497
+ /** @property {Mesh|undefined} - Sky dome from buildSky or setSky, drawn around the camera behind everything
498
+ * @type {Mesh|undefined} */
499
+ this.sky = undefined;
500
+ /** @property {boolean} - Draw the 3D scene on top of the 2D scene instead of under it */
501
+ this.renderAfter2D = false;
502
+ /** @property {boolean} - Draw see through things far to near so they blend correctly */
503
+ this.sortTransparent = true;
504
+ /** @property {boolean} - Skip meshes whose bounding sphere is outside the view */
505
+ this.frustumCulling = true;
506
+ /** @property {boolean} - Draw every use of a mesh in the opaque stage as one instanced call, mesh.instanced
507
+ * overrides it per mesh */
508
+ this.instancing = true;
509
+ /** @property {boolean} - Sample textures through mipmaps so they do not shimmer in the distance, false uses
510
+ * each texture's own filtering like 2D */
511
+ this.mipmaps = true;
512
+ /** @property {boolean} - Draw state: keep texture pixels hard edged, no mipmaps and no blending between them,
513
+ * set per object by pixelated */
514
+ this.pixelated = false;
515
+ /** @property {number} - Anisotropic filtering for textures seen at an angle, 1 to 16, 1 is off; needs mipmaps */
516
+ this.anisotropy = 4;
517
+
518
+ // shared meshes, every object using one draws in the same batch
519
+ /** @property {Mesh} - A box of size 1 that drawBox uses, for any object that is a box; set the object's scale3D
520
+ * and color instead of editing the mesh, which would change every box that uses it */
521
+ this.boxMesh = buildBox();
522
+ /** @property {Mesh} - A smooth sphere of diameter 1 that drawSphere uses, shared the same way as boxMesh */
523
+ this.sphereMesh = buildSphere(1, 16, 8, true);
524
+ /** @property {Mesh} - A flat square of size 1 facing +Y, seen from above only, for floors, water and decals;
525
+ * stand it up with the object's rotation3D, and size it with scale3D */
526
+ this.planeMesh = buildGrid();
527
+ this.planeMesh.doubleSided = false;
528
+ /** @property {Mesh} - The same square seen and lit from both sides, for signs, cards and leaves */
529
+ this.planeMeshDoubleSided = buildGrid();
530
+ /** @property {Mesh} - A square of size 1 facing +Z with the tile across it, the corners in the order
531
+ * drawBillboard writes them; a ParticleEmitter3D draws its particles as instances of it, each with its own matrix */
532
+ this.billboardMesh = new Mesh().addStrip([vec3(-.5, .5, 0), vec3(-.5, -.5, 0), vec3(.5, .5, 0), vec3(.5, -.5, 0)],
533
+ vec3(0, 0, 1), RENDER3D_QUAD_UVS);
534
+ this.billboardMesh.doubleSided = true;
535
+
536
+ // read only
537
+ /** @property {boolean} - True while the 3D pass is running, 3D draws are only valid then */
538
+ this.isRendering = false;
539
+ /** @property {boolean} - True while the shadow map is being drawn, draws go to the depth only shader */
540
+ this.shadowPass = false;
541
+ /** @property {Matrix4} - This frame's view matrix */
542
+ this.viewMatrix = new Matrix4;
543
+ /** @property {Matrix4} - This frame's projection matrix */
544
+ this.projectionMatrix = new Matrix4;
545
+ /** @property {Matrix4} - This frame's combined view projection */
546
+ this.viewProjection = new Matrix4;
547
+ /** @property {Matrix4} - This frame's light view projection for the shadow map */
548
+ this.shadowMatrix = new Matrix4;
549
+ /** @property {Vector3} - Camera right axis this frame */
550
+ this.cameraRight = vec3(1, 0, 0);
551
+ /** @property {Vector3} - Camera up axis this frame */
552
+ this.cameraUp = vec3(0, 1, 0);
553
+ /** @property {Vector3} - Camera forward axis this frame */
554
+ this.cameraForward = vec3(0, 0, -1);
555
+ this.cameraBack = vec3(0, 0, 1); // its opposite, the normal of camera facing draws
556
+
557
+ // internal state
558
+ this.blend = false; // blending on, set by the stages
559
+ /** @type {Array<Array<number>>} */
560
+ this.frustumPlanes = []; // the view as six inward planes [x, y, z, w]
561
+ /** @type {Array<Array<number>>} */
562
+ this.shadowPlanes = []; // the shadow map's box as six planes
563
+ /** @type {WebGLProgram|undefined} */
564
+ this.program = undefined; // the main program, undefined when not available
565
+ /** @type {WebGLProgram|undefined} */
566
+ this.currentProgram = undefined; // the program in use during a pass, a Shader's or the main one
567
+ this.lightCount = 0; // Light3D objects sent this pass
568
+ /** @type {WebGLProgram|undefined} */
569
+ this.shadowShader = undefined;
570
+ /** @type {WebGLVertexArrayObject|undefined} */
571
+ this.vao = undefined;
572
+ /** @type {WebGLTexture|undefined} */
573
+ this.whiteTexture = undefined; // 1x1 white for untextured draws
574
+ /** @type {Map<number, WebGLSampler>} */
575
+ this.samplers = new Map; // how textures are filtered in 3D, by wrap and hard edge, see render3DSampler
576
+ /** @type {string|undefined} */
577
+ this.samplerKey = undefined; // the settings the samplers were made for, they are rebuilt when it changes
578
+ /** @type {WebGLTexture|undefined} */
579
+ this.shadowTexture = undefined;
580
+ /** @type {WebGLFramebuffer|undefined} */
581
+ this.shadowFramebuffer = undefined;
582
+ this.shadowTextureSize = 0;
583
+ this.contextGeneration = 0; // counts context losses, a mesh uploaded under an older one uploads again
584
+ this.uniforms = new Map; // uniform locations by program
585
+ /** @type {Object<string, Array<number>>} */
586
+ this.uniformValues = {}; // last values sent for the cached vec4 uniforms
587
+ this.shadowMapDrawn = false; // the shadow map is drawn by the first pass of the frame
588
+ this.passIsDefault = true; // the running pass is the default layer, the only one shadowed
589
+ this.lightPositions = new Float32Array(RENDER3D_MAX_LIGHTS * 4); // Light3D uniforms, filled each pass
590
+ this.lightColors = new Float32Array(RENDER3D_MAX_LIGHTS * 4);
591
+
592
+ // the stream of immediate mode draws
593
+ /** @type {WebGLBuffer|undefined} */
594
+ this.streamBuffer = undefined;
595
+ /** @type {Array<WebGLBuffer>} */
596
+ this.instanceBuffers = []; // the per instance values of the batches being drawn, used in turn
597
+ this.instanceBufferIndex = 0;
598
+ /** @type {Array<Mesh>} */
599
+ this.instanceMeshes = []; // meshes with a batch pending this stage
600
+ /** @type {Array<Array<number>>} */
601
+ this.attribValues = []; // last values sent for the cached constant attributes
602
+ this.streamData = new ArrayBuffer(RENDER3D_MAX_STREAM_VERTS * RENDER3D_VERTEX_BYTES);
603
+ this.streamFloats = new Float32Array(this.streamData);
604
+ this.streamInts = new Uint32Array(this.streamData);
605
+ this.streamCount = 0;
606
+ /** @type {TextureInfo|undefined} */
607
+ this.streamTileInfo = undefined;
608
+ this.streamState = undefined; // captured state the pending batch was drawn under
609
+ /** @type {Mesh|undefined} */
610
+ this.capture = undefined; // the mesh a bake is filling
611
+ /** @type {Array<{distance: number, state: Object, draw: function(): void}>|undefined} */
612
+ this.transparentQueue = undefined; // draws queued during the transparent stage, replayed far to near
613
+
614
+ render3DInitGL();
615
+ engineAddPlugin(undefined, render3DRender, render3DContextLost, render3DContextRestored, render3DPreRender);
616
+ }
617
+
618
+ ///////////////////////////////////////////////////////////////////////////
619
+ // Matrices and picking
620
+
621
+ /** Rebuild the view and projection matrices from the camera, called automatically each frame
622
+ * @param {number} [aspect] - Width over height, defaults to the main canvas */
623
+ updateMatrices(aspect=mainCanvasSize.y ? mainCanvasSize.x / mainCanvasSize.y : 1)
624
+ {
625
+ const camera = this.camera;
626
+ if (camera.align2D)
627
+ camera.update2D();
628
+ const cameraMatrix = camera.getMatrix();
629
+ this.viewMatrix = cameraMatrix.copy().invert();
630
+ this.projectionMatrix = camera.getProjectionMatrix(aspect);
631
+ this.viewProjection = this.projectionMatrix.copy().multiply(this.viewMatrix);
632
+ const m = cameraMatrix.m;
633
+ this.cameraRight = render3DAxis(m, 0);
634
+ this.cameraUp = render3DAxis(m, 4);
635
+ this.cameraBack = render3DAxis(m, 8); // the normal of anything facing the camera
636
+ this.cameraForward = this.cameraBack.scale(-1);
637
+ this.frustumPlanes = render3DFrustumPlanes(this.viewProjection);
638
+ }
639
+
640
+ /** Where a world point lands on screen as -1 to 1 across and up, with z as depth
641
+ * - Uses this frame's camera, call updateMatrices first if the camera just moved
642
+ * @param {Vector3} pos
643
+ * @return {Vector3|undefined} - undefined when behind the camera or closer than the near plane */
644
+ worldToClip(pos)
645
+ {
646
+ const m = this.viewProjection.m;
647
+ const w = m[3]*pos.x + m[7]*pos.y + m[11]*pos.z + m[15];
648
+ const z = (m[2]*pos.x + m[6]*pos.y + m[10]*pos.z + m[14]) / w;
649
+ if (w <= 0 || z < -1)
650
+ return; // behind the camera, or in front of the near plane
651
+ return vec3(
652
+ (m[0]*pos.x + m[4]*pos.y + m[8]*pos.z + m[12]) / w,
653
+ (m[1]*pos.x + m[5]*pos.y + m[9]*pos.z + m[13]) / w, z);
654
+ }
655
+
656
+ /** Project a world point to screen space pixels, same space as mousePosScreen
657
+ * - The opposite of screenToRay, and it takes the same canvas so the pair agree
658
+ * @param {Vector3} pos
659
+ * @param {Vector2} [canvasSize] - Defaults to the main canvas size, as in screenToRay;
660
+ * the projection is whatever updateMatrices last built, which screenToRay does for its canvas
661
+ * @return {Vector2|undefined} - undefined when behind the camera or closer than the near plane */
662
+ worldToScreen(pos, canvasSize=mainCanvasSize)
663
+ {
664
+ const clip = this.worldToClip(pos);
665
+ if (!clip)
666
+ return;
667
+ return vec2((clip.x + 1) / 2 * canvasSize.x, (1 - clip.y) / 2 * canvasSize.y);
668
+ }
669
+
670
+ /** Get the world ray under a screen position, for clicking on things in 3D
671
+ * - Uses the camera where it is right now, so it is fine to call from gameUpdate
672
+ * - It brings the view matrices up to date for that canvas, so worldToScreen stays its exact opposite
673
+ * @param {Vector2} screenPos - Same space as mousePosScreen
674
+ * @param {Vector2} [canvasSize] - Defaults to the main canvas size
675
+ * @return {Ray3D} - Starts at the camera with a unit direction, or on the near plane when orthographic */
676
+ screenToRay(screenPos, canvasSize=mainCanvasSize)
677
+ {
678
+ // a canvas with no size stands in as 1x1, rather than dividing by zero
679
+ const width = canvasSize.x || 1, height = canvasSize.y || 1;
680
+ const aspect = width / height, camera = this.camera;
681
+ // bring the matrices up to date for this canvas, so worldToScreen and this agree on where things are
682
+ this.updateMatrices(aspect);
683
+ const clipX = screenPos.x / width * 2 - 1;
684
+ const clipY = 1 - screenPos.y / height * 2;
685
+ // the screen offset moves a parallel ray's origin, or bends a perspective ray's direction
686
+ const h = camera.orthographic ? camera.orthographic / 2 : tan(camera.fov / 2);
687
+ const offset = this.cameraRight.scale(clipX * h * aspect).add(this.cameraUp.scale(clipY * h));
688
+ // a parallel ray starts on the near plane, which an orthographic camera may put behind it, like three.js
689
+ return camera.orthographic
690
+ ? new Ray3D(camera.pos.add(offset).add(this.cameraForward.scale(camera.near)), this.cameraForward.copy())
691
+ : new Ray3D(camera.pos.copy(), this.cameraForward.add(offset).normalize());
692
+ }
693
+
694
+ /** Where a screen position lands on a flat ground plane, for top down games; use HeightMap.raycast for terrain
695
+ * @param {Vector2} screenPos - Same space as mousePosScreen
696
+ * @param {number} [groundHeight] - World height of the ground plane
697
+ * @param {Vector2} [canvasSize] - Defaults to the main canvas size, as in screenToRay
698
+ * @return {Vector3|undefined} - undefined when the ray misses the plane */
699
+ screenToGround(screenPos, groundHeight=0, canvasSize=mainCanvasSize)
700
+ {
701
+ const ray = this.screenToRay(screenPos, canvasSize);
702
+ const t = raycastPlane(ray, vec3(0, groundHeight, 0), RENDER3D_DEFAULT_NORMAL);
703
+ return t === undefined ? undefined : ray.getPosition(t);
704
+ }
705
+
706
+ /** Find the nearest object under a screen position or along a ray, for clicking on things
707
+ * - Each object is tested as the box around its mesh in its own space, or a sprite as the quad it draws,
708
+ * not triangle by triangle
709
+ * - engineObjectsRaycast3D is the other half of this, every object along a ray instead of the nearest
710
+ * @param {Vector2|Ray3D} from - A screen position like mousePosScreen, or a ray to look along
711
+ * @param {Array<EngineObject>} [objects] - Defaults to every object; only those with a mesh or a sprite count
712
+ * @return {{object: EngineObject3D, distance: number}|undefined} */
713
+ pick(from, objects=engineObjects)
714
+ {
715
+ const ray = from instanceof Ray3D ? from : this.screenToRay(from);
716
+ let nearest;
717
+ for (const o of objects)
718
+ {
719
+ const distance = render3DRaycastObject(ray, o);
720
+ if (distance !== undefined && (!nearest || distance < nearest.distance))
721
+ nearest = {object: /** @type {EngineObject3D} */ (o), distance}; // only a 3D object has a distance
722
+ }
723
+ return nearest;
724
+ }
725
+
726
+ /** Play a sound at a 3D position, quieter with distance from the camera and panned by its side, like Sound.play
727
+ * with a 2D position
728
+ * @param {Sound} sound
729
+ * @param {Vector3} pos3D
730
+ * @param {number} [volume]
731
+ * @param {number} [pitch]
732
+ * @param {number} [randomnessScale] - How much to scale pitch randomness
733
+ * @param {boolean} [loop]
734
+ * @param {boolean} [paused] - Start it paused
735
+ * @return {SoundInstance|undefined} - undefined when out of range or sound is off */
736
+ playSound(sound, pos3D, volume=1, pitch=1, randomnessScale=1, loop=false, paused=false)
737
+ {
738
+ // the 3D range fade and pan, Sound.play with no position does the rest
739
+ ASSERT(sound instanceof Sound, 'sound must be a Sound');
740
+ ASSERT(isVector3(pos3D), 'pos3D must be a vec3');
741
+ const offset = pos3D.subtract(this.camera.pos), range = sound.range;
742
+ if (range)
743
+ {
744
+ const distance = offset.length();
745
+ if (distance > range)
746
+ return; // out of range
747
+ const taperRange = range * sound.taper;
748
+ if (distance > taperRange)
749
+ volume *= percent(distance, range, taperRange);
750
+ }
751
+ const instance = sound.play(undefined, volume, pitch, randomnessScale, loop, paused);
752
+ instance?.setPan(offset.normalize().dot(this.cameraRight));
753
+ return instance;
754
+ }
755
+
756
+ /** Play a sound on a loop at a 3D position, the same as playSound with loop on
757
+ * - Its volume and pan are set when it starts, change or stop it through the SoundInstance returned
758
+ * @param {Sound} sound
759
+ * @param {Vector3} pos3D
760
+ * @param {number} [volume]
761
+ * @param {number} [pitch]
762
+ * @param {number} [randomnessScale] - How much to scale pitch randomness
763
+ * @return {SoundInstance|undefined} - undefined when out of range or sound is off */
764
+ playSoundLoop(sound, pos3D, volume=1, pitch=1, randomnessScale=1)
765
+ { return this.playSound(sound, pos3D, volume, pitch, randomnessScale, true); }
766
+
767
+ /** Is any part of a sphere on screen this frame, the test that skips meshes the camera cannot see
768
+ * - While the shadow map is drawing it tests the shadow area instead
769
+ * @param {Vector3} center
770
+ * @param {number} radius
771
+ * @return {boolean} */
772
+ isSphereVisible(center, radius) { return render3DSphereVisible(center.x, center.y, center.z, radius); }
773
+
774
+ ///////////////////////////////////////////////////////////////////////////
775
+ // Meshes and the stream
776
+
777
+ /** Draw a mesh with the current draw state, batched with its other uses in the opaque stage when instancing is on
778
+ * @param {Mesh} mesh
779
+ * @param {Matrix4|Vector3} [matrix] - Object transform, or just a position to draw it at
780
+ * @param {TileInfo|TextureInfo} [tileInfo] - Texture, mesh uvs map across the tile or the whole texture
781
+ * @param {Color} [color] - Tint
782
+ * @return {void} */
783
+ drawMesh(mesh, matrix=RENDER3D_IDENTITY, tileInfo, color=WHITE)
784
+ {
785
+ matrix = render3DMatrix(matrix);
786
+ ASSERT(!tileInfo || tileInfo instanceof TileInfo || tileInfo instanceof TextureInfo,
787
+ 'tileInfo must be a TileInfo or TextureInfo, it comes before color');
788
+ ASSERT(isColor(color), 'color must be a Color');
789
+ if (this.capture)
790
+ return void this.capture.combine(mesh, matrix, color);
791
+ if (this.transparentQueue)
792
+ {
793
+ // the queue replays later, so it keeps copies of what a caller may reuse, like a scratch matrix
794
+ const m = matrix.copy(), c = color.copy();
795
+ return this.queueTransparent(m.getTranslation(), ()=> this.drawMesh(mesh, m, tileInfo, c));
796
+ }
797
+ if (!render3DCanDraw()) return;
798
+ if (this.shadowPass && !this.lighting) return; // unlit things cast no shadow
799
+ render3DMeshUpload(mesh);
800
+ if (!mesh.bufferCount) return;
801
+ const m = matrix.m;
802
+ if (this.frustumCulling && !render3DSphereVisible(m[12], m[13], m[14], mesh.radius * render3DMaxStretch(m)))
803
+ return;
804
+ // the mesh says whether its back faces can be skipped, and a mirroring transform, one with a negative
805
+ // determinant, turns the winding around so the other one is its front
806
+ const cullBackFaces = this.cullBackFaces, mirrored = this.mirrored;
807
+ this.cullBackFaces = !mesh.doubleSided;
808
+ this.mirrored = render3DMirrors(matrix);
809
+ // the stage draws the batch at its end
810
+ if (!this.blend && this.depthTest && (mesh.instanced ?? this.instancing))
811
+ render3DInstance(mesh, matrix, tileInfo, color);
812
+ else
813
+ {
814
+ this.flush();
815
+ render3DFlushBeforeOverlay();
816
+ render3DSetDrawUniforms(matrix, tileInfo, color);
817
+ render3DBindMesh(mesh);
818
+ glContext.drawElements(glContext.TRIANGLES, mesh.bufferCount, mesh.indexType, 0);
819
+ ++drawCount;
820
+ primitiveCount += mesh.bufferCount / 3;
821
+ }
822
+ this.cullBackFaces = cullBackFaces, this.mirrored = mirrored;
823
+ }
824
+
825
+ /** Draw a triangle strip, batched into the stream with the current draw state
826
+ * - Strip order: the first three points make a triangle, then each point makes another with the two before it
827
+ * - List the first three points counter clockwise as seen from the front, or the face points away
828
+ * and may vanish when back faces are culled
829
+ * - inside a bake the strip goes into the mesh instead, in the transparent stage it is queued for sorting
830
+ * and the arrays are read when the queue replays, so leave them unchanged until the stage ends
831
+ * @param {Array<Vector3>} points - In strip order
832
+ * @param {Vector3|Array<Vector3>} [normals] - One for all or one per point, default up
833
+ * @param {Vector2|Array<Vector2>} [uvs] - One for all or one per point, 0-1 across the tile
834
+ * @param {Color|Array<Color>} [colors] - One for all or one per point, vertex colors come before the texture
835
+ * @param {TileInfo|TextureInfo} [tileInfo] - Texture for this strip
836
+ * @return {void} */
837
+ drawStrip(points, normals, uvs, colors, tileInfo)
838
+ {
839
+ if (this.capture)
840
+ {
841
+ this.capture.addStrip(points, normals, uvs, colors);
842
+ return;
843
+ }
844
+ if (this.transparentQueue)
845
+ {
846
+ // sort by the center of the strip
847
+ let x = 0, y = 0, z = 0;
848
+ for (const p of points)
849
+ x += p.x, y += p.y, z += p.z;
850
+ return this.queueTransparent(vec3(x, y, z).scale(1 / points.length),
851
+ ()=> this.drawStrip(points, normals, uvs, colors, tileInfo));
852
+ }
853
+ ASSERT(isArray(points) && points.length > 2, 'strip needs at least 3 points');
854
+ const n = points.length, count = render3DStripCount(n);
855
+ const uvRect = render3DBeginStrip(count, tileInfo);
856
+ if (!uvRect) return;
857
+
858
+ // the tile rect is applied to each uv now, so the whole texture maps at flush
859
+ const floats = this.streamFloats, ints = this.streamInts;
860
+ const normalArray = isArray(normals), uvArray = isArray(uvs), colorArray = isArray(colors);
861
+ const rgba = colorArray ? 0 : (colors || WHITE).rgbaInt();
862
+ for (let k = 0; k < count; ++k)
863
+ {
864
+ const i = render3DStripIndex(k, n), p = points[i];
865
+ const uv = uvArray ? uvs[i] : uvs || RENDER3D_DEFAULT_UV;
866
+ render3DWriteVertex(floats, ints, this.streamCount++ * RENDER3D_VERTEX_FLOATS, p.x, p.y, p.z,
867
+ normalArray ? normals[i] : normals || RENDER3D_DEFAULT_NORMAL,
868
+ uvRect.x + uv.x * uvRect.w, uvRect.y + uv.y * uvRect.h, colorArray ? colors[i].rgbaInt() : rgba);
869
+ }
870
+ }
871
+
872
+ /** Draw a strip with lighting off, for camera facing shapes where the light direction means nothing
873
+ * @param {Array<Vector3>} points - Strip order
874
+ * @param {Vector3|Array<Vector3>} [normals]
875
+ * @param {Vector2|Array<Vector2>} [uvs]
876
+ * @param {Color|Array<Color>} [colors]
877
+ * @param {TileInfo|TextureInfo} [tileInfo] */
878
+ drawStripUnlit(points, normals, uvs, colors, tileInfo)
879
+ { render3DWithState({lighting: false}, ()=> this.drawStrip(points, normals, uvs, colors, tileInfo)); }
880
+
881
+ /** Draw the pending stream vertices as one strip with the state they were drawn under, called automatically when needed */
882
+ flush()
883
+ {
884
+ if (!this.streamCount || !render3DCanDraw()) return;
885
+ const gl = glContext;
886
+ render3DSetDrawUniforms(RENDER3D_IDENTITY, this.streamTileInfo, WHITE, RENDER3D_FULL_UV_RECT, this.streamState);
887
+ render3DBindVertexBuffer(this.streamBuffer);
888
+ gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.streamFloats, 0, this.streamCount * RENDER3D_VERTEX_FLOATS);
889
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, this.streamCount);
890
+ ++drawCount;
891
+ primitiveCount += this.streamCount;
892
+ this.streamCount = 0;
893
+ }
894
+
895
+ /** Build a mesh once out of draw calls, instead of redrawing the shapes every frame
896
+ * - Call the same drawStrip, drawQuad and drawBox calls inside, and get a mesh back
897
+ * - Strips inside a bake ignore their tileInfo, the finished mesh picks the texture when it draws
898
+ * - drawMesh, drawBox and drawSphere copy their mesh in, moved and tinted, their tileInfo dropped too
899
+ * - The mesh skips its back faces like any, set doubleSided when what was drawn is open
900
+ * @param {Function} drawFunction
901
+ * @return {Mesh} */
902
+ bake(drawFunction)
903
+ {
904
+ this.flush();
905
+ ASSERT(!this.capture, 'bake cannot be nested');
906
+ const mesh = this.capture = new Mesh;
907
+ try { drawFunction(); }
908
+ finally { this.capture = undefined; }
909
+ return mesh;
910
+ }
911
+
912
+ ///////////////////////////////////////////////////////////////////////////
913
+ // The stages, run by the pass
914
+
915
+ /** Draw a layer's objects, solid ones first and see through ones after, called automatically
916
+ * - The main layer also draws the sky, the render callbacks and the debug shapes
917
+ * @param {Array<EngineObject3D>} objects
918
+ * @param {boolean} [isDefault] */
919
+ renderStages(objects, isDefault=true)
920
+ {
921
+ const opaque = [], transparent = [];
922
+ for (const o of objects)
923
+ (o.transparent || o.additive ? transparent : opaque).push(o);
924
+
925
+ isDefault && this.sky && this.drawSky();
926
+
927
+ // opaque: no blending, depth writes on, by render order
928
+ this.blend = false;
929
+ this.depthWrite = true;
930
+ const byOrder = (a, b)=> a.renderOrder - b.renderOrder;
931
+ opaque.sort(byOrder);
932
+ transparent.sort(byOrder);
933
+ render3DDrawObjects(opaque);
934
+ isDefault && this.onRenderOpaque?.();
935
+ this.flush();
936
+ render3DFlushInstances();
937
+
938
+ // transparent: blending on, depth writes off, every draw queued then replayed far to near
939
+ this.blend = true;
940
+ this.depthWrite = false;
941
+ this.transparentQueue = this.sortTransparent ? [] : undefined;
942
+ try
943
+ {
944
+ render3DDrawObjects(transparent);
945
+ for (const o of objects)
946
+ if (o.softShadow)
947
+ {
948
+ // the shadow grows with the object, by the same scale picking and culling
949
+ // measure it at, so one size set once holds however the object is scaled
950
+ const m = render3DObjectMatrix(o);
951
+ this.drawSoftShadow(m.getTranslation(), o.softShadow * render3DMaxScale(m.m), this.softShadowHeight);
952
+ }
953
+ isDefault && this.onRenderTransparent?.();
954
+ }
955
+ finally { this.flushTransparentQueue(); }
956
+ isDefault && render3DRenderDebug();
957
+ this.flush();
958
+
959
+ // leave the fields at the opaque defaults for anything reading them outside the pass
960
+ render3DSetObjectState();
961
+ this.blend = false;
962
+ this.depthWrite = true;
963
+ }
964
+
965
+ /** Queue a draw for the transparent stage, replayed far to near with the current draw state, or draw it now when
966
+ * sorting is off
967
+ * - The draw runs later, so it should hold copies of any values the caller may change before then
968
+ * @param {Vector3} pos - Where the draw is, for sorting
969
+ * @param {function(): void} draw
970
+ * @return {void} */
971
+ queueTransparent(pos, draw)
972
+ {
973
+ if (!this.transparentQueue)
974
+ {
975
+ draw();
976
+ return;
977
+ }
978
+ // sort by depth along the view, not distance, so an orthographic camera orders them right too
979
+ const f = this.cameraForward, c = this.camera.pos;
980
+ const distance = (pos.x - c.x)*f.x + (pos.y - c.y)*f.y + (pos.z - c.z)*f.z;
981
+ this.transparentQueue.push({distance, state: render3DCaptureBatchState(), draw});
982
+ }
983
+
984
+ /** Draw the queued transparent draws far to near with the state each was drawn under, called automatically at the
985
+ * end of the transparent stage */
986
+ flushTransparentQueue()
987
+ {
988
+ const queue = this.transparentQueue;
989
+ if (!queue) return;
990
+ this.transparentQueue = undefined;
991
+ queue.sort((a, b)=> b.distance - a.distance);
992
+ // each draw under the state it was queued with, and the state left as it was found
993
+ const state = render3DCaptureBatchState();
994
+ try
995
+ {
996
+ for (const item of queue)
997
+ render3DApplyBatchState(item.state), item.draw();
998
+ }
999
+ finally { render3DApplyBatchState(state); }
1000
+ }
1001
+
1002
+ /** Draw render3D.sky around the camera, unlit, unfogged and behind everything, called automatically by the pass */
1003
+ drawSky()
1004
+ {
1005
+ this.flush();
1006
+ // the dome only has to sit between the clip planes, the pass draws it first with no depth test;
1007
+ // a far plane at Infinity has no midpoint, so put it a long way out instead
1008
+ const {near, far} = this.camera;
1009
+ const radius = far == Infinity ? near * 1e4 : (near + far) / 2;
1010
+ render3DWithState({lighting: false, blend: false, depthTest: false, depthWrite: false, fogEnd: 0, shader: undefined}, ()=>
1011
+ this.drawMesh(this.sky, buildMatrix(this.camera.pos, undefined, vec3(radius))));
1012
+ }
1013
+
1014
+ /** Rebuild the light's view projection around the shadow center, called automatically each frame shadows are on */
1015
+ updateShadowMatrix()
1016
+ {
1017
+ ASSERT(this.shadowRange > 0, 'shadowRange must be positive');
1018
+ const range = this.shadowRange > 0 ? this.shadowRange : 1, half = range / 2;
1019
+ const toSun = this.sunDirection.normalize();
1020
+ const center = this.shadowCenter || this.camera.pos.add(this.cameraForward.scale(half * .8));
1021
+ const view = Matrix4.lookAt(center.add(toSun.scale(range)), center).invert();
1022
+ // move the light's view in whole pixel steps so shadow edges do not crawl as the camera moves
1023
+ const texel = range / (this.shadowTextureSize || this.shadowMapSize), m = view.m; // no texture in headless mode
1024
+ m[12] = round(m[12] / texel) * texel;
1025
+ m[13] = round(m[13] / texel) * texel;
1026
+ this.shadowMatrix = Matrix4.orthographic(-half, half, -half, half, 0, range * 2).multiply(view);
1027
+ this.shadowPlanes = render3DFrustumPlanes(this.shadowMatrix);
1028
+ }
1029
+
1030
+ /** Build a sky dome, set it as the sky, and light the scene by it: the fog takes the horizon color, and the
1031
+ * ambient light comes from the top color above and the bottom color below, both at the ambient strength
1032
+ * @param {Color} [topColor] - Straight up
1033
+ * @param {Color} [horizonColor] - Level with the camera
1034
+ * @param {Color} [bottomColor] - Straight down, defaults to the horizon color
1035
+ * @param {number} [ambient] - How much of the sky colors lights the scene as ambient, 0 for none
1036
+ * @return {Mesh} - The dome, also in render3D.sky */
1037
+ setSky(topColor=hsl(.6, .8, .55), horizonColor=hsl(.6, 1, .9), bottomColor=horizonColor, ambient=.5)
1038
+ {
1039
+ this.sky?.dispose();
1040
+ this.sky = buildSky(topColor, horizonColor, bottomColor);
1041
+ this.fogColor = horizonColor.copy();
1042
+ this.ambientColor = topColor.scale(ambient, 1);
1043
+ this.ambientGroundColor = bottomColor.scale(ambient, 1);
1044
+ return this.sky;
1045
+ }
1046
+
1047
+ /** Set where fog starts and ends, and its color
1048
+ * @param {number} fogStart - Distance from the camera where fog starts
1049
+ * @param {number} fogEnd - Distance where fog is total, 0 disables fog
1050
+ * @param {Color} [fogColor] - Leaves the color alone when not passed, setSky sets it to the horizon */
1051
+ setFog(fogStart, fogEnd, fogColor)
1052
+ {
1053
+ this.fogStart = fogStart;
1054
+ this.fogEnd = fogEnd;
1055
+ if (fogColor)
1056
+ this.fogColor = fogColor.copy();
1057
+ }
1058
+
1059
+ ///////////////////////////////////////////////////////////////////////////
1060
+ // Immediate mode shapes
1061
+
1062
+ /** Draw a box, untextured, for blocking out a scene without meshes or objects
1063
+ * @param {Vector3} pos - Center
1064
+ * @param {Vector3|number} [size] - Full size, a number for a cube
1065
+ * @param {Color} [color]
1066
+ * @param {Vector3} [rotation] - vec3(pitch, yaw, roll) */
1067
+ drawBox(pos, size=1, color=WHITE, rotation)
1068
+ {
1069
+ this.drawMesh(this.boxMesh, buildMatrix(pos, rotation, render3DSize3(size)), undefined, color);
1070
+ }
1071
+
1072
+ /** Draw a sphere, untextured and smooth shaded
1073
+ * @param {Vector3} pos - Center
1074
+ * @param {number} [size] - Diameter
1075
+ * @param {Color} [color] */
1076
+ drawSphere(pos, size=1, color=WHITE)
1077
+ {
1078
+ this.drawMesh(this.sphereMesh, buildMatrix(pos, undefined, vec3(size)), undefined, color);
1079
+ }
1080
+
1081
+ /** Draw a flat square that always faces the camera, unlit so it keeps its own colors
1082
+ * - Draw it from onRenderTransparent or a transparent object so it can fade
1083
+ * @param {Vector3} pos - Center
1084
+ * @param {Vector2} [size] - World units
1085
+ * @param {TileInfo|TextureInfo} [tileInfo]
1086
+ * @param {Color} [color]
1087
+ * @param {number} [angle] - Rotation in the camera plane, counter clockwise
1088
+ * @param {boolean} [upright] - Stand on world up and only turn to face the camera, for sprites on the ground
1089
+ * @return {void} */
1090
+ drawBillboard(pos, size=vec2(1), tileInfo, color=WHITE, angle=0, upright=false)
1091
+ {
1092
+ if (this.capture) // the mesh keeps the color, so it gets its own, the particles reuse theirs
1093
+ return this.drawStripUnlit(render3DBillboardCorners(pos, size, angle, upright), this.cameraBack,
1094
+ RENDER3D_QUAD_UVS, color.copy(), tileInfo);
1095
+ if (this.transparentQueue) // sort by the exact position, a shadow under it sorts by the floor
1096
+ {
1097
+ const p = pos.copy(), s = size.copy(), c = color.copy(); // copies, the queue replays later
1098
+ return this.queueTransparent(p, ()=> this.drawBillboard(p, s, tileInfo, c, angle, upright));
1099
+ }
1100
+
1101
+ // the quad's six stream vertices written straight in with no vectors made, unlit, for sprites, and for
1102
+ // particles when instancing is off
1103
+ const lit = this.lighting;
1104
+ this.lighting = this.shadowPass && lit; // unlit on screen, in the shadow map the object's flag decides
1105
+ let uvRect;
1106
+ try { uvRect = render3DBeginStrip(6, tileInfo); }
1107
+ finally { this.lighting = lit; }
1108
+ if (!uvRect) return;
1109
+ const a = render3DBillboardAxes(size, angle, upright);
1110
+ const rx = a[0], ry = a[1], rz = a[2], ux = a[3], uy = a[4], uz = a[5];
1111
+ const x = pos.x, y = pos.y, z = pos.z, n = this.cameraBack, rgba = color.rgbaInt();
1112
+ const u0 = uvRect.x, v0 = uvRect.y, u1 = u0 + uvRect.w, v1 = v0 + uvRect.h;
1113
+ const floats = this.streamFloats, ints = this.streamInts, stride = RENDER3D_VERTEX_FLOATS;
1114
+ let j = this.streamCount * stride;
1115
+ this.streamCount += 6;
1116
+ // the corners in strip order with the repeats: top left twice, bottom left, top right, bottom right twice
1117
+ render3DWriteVertex(floats, ints, j, x - rx + ux, y - ry + uy, z - rz + uz, n, u0, v0, rgba);
1118
+ render3DWriteVertex(floats, ints, j += stride, x - rx + ux, y - ry + uy, z - rz + uz, n, u0, v0, rgba);
1119
+ render3DWriteVertex(floats, ints, j += stride, x - rx - ux, y - ry - uy, z - rz - uz, n, u0, v1, rgba);
1120
+ render3DWriteVertex(floats, ints, j += stride, x + rx + ux, y + ry + uy, z + rz + uz, n, u1, v0, rgba);
1121
+ render3DWriteVertex(floats, ints, j += stride, x + rx - ux, y + ry - uy, z + rz - uz, n, u1, v1, rgba);
1122
+ render3DWriteVertex(floats, ints, j += stride, x + rx - ux, y + ry - uy, z + rz - uz, n, u1, v1, rgba);
1123
+ }
1124
+
1125
+ /** Draw a quad from four corners in loop order, counter clockwise seen from the front, a is the top left of the texture
1126
+ * @param {Vector3} a
1127
+ * @param {Vector3} b
1128
+ * @param {Vector3} c
1129
+ * @param {Vector3} d
1130
+ * @param {TileInfo|TextureInfo} [tileInfo]
1131
+ * @param {Color|Array<Color>} [color] - One for all or one per corner */
1132
+ drawQuad(a, b, c, d, tileInfo, color=WHITE)
1133
+ {
1134
+ if (this.transparentQueue) // the queue replays later, so it keeps copies of what a caller may reuse
1135
+ a = a.copy(), b = b.copy(), c = c.copy(), d = d.copy();
1136
+ color = isArray(color) ? color.map(k=> k.copy()) : color.copy();
1137
+ this.drawStrip(render3DQuadStrip(a, b, c, d), render3DFaceNormal(a, b, c, d), RENDER3D_QUAD_UVS,
1138
+ render3DQuadValues(color), tileInfo);
1139
+ }
1140
+
1141
+ /** Draw a triangle, counter clockwise from outside is the front
1142
+ * @param {Vector3} a
1143
+ * @param {Vector3} b
1144
+ * @param {Vector3} c
1145
+ * @param {Color} [color] */
1146
+ drawTriangle(a, b, c, color=WHITE)
1147
+ {
1148
+ if (this.transparentQueue) // the queue replays later, so it keeps copies of what a caller may reuse
1149
+ a = a.copy(), b = b.copy(), c = c.copy(), color = color.copy();
1150
+ this.drawStrip([a, b, c], render3DFaceNormal(a, b, c), undefined, color);
1151
+ }
1152
+
1153
+ /** Draw a line as a camera facing ribbon, unlit
1154
+ * @param {Vector3} posA
1155
+ * @param {Vector3} posB
1156
+ * @param {number} [width]
1157
+ * @param {Color} [color] */
1158
+ drawLine(posA, posB, width=.1, color=WHITE)
1159
+ {
1160
+ this.drawRibbon([posA, posB], width, undefined, color);
1161
+ }
1162
+
1163
+ /** Draw a ribbon along a path, unlit and visible from both sides; width and color can change along it
1164
+ * - The texture runs along the length, u from the first point to the last
1165
+ * - A path that ends where it starts is a loop, and joins with no seam
1166
+ * @param {Array<Vector3>} points - Center line in order, at least two
1167
+ * @param {number|Array<number>} [width] - Full width, one for all or one per point
1168
+ * @param {TileInfo|TextureInfo} [tileInfo]
1169
+ * @param {Color|Array<Color>} [color] - One for all or one per point
1170
+ * @param {Vector3|Array<Vector3>} [side] - Direction across the ribbon, one for all or one per point, default
1171
+ * faces the camera */
1172
+ drawRibbon(points, width=.1, tileInfo, color=WHITE, side)
1173
+ {
1174
+ const count = points.length;
1175
+ ASSERT(count > 1, 'a ribbon needs at least two points');
1176
+ ASSERT(!tileInfo || tileInfo instanceof TileInfo || tileInfo instanceof TextureInfo,
1177
+ 'tileInfo must be a TileInfo or TextureInfo, it comes before color');
1178
+ const strip = [], uvs = tileInfo ? [] : undefined, colors = [], forward = this.cameraForward;
1179
+ let across = vec3(1, 0, 0); // kept from the last point where the direction vanishes
1180
+ // a loop's two ends take their direction across the join, so they meet edge to edge
1181
+ const loop = count > 2 && points[0].distanceSquared(points[count - 1]) < 1e-12;
1182
+ for (let i = 0; i < count; ++i)
1183
+ {
1184
+ const p = points[i];
1185
+ const w = isArray(width) ? width[i] : width;
1186
+ let c = isArray(color) ? color[i] : color;
1187
+ if (this.transparentQueue) // the queue replays later, so it keeps a copy of a color a caller may reuse
1188
+ c = c.copy();
1189
+ const s = side && (isArray(side) ? side[i] : side);
1190
+ // across the path in the camera plane unless a side is given
1191
+ const next = points[i < count - 1 ? i + 1 : loop ? 1 : i];
1192
+ const last = points[i > 0 ? i - 1 : loop ? count - 2 : i];
1193
+ const dir = s || next.subtract(last).cross(forward);
1194
+ if (dir.lengthSquared() > 1e-12)
1195
+ across = dir.normalize();
1196
+ const half = across.scale(w / 2);
1197
+ strip.push(p.add(half), p.subtract(half));
1198
+ uvs?.push(vec2(i / (count - 1), 0), vec2(i / (count - 1), 1));
1199
+ colors.push(c, c);
1200
+ }
1201
+ render3DWithState({lighting: false, cullBackFaces: false},
1202
+ ()=> this.drawStrip(strip, forward.scale(-1), uvs, colors, tileInfo));
1203
+ }
1204
+
1205
+ /** Draw a disc that fades to transparent at the rim, unlit, for glows, puffs and sky dots
1206
+ * @param {Vector3} pos - Center
1207
+ * @param {number} [size] - Diameter
1208
+ * @param {Color} [color]
1209
+ * @param {Vector3} [normal] - Facing direction, faces the camera by default
1210
+ * @param {number} [sides]
1211
+ * @return {void} */
1212
+ drawSoftDisc(pos, size=1, color=WHITE, normal=this.cameraBack, sides=16)
1213
+ {
1214
+ render3DAssertBlending();
1215
+ if (this.transparentQueue && !this.capture)
1216
+ {
1217
+ const p = pos.copy(), c = color.copy(), n = normal.copy(); // copies, the queue replays later
1218
+ return this.queueTransparent(p, ()=> this.drawSoftDisc(p, size, c, n, sides));
1219
+ }
1220
+ // basis in the disc's plane
1221
+ const n = normal.normalize();
1222
+ const helper = abs(n.y) < .9 ? vec3(0, 1, 0) : vec3(1, 0, 0);
1223
+ const u = helper.cross(n).normalize(), w = u.cross(n);
1224
+ render3DDrawSoftDisc(size / 2, color, sides, n, (c, s, r)=>
1225
+ vec3(pos.x + (u.x * c + w.x * s) * r, pos.y + (u.y * c + w.y * s) * r, pos.z + (u.z * c + w.z * s) * r));
1226
+ }
1227
+
1228
+ /** Draw a soft round shadow on the ground under something, much cheaper than a real shadow
1229
+ * - Draw it from onRenderTransparent or from a transparent object
1230
+ * @param {Vector3} pos - Position of the thing casting the shadow
1231
+ * @param {number} [size] - Diameter
1232
+ * @param {number|HeightMap|function(number, number): number} [floorHeight] - Height of the ground, a HeightMap, or
1233
+ * (x, z) => y to follow terrain
1234
+ * @param {Color} [color]
1235
+ * @param {number} [lift] - How far above the ground to draw, raise it if the shadow cuts into rough ground
1236
+ * @return {void} */
1237
+ drawSoftShadow(pos, size=1, floorHeight=0, color=RENDER3D_SHADOW_COLOR, lift=.02)
1238
+ {
1239
+ render3DAssertBlending();
1240
+ // a HeightMap is in the extras plugin, so it is known by its getHeight rather than its class
1241
+ const heightMap = /** @type {HeightMap} */ (floorHeight);
1242
+ const height = /** @type {function(number, number): number} */ (isNumber(floorHeight) ? ()=> floorHeight
1243
+ : heightMap.getHeight ? (x, z)=> heightMap.getHeight(x, z) : floorHeight);
1244
+ if (this.transparentQueue && !this.capture) // sort from the floor, under whatever casts it
1245
+ {
1246
+ const p = pos.copy(), c = color.copy(); // copies, the queue replays later
1247
+ return this.queueTransparent(vec3(p.x, height(p.x, p.z) + lift, p.z),
1248
+ ()=> this.drawSoftShadow(p, size, floorHeight, c, lift));
1249
+ }
1250
+ render3DDrawSoftDisc(size / 2, color, 16, RENDER3D_DEFAULT_NORMAL, (c, s, r)=>
1251
+ {
1252
+ const x = pos.x + c * r, z = pos.z + s * r;
1253
+ return vec3(x, height(x, z) + lift, z);
1254
+ });
1255
+ }
1256
+ }
1257
+
1258
+ function render3DAssertBlending()
1259
+ {
1260
+ const r = render3D;
1261
+ ASSERT(r.blend || r.capture || r.shadowPass || !r.isRendering,
1262
+ 'soft discs and shadows need blending: set the object transparent or draw from onRenderTransparent');
1263
+ }
1264
+
1265
+ // draw the three rings of a soft disc as unlit strips, pointAt(cos, sin, radius) gives the world point
1266
+ function render3DDrawSoftDisc(radius, color, sides, normal, pointAt)
1267
+ {
1268
+ const alpha = [1, .9, .7, 0], circle = render3DCircle(sides); // alpha by ring, center to rim
1269
+ for (let k = 0; k < 3; ++k)
1270
+ {
1271
+ const points = [], colors = [];
1272
+ const c0 = color.withAlpha(color.a * alpha[k]), c1 = color.withAlpha(color.a * alpha[k+1]);
1273
+ const r0 = radius * k / 3, r1 = radius * (k + 1) / 3;
1274
+ for (let i = 0; i <= sides; ++i)
1275
+ {
1276
+ const c = circle[i*2], s = circle[i*2 + 1];
1277
+ points.push(pointAt(c, s, r1), pointAt(c, s, r0));
1278
+ colors.push(c1, c0);
1279
+ }
1280
+ render3D.drawStripUnlit(points, normal, undefined, colors);
1281
+ }
1282
+ }
1283
+
1284
+ ///////////////////////////////////////////////////////////////////////////////
1285
+ /**
1286
+ * Camera3D - Position, rotation and lens for the 3D view
1287
+ * - Looks down its -Z axis, rotation is vec3(pitch, yaw, roll)
1288
+ * @memberof Render3D
1289
+ */
1290
+ class Camera3D
1291
+ {
1292
+ /** Create a camera, looking down -Z from z=10 by default */
1293
+ constructor()
1294
+ {
1295
+ /** @property {Vector3} - World position */
1296
+ this.pos = vec3(0, 0, 10);
1297
+ /** @property {Vector3} - Euler rotation, vec3(pitch, yaw, roll) in radians */
1298
+ this.rotation = vec3();
1299
+ /** @property {number} - Vertical field of view in radians */
1300
+ this.fov = PI/3;
1301
+ /** @property {number} - Near clip distance */
1302
+ this.near = .1;
1303
+ /** @property {number} - Far clip distance, Infinity is allowed for a perspective view */
1304
+ this.far = 1e3;
1305
+ /** @property {number} - Visible height in world units for an orthographic view, 0 is perspective */
1306
+ this.orthographic = 0;
1307
+ /** @property {boolean} - Line the 3D camera up with the 2D camera, so 3D things at z=0 sit on the 2D sprites */
1308
+ this.align2D = false;
1309
+ }
1310
+
1311
+ /** Returns the camera's world transform
1312
+ * @return {Matrix4} */
1313
+ getMatrix() { return buildMatrix(this.pos, this.rotation); }
1314
+
1315
+ /** Returns the view matrix, world to camera space
1316
+ * @return {Matrix4} */
1317
+ getViewMatrix() { return this.getMatrix().invert(); }
1318
+
1319
+ /** Returns the projection matrix
1320
+ * @param {number} aspect - Width over height
1321
+ * @return {Matrix4} */
1322
+ getProjectionMatrix(aspect)
1323
+ {
1324
+ const h = this.orthographic / 2, w = h * aspect;
1325
+ return h ? Matrix4.orthographic(-w, w, -h, h, this.near, this.far) :
1326
+ Matrix4.perspective(this.fov, aspect, this.near, this.far);
1327
+ }
1328
+
1329
+ /** Returns the direction the camera looks
1330
+ * @return {Vector3} */
1331
+ getForward() { return render3DAxis(this.getMatrix().m, 8).scale(-1); }
1332
+
1333
+ /** Returns the camera's right axis
1334
+ * @return {Vector3} */
1335
+ getRight() { return render3DAxis(this.getMatrix().m, 0); }
1336
+
1337
+ /** Returns the camera's up axis
1338
+ * @return {Vector3} */
1339
+ getUp() { return render3DAxis(this.getMatrix().m, 4); }
1340
+
1341
+ /** Point the camera at a target, sets pitch and yaw and clears roll
1342
+ * @param {Vector3} target */
1343
+ lookAt(target) { this.rotation = render3DLookRotation(target.subtract(this.pos), this.rotation); }
1344
+
1345
+ /** Put the camera on an orbit around a target, looking at it
1346
+ * @param {Vector3} target
1347
+ * @param {number} distance
1348
+ * @param {number} yaw - Radians around Y
1349
+ * @param {number} [pitch] - Radians above the horizon */
1350
+ orbit(target, distance, yaw, pitch=.5)
1351
+ {
1352
+ const r = cos(pitch) * distance;
1353
+ this.pos = target.add(vec3(sin(yaw) * r, sin(pitch) * distance, cos(yaw) * r));
1354
+ // straight down has no yaw of its own to look along, so the orbit's yaw is used, and a top down view turns
1355
+ this.rotation = render3DLookRotation(target.subtract(this.pos), vec3(0, yaw, 0));
1356
+ }
1357
+
1358
+ /** Chase a target from an offset, easing toward it, and look at it
1359
+ * @param {Vector3} target
1360
+ * @param {Vector3} offset - Where to sit relative to the target
1361
+ * @param {number} [percent] - How far to move toward the spot each call, 1 snaps */
1362
+ follow(target, offset, percent=1)
1363
+ {
1364
+ this.pos = this.pos.lerp(target.add(offset), percent);
1365
+ this.lookAt(target);
1366
+ }
1367
+
1368
+ /** Line the 3D camera up with the 2D camera, called automatically when align2D is set
1369
+ * @param {number} [canvasHeight] - Defaults to the main canvas height */
1370
+ update2D(canvasHeight=mainCanvasSize.y)
1371
+ {
1372
+ const halfHeight = canvasHeight / 2 / cameraScale; // half visible height in world units
1373
+ const distance = halfHeight / tan(this.fov/2);
1374
+ // a zoomed out 2D camera sits a long way back, far enough to fall past the far plane and
1375
+ // clip the whole scene away, which looks like nothing rendering at all
1376
+ ASSERT(!canvasHeight || distance < this.far,
1377
+ 'align2D needs this camera distance to match the 2D view, raise camera.far past it', distance);
1378
+ this.orthographic &&= halfHeight * 2; // an orthographic camera stays orthographic and shows the same height
1379
+ this.pos = vec3(cameraPos.x, cameraPos.y, distance);
1380
+ this.rotation = vec3(0, 0, -cameraAngle); // 2D angles turn the other way
1381
+ }
1382
+ }
1383
+
1384
+ ///////////////////////////////////////////////////////////////////////////////
1385
+ // GL setup, shaders and the frame hooks
1386
+
1387
+ // the four attributes of a 36 byte vertex at the locations the shaders declare: location, size, type, normalize, byte offset
1388
+ /** @type {Array<[number, number, number, boolean, number]>} */
1389
+ const RENDER3D_ATTRIBS = [[0, 3, 5126, false, 0], [1, 3, 5126, false, 12], [2, 2, 5126, false, 24], [3, 4, 5121, true, 32]];
1390
+
1391
+ // the vertex shader, shared by the plugin's program and every Shader's
1392
+ // attributes: p position, n normal, t uv, c color, at fixed slots the depth shader also uses
1393
+ // uniforms: viewProj, lightViewProj; the model matrix, the tint and the uv rect are vertex attributes, see
1394
+ // RENDER3D_VERTEX_INPUTS; L is the mesh's own uv for a Shader's localUV
1395
+ // the normal matrix comes from the model matrix here: each column over its squared length, which is the inverse
1396
+ // transpose of any rotation and scale, mirrored or not, and skips a 3x3 inverse per draw on the CPU; a sheared
1397
+ // matrix, one built by multiplying rotations with scales between them, gets normals that are only close
1398
+ const RENDER3D_VERTEX_SOURCE =
1399
+ '#version 300 es\n' +
1400
+ 'precision highp float;' +
1401
+ 'uniform mat4 viewProj,lightViewProj;' +
1402
+ RENDER3D_VERTEX_INPUTS +
1403
+ 'out vec3 P,N;out vec2 T,L;out vec4 C,S;' +
1404
+ 'void main(){' +
1405
+ 'vec4 w=mat4(m0,m1,m2,m3)*vec4(p,1.);' +
1406
+ 'gl_Position=viewProj*w;' +
1407
+ 'P=w.xyz;' +
1408
+ 'vec3 c0=m0.xyz,c1=m1.xyz,c2=m2.xyz;' +
1409
+ 'N=mat3(c0/dot(c0,c0),c1/dot(c1,c1),c2/dot(c2,c2))*n;' +
1410
+ 'T=uvRect.xy+t*uvRect.zw;' +
1411
+ 'L=t;' +
1412
+ 'C=c*tint;' +
1413
+ 'S=lightViewProj*w;' +
1414
+ '}';
1415
+
1416
+ // the names a Shader's snippet can use in 3D, over the plugin's own uniforms and varyings
1417
+ const RENDER3D_SNIPPET_NAMES =
1418
+ 'uniform float iTime;uniform vec3 iResolution;\n' +
1419
+ '#define iChannel0 tex\n' +
1420
+ '#define localUV L\n' +
1421
+ '#define worldPos P\n' +
1422
+ '#define worldNormal N\n' +
1423
+ '#define sunDirection (-lightDir.xyz)\n' +
1424
+ '#define sunColor lightColor.rgb\n' +
1425
+ '#define ambientColor ambientFog.rgb\n' +
1426
+ '#define ambientGroundColor ambientGround.rgb\n' +
1427
+ '#define lightCount extraLightCount\n' +
1428
+ '#define lights extraLights\n' +
1429
+ '#define lightColors extraLightColors\n';
1430
+
1431
+ // the fragment shader; given a Shader's snippet, its mainImage replaces the texture sample and all else is the same
1432
+ // uniforms: lightDir (xyz the way the sunlight travels, w = emissive, 1 or more skips the lighting),
1433
+ // lightColor (the sun's rgb, a = specular), ambientFog (rgb, a = fogEnd), fogColor (rgb, a = fogStart),
1434
+ // cameraPos, tex, shadowMap, shadowParams (x = shadows on, y = bias, z = blur step in texture space,
1435
+ // w = how the draw finishes: 1 opaque and alpha tested, 0 blended, -1 additive)
1436
+ function render3DFragmentSource(fragmentCode)
1437
+ {
1438
+ return '#version 300 es\n' +
1439
+ 'precision highp float;' +
1440
+ 'uniform vec4 lightDir,lightColor,ambientFog,ambientGround,fogColor,shadowParams;' +
1441
+ 'uniform vec4 extraLights[' + RENDER3D_MAX_LIGHTS + '],extraLightColors[' + RENDER3D_MAX_LIGHTS + '];' +
1442
+ 'uniform int extraLightCount;' +
1443
+ 'uniform vec3 cameraPos;' +
1444
+ 'uniform sampler2D tex;' +
1445
+ 'uniform bool premultipliedTexture;' + // is the texture a render target, which holds premultiplied color
1446
+ 'uniform highp sampler2DShadow shadowMap;' +
1447
+ 'in vec3 P,N;in vec2 T,L;in vec4 C,S;' +
1448
+ 'out vec4 o;' +
1449
+ // the sun shadow at this fragment, 0 to 1: the light's depth map with a 3x3 blur, outside the map is lit
1450
+ 'float shadow(){' +
1451
+ 'if(shadowParams.x<=0.)return 1.;' +
1452
+ 'vec3 q=S.xyz/S.w*.5+.5;' +
1453
+ 'if(any(greaterThanEqual(abs(q-.5),vec3(.5))))return 1.;' +
1454
+ 'q.z-=shadowParams.y;' +
1455
+ 'float s=0.;' +
1456
+ 'for(int x=-1;x<=1;++x)for(int y=-1;y<=1;++y)' +
1457
+ 's+=texture(shadowMap,vec3(q.xy+vec2(x,y)*shadowParams.z,q.z));' +
1458
+ 'return s/9.;}' +
1459
+ (fragmentCode ? RENDER3D_SNIPPET_NAMES + fragmentCode + '\n' : '') +
1460
+ 'void main(){' +
1461
+ (fragmentCode ? 'vec4 t;mainImage(t,T);' : 'vec4 t=texture(tex,T);') +
1462
+ 'if(premultipliedTexture&&t.a>0.)t.rgb/=t.a;' + // back to straight color, what the lighting and blend expect
1463
+ 'if(shadowParams.w>0.&&t.a<.5)discard;' + // an opaque draw drops see through texels, as the shadow map does
1464
+ 'vec4 c=C*t;' +
1465
+ 'float e=lightDir.w;' +
1466
+ 'if(e<1.){' +
1467
+ 'vec3 n=dot(N,N)>0.?normalize(N):vec3(0,1,0);' +
1468
+ 'if(!gl_FrontFacing)n=-n;' + // only a double sided mesh shows a back face, light it on the side that is seen
1469
+ 'float nl=dot(n,-lightDir.xyz);' +
1470
+ 'float s=shadow();' +
1471
+ // the ambient: one color, or blended from the ground color below to the sky color above by the way the face points
1472
+ 'vec3 l=(ambientGround.a>0.?mix(ambientGround.rgb,ambientFog.rgb,n.y*.5+.5):ambientFog.rgb)+lightColor.rgb*max(nl,0.)*s;' +
1473
+ // the Light3D objects: a point light falls off with distance, a directional one does not and carries the
1474
+ // direction toward it in xyz, marked by a negative radius; each adds its own highlight when there is a strength
1475
+ 'vec3 eye=lightColor.a>0.?normalize(cameraPos-P):vec3(0),sp=vec3(0);' +
1476
+ 'for(int i=0;i<' + RENDER3D_MAX_LIGHTS + ';++i){' +
1477
+ 'if(i>=extraLightCount)break;' +
1478
+ 'vec4 L=extraLights[i];' +
1479
+ 'bool directional=L.w<0.;' +
1480
+ 'vec3 v=directional?L.xyz:L.xyz-P;' +
1481
+ 'float d=length(v);' +
1482
+ 'float a=directional?1.:max(0.,1.-d/L.w);' +
1483
+ 'v/=max(d,1e-6);' +
1484
+ 'float ln=dot(n,v);' +
1485
+ 'vec3 lc=extraLightColors[i].rgb*extraLightColors[i].a*a*a;' +
1486
+ 'l+=lc*max(0.,ln);' +
1487
+ 'if(lightColor.a>0.)sp+=lc*pow(max(dot(reflect(-v,n),eye),0.),16.)*step(0.,ln);' +
1488
+ '}' +
1489
+ 'c.rgb*=l*(1.-e)+e;' + // lit, blended toward its own color by how emissive it is
1490
+ // specular: the sun's only where its light hits and out of shadow, then the Light3D highlights,
1491
+ // skipped entirely when the strength is zero
1492
+ 'if(lightColor.a>0.){' +
1493
+ 'vec3 r=reflect(lightDir.xyz,n);' +
1494
+ 'c.rgb+=lightColor.rgb*pow(max(dot(r,eye),0.),16.)*lightColor.a*step(0.,nl)*s*(1.-e)+sp*lightColor.a*(1.-e);' +
1495
+ '}}else c.rgb*=e;' + // fully emissive: its own color, or brighter, with no lighting to work out
1496
+ 'if(ambientFog.a>0.){' +
1497
+ 'float z=distance(cameraPos,P);' +
1498
+ 'c.rgb=mix(c.rgb,shadowParams.w<0.?vec3(0):fogColor.rgb,smoothstep(fogColor.a,ambientFog.a,z));' +
1499
+ '}' +
1500
+ 'o=vec4(c.rgb,shadowParams.w>0.?1.:c.a);' + // an opaque draw stays opaque whatever the tint alpha says
1501
+ '}';
1502
+ }
1503
+
1504
+ // a Shader's 3D program, compiled the first time a draw needs it
1505
+ function render3DShaderProgram(shader)
1506
+ {
1507
+ ASSERT(shader instanceof Shader, 'render3D.shader must be a Shader, not the snippet itself');
1508
+ return shader.program3D ||= glCreateProgram(RENDER3D_VERTEX_SOURCE, render3DFragmentSource(shader.fragmentCode));
1509
+ }
1510
+
1511
+ // make a program current for the pass and send it the pass uniforms: the matrices, the camera and the lights,
1512
+ // plus the time and canvas size for a Shader's program; the per draw uniform cache starts over
1513
+ function render3DUseProgram(program)
1514
+ {
1515
+ const gl = glContext, r = render3D;
1516
+ gl.useProgram(r.currentProgram = program);
1517
+ r.uniformValues = {};
1518
+ gl.uniformMatrix4fv(render3DUniform('viewProj'), false, r.viewProjection.m);
1519
+ gl.uniformMatrix4fv(render3DUniform('lightViewProj'), false, r.shadowMatrix.m);
1520
+ gl.uniform1i(render3DUniform('tex'), 0);
1521
+ gl.uniform1i(render3DUniform('shadowMap'), 1);
1522
+ const c = r.camera.pos;
1523
+ gl.uniform3f(render3DUniform('cameraPos'), c.x, c.y, c.z);
1524
+ gl.uniform1i(render3DUniform('extraLightCount'), r.lightCount);
1525
+ if (r.lightCount)
1526
+ {
1527
+ gl.uniform4fv(render3DUniform('extraLights'), r.lightPositions, 0, r.lightCount * 4);
1528
+ gl.uniform4fv(render3DUniform('extraLightColors'), r.lightColors, 0, r.lightCount * 4);
1529
+ }
1530
+ if (program !== r.program)
1531
+ {
1532
+ gl.uniform1f(render3DUniform('iTime'), time);
1533
+ gl.uniform3f(render3DUniform('iResolution'), glCanvas.width, glCanvas.height, 1);
1534
+ }
1535
+ }
1536
+
1537
+ function render3DInitGL()
1538
+ {
1539
+ if (headlessMode) return;
1540
+ if (!glEnable || !glContext)
1541
+ {
1542
+ console.warn('Render3DPlugin: WebGL not enabled, construct the plugin in gameInit with glEnable set');
1543
+ return;
1544
+ }
1545
+ const gl = glContext, r = render3D;
1546
+ glFlush(); // a pending 2D batch draws now, while the engine's own buffer, vertex array and program are bound
1547
+ r.uniforms = new Map;
1548
+ r.uniformValues = {};
1549
+ render3DShadowCut = undefined; // the shadow shader is new too
1550
+ r.attribValues = []; // a fresh context has its own attribute defaults, so nothing sent before it counts
1551
+
1552
+ // the shader, see RENDER3D_VERTEX_SOURCE and render3DFragmentSource
1553
+ r.program = glCreateProgram(RENDER3D_VERTEX_SOURCE, render3DFragmentSource());
1554
+
1555
+ // the depth only shader for the shadow map, same vertex layout; see through pixels cast nothing, so sprites and
1556
+ // cut out textures cast their outline, and a blended object faded below half its alpha casts nothing, as it
1557
+ // draws; an opaque one draws solid whatever its tint alpha, and casts so
1558
+ r.shadowShader = glCreateProgram(
1559
+ '#version 300 es\n' +
1560
+ 'precision highp float;' +
1561
+ 'uniform mat4 viewProj;' +
1562
+ RENDER3D_VERTEX_INPUTS +
1563
+ 'out vec2 T;out float A;' +
1564
+ 'void main(){T=uvRect.xy+t*uvRect.zw;A=c.a*tint.a;gl_Position=viewProj*mat4(m0,m1,m2,m3)*vec4(p,1.);}'
1565
+ ,
1566
+ '#version 300 es\n' +
1567
+ 'precision highp float;' +
1568
+ 'uniform sampler2D tex;' +
1569
+ 'uniform float cut;' + // 1 for a blended batch, cut by its tint and vertex alpha too, 0 by the texture only
1570
+ 'in vec2 T;in float A;' +
1571
+ 'void main(){if(texture(tex,T).a*mix(1.,A,cut)<.5)discard;}'
1572
+ );
1573
+
1574
+ // the vertex array object with the attributes enabled once, pointers are set per buffer by render3DBindVertexBuffer
1575
+ // the per instance attributes get their divisor only while a batch has them on, see render3DDrawInstanced
1576
+ r.vao = gl.createVertexArray();
1577
+ gl.bindVertexArray(r.vao);
1578
+ for (const [location] of RENDER3D_ATTRIBS)
1579
+ gl.enableVertexAttribArray(location);
1580
+
1581
+ // the stream buffer
1582
+ r.streamBuffer = gl.createBuffer();
1583
+ gl.bindBuffer(gl.ARRAY_BUFFER, r.streamBuffer);
1584
+ gl.bufferData(gl.ARRAY_BUFFER, r.streamData.byteLength, gl.DYNAMIC_DRAW);
1585
+ r.streamCount = 0;
1586
+ r.instanceBuffers = [gl.createBuffer(), gl.createBuffer(), gl.createBuffer()];
1587
+
1588
+ // white texture for untextured draws, and a one texel shadow map that keeps the shadow sampler valid until shadows
1589
+ // are on
1590
+ r.whiteTexture = glCreateTexture();
1591
+
1592
+ r.samplers = new Map;
1593
+ r.samplerKey = undefined;
1594
+ render3DUpdateShadowMap(1);
1595
+
1596
+ // hand the engine back its own buffer, vertex array and program, the 2D batch was flushed before they changed
1597
+ gl.bindBuffer(gl.ARRAY_BUFFER, glArrayBuffer);
1598
+ glSetInstancedMode(true);
1599
+ }
1600
+
1601
+ function render3DContextLost()
1602
+ {
1603
+ const r = render3D;
1604
+ r.program = r.currentProgram = r.shadowShader = r.vao = r.streamBuffer = r.whiteTexture = undefined;
1605
+ for (const shader of glShaderObjects)
1606
+ shader.program3D = undefined; // compiled again by the next draw
1607
+ r.lightCount = 0;
1608
+ r.instanceBuffers = [];
1609
+ r.samplers = new Map;
1610
+ r.samplerKey = undefined;
1611
+ render3DClearInstances();
1612
+ r.shadowFramebuffer = r.shadowTexture = undefined;
1613
+ r.shadowTextureSize = 0;
1614
+ r.streamCount = 0;
1615
+ ++r.contextGeneration; // every uploaded mesh is stale now, the soft dot is a TextureInfo the engine restores
1616
+ }
1617
+
1618
+ function render3DContextRestored()
1619
+ {
1620
+ render3DInitGL();
1621
+ }
1622
+
1623
+ // a uniform location, looked up once per program
1624
+ function render3DUniform(name, program=render3D.currentProgram)
1625
+ {
1626
+ const u = render3D.uniforms;
1627
+ let cache = u.get(program);
1628
+ cache || u.set(program, cache = {});
1629
+ return cache[name] ??= glContext.getUniformLocation(program, name);
1630
+ }
1631
+
1632
+ // the model matrix, the tint and the uv rect as constant attributes for one draw
1633
+ function render3DDrawAttribs(m, tint, uvRect)
1634
+ {
1635
+ const gl = glContext;
1636
+ gl.vertexAttrib4f(4, m[0], m[1], m[2], m[3]);
1637
+ gl.vertexAttrib4f(5, m[4], m[5], m[6], m[7]);
1638
+ gl.vertexAttrib4f(6, m[8], m[9], m[10], m[11]);
1639
+ gl.vertexAttrib4f(7, m[12], m[13], m[14], m[15]);
1640
+ render3DAttrib4f(11, tint.r, tint.g, tint.b, tint.a);
1641
+ render3DAttrib4f(12, uvRect.x, uvRect.y, uvRect.w, uvRect.h);
1642
+ }
1643
+
1644
+ // set a constant vec4 attribute only when its value changed since the last time
1645
+ function render3DAttrib4f(location, x, y, z, w)
1646
+ {
1647
+ const values = render3D.attribValues, last = values[location] ||= [NaN, NaN, NaN, NaN]; // kept, not made per draw
1648
+ if (last[0] === x && last[1] === y && last[2] === z && last[3] === w)
1649
+ return;
1650
+ last[0] = x, last[1] = y, last[2] = z, last[3] = w;
1651
+ glContext.vertexAttrib4f(location, x, y, z, w);
1652
+ }
1653
+
1654
+ // textures in 3D shrink into the distance far more than sprites do, so the pass samples them through their mipmaps;
1655
+ // a sampler sets the filtering for the 3D pass only and leaves the engine's textures as they are for 2D; they are
1656
+ // made as textures need them, and all dropped when the settings change
1657
+ function render3DUpdateSamplers()
1658
+ {
1659
+ const r = render3D, key = tilesPixelated + ' ' + r.anisotropy;
1660
+ if (r.samplerKey === key) return;
1661
+ r.samplerKey = key;
1662
+ for (const sampler of r.samplers.values())
1663
+ glContext.deleteSampler(sampler); // the set being replaced, a lost context empties this first
1664
+ r.samplers.clear();
1665
+ }
1666
+
1667
+ // the sampler for a texture's wrap modes, smooth or hard edged, made the first time it is needed
1668
+ function render3DSampler(wrap, pixelated)
1669
+ {
1670
+ const gl = glContext, r = render3D, [wrapS, wrapT] = glWrapModes(wrap);
1671
+ const key = wrapS * 1e5 + wrapT * 2 + (pixelated ? 1 : 0); // the modes are 5 digit numbers
1672
+ let sampler = r.samplers.get(key);
1673
+ if (sampler) return sampler;
1674
+ sampler = gl.createSampler();
1675
+ const sharp = pixelated || tilesPixelated;
1676
+ gl.samplerParameteri(sampler, gl.TEXTURE_MAG_FILTER, sharp ? gl.NEAREST : gl.LINEAR);
1677
+ gl.samplerParameteri(sampler, gl.TEXTURE_MIN_FILTER, pixelated ? gl.NEAREST
1678
+ : tilesPixelated ? gl.NEAREST_MIPMAP_LINEAR : gl.LINEAR_MIPMAP_LINEAR);
1679
+ gl.samplerParameteri(sampler, gl.TEXTURE_WRAP_S, wrapS);
1680
+ gl.samplerParameteri(sampler, gl.TEXTURE_WRAP_T, wrapT);
1681
+ const anisotropy = gl.getExtension('EXT_texture_filter_anisotropic');
1682
+ if (anisotropy && !pixelated)
1683
+ {
1684
+ const most = gl.getParameter(anisotropy.MAX_TEXTURE_MAX_ANISOTROPY_EXT);
1685
+ gl.samplerParameterf(sampler, anisotropy.TEXTURE_MAX_ANISOTROPY_EXT, clamp(r.anisotropy, 1, most));
1686
+ }
1687
+ r.samplers.set(key, sampler);
1688
+ return sampler;
1689
+ }
1690
+
1691
+ // bind the texture of a tile or texture, white when there is none or it is not loaded, with the 3D sampler that
1692
+ // matches its wrap mode; the first time a texture is used in 3D it gets its mipmaps
1693
+ function render3DBindTexture(tileInfo, state=render3D)
1694
+ {
1695
+ const gl = glContext, r = render3D;
1696
+ const textureInfo = render3DTextureOf(tileInfo);
1697
+ const texture = textureInfo?.glTexture || r.whiteTexture;
1698
+ gl.bindTexture(gl.TEXTURE_2D, texture);
1699
+ if (texture === r.whiteTexture || !r.mipmaps && !state.pixelated)
1700
+ return gl.bindSampler(0, null); // the texture's own filtering, as in 2D; the white texel needs no mipmaps
1701
+ // or anisotropy, and filtering it that way costs every untextured fragment
1702
+ gl.bindSampler(0, render3DSampler(textureInfo?.wrap, state.pixelated));
1703
+ glUpdateMipmaps(texture); // drawn into since its mipmaps were made
1704
+ if (!state.pixelated && !glMipmappedTextures.has(texture)) // a hard edged draw never reads them
1705
+ {
1706
+ glMipmappedTextures.add(texture); // the core makes them again when the texture changes
1707
+ gl.generateMipmap(gl.TEXTURE_2D);
1708
+ }
1709
+ }
1710
+
1711
+ // send a vec4 uniform of the main shader only when its value changed since the last send
1712
+ function render3DUniform4f(name, x, y, z, w)
1713
+ {
1714
+ const values = render3D.uniformValues, last = values[name] ||= [NaN, NaN, NaN, NaN]; // kept, not made per draw
1715
+ if (last[0] === x && last[1] === y && last[2] === z && last[3] === w)
1716
+ return;
1717
+ last[0] = x, last[1] = y, last[2] = z, last[3] = w;
1718
+ glContext.uniform4f(render3DUniform(name), x, y, z, w);
1719
+ }
1720
+
1721
+ // send an int uniform of the main shader only when its value changed since the last send
1722
+ function render3DUniform1i(name, x)
1723
+ {
1724
+ const values = render3D.uniformValues;
1725
+ if (values[name] === x)
1726
+ return;
1727
+ values[name] = x;
1728
+ glContext.uniform1i(render3DUniform(name), x);
1729
+ }
1730
+
1731
+ // bind a vertex buffer and point the attributes at it
1732
+ function render3DBindVertexBuffer(buffer)
1733
+ {
1734
+ const gl = glContext;
1735
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
1736
+ for (const a of RENDER3D_ATTRIBS)
1737
+ gl.vertexAttribPointer(a[0], a[1], a[2], a[3], RENDER3D_VERTEX_BYTES, a[4]);
1738
+ }
1739
+
1740
+ // a mesh's vertices and its triangle indices, ready for drawElements
1741
+ function render3DBindMesh(mesh)
1742
+ {
1743
+ render3DBindVertexBuffer(mesh.buffer);
1744
+ glContext.bindBuffer(glContext.ELEMENT_ARRAY_BUFFER, mesh.indexBuffer);
1745
+ }
1746
+
1747
+ // the texture a draw samples: a tile's texture, a TextureInfo as it is, or undefined for white
1748
+ function render3DTextureOf(tileInfo) { return tileInfo instanceof TileInfo ? tileInfo.textureInfo : tileInfo; }
1749
+
1750
+ // where a tile sits in its texture, pulled in slightly at the edges so neighbors do not bleed in
1751
+ // the level's solid geometry in 3D, the height maps and voxel maps that objects with collideLevel collide with, as
1752
+ // tileCollisionLayers is in 2D; each joins when made and leaves when destroyed
1753
+ const render3DLevel = [];
1754
+
1755
+ // take one of the level's parts out of it, when it is destroyed
1756
+ function render3DLevelLeave(o)
1757
+ {
1758
+ const i = render3DLevel.indexOf(o);
1759
+ i >= 0 && render3DLevel.splice(i, 1);
1760
+ }
1761
+
1762
+ // the level's parts are looked up in world space, so they stay upright and unscaled at the root
1763
+ function render3DLevelAssertPlaced(o)
1764
+ {
1765
+ ASSERT(!o.parent && !o.rotation3D.lengthSquared() && o.scale3D.x === 1 && o.scale3D.y === 1 && o.scale3D.z === 1,
1766
+ 'a height map or voxel map stays upright and unscaled at the root, its lookups are in world space');
1767
+ }
1768
+
1769
+ // this returns one shared object, so read it before calling again
1770
+ const render3DTileUVRect = {x:0, y:0, w:1, h:1};
1771
+ // the tiles of objects made from a whole TextureInfo, they cover all of it even after the texture is resized
1772
+ const render3DWholeTiles = new WeakSet;
1773
+ function render3DGetTileUVs(tileInfo)
1774
+ {
1775
+ // a headless tile has no texture, and a whole texture's tile covers it at any size
1776
+ if (!(tileInfo instanceof TileInfo) || !tileInfo.textureInfo || render3DWholeTiles.has(tileInfo))
1777
+ return RENDER3D_FULL_UV_RECT;
1778
+ const inv = tileInfo.textureInfo.sizeInverse, rect = render3DTileUVRect;
1779
+ const bleedX = inv.x * tileInfo.bleed, bleedY = inv.y * tileInfo.bleed;
1780
+ rect.x = tileInfo.pos.x * inv.x + bleedX;
1781
+ rect.y = tileInfo.pos.y * inv.y + bleedY;
1782
+ rect.w = tileInfo.size.x * inv.x - 2*bleedX;
1783
+ rect.h = tileInfo.size.y * inv.y - 2*bleedY;
1784
+ return rect;
1785
+ }
1786
+
1787
+ // set the per draw uniforms and gl state for a draw, in the shadow pass only the per draw attributes, the texture
1788
+ // and the depth shader's cut
1789
+ // tileInfo may be a TileInfo, a TextureInfo, or undefined for the white texture
1790
+ // state is the plugin's current fields, or the captured state of a stream batch
1791
+ function render3DSetDrawUniforms(matrix, tileInfo, tint, uvRect, state=render3D)
1792
+ {
1793
+ const gl = glContext, r = render3D;
1794
+
1795
+ // the per draw values are constant vertex attributes, a batch turns on a per instance array over them
1796
+ uvRect ||= render3DGetTileUVs(tileInfo);
1797
+ render3DDrawAttribs(matrix.m, tint, uvRect);
1798
+ render3DBindTexture(tileInfo, state);
1799
+ if (r.shadowPass)
1800
+ {
1801
+ // the shadow map needs only how to cut: by the alpha it blends with, or the texture's as an opaque draw
1802
+ const cut = state.blend ? 1 : 0;
1803
+ if (render3DShadowCut !== cut)
1804
+ glContext.uniform1f(render3DUniform('cut', r.shadowShader), render3DShadowCut = cut);
1805
+ return;
1806
+ }
1807
+
1808
+ // the program: a Shader's own, compiled by its first draw, or the plugin's; switching sends the pass uniforms
1809
+ const program = state.shader ? render3DShaderProgram(state.shader) : r.program;
1810
+ program === r.currentProgram || render3DUseProgram(program);
1811
+
1812
+ // blending, matches the engine's 2D blend functions
1813
+ if (state.blend)
1814
+ {
1815
+ gl.enable(gl.BLEND);
1816
+ const destBlend = state.additive ? gl.ONE : gl.ONE_MINUS_SRC_ALPHA;
1817
+ gl.blendFuncSeparate(gl.SRC_ALPHA, destBlend, gl.ONE, destBlend);
1818
+ }
1819
+ else
1820
+ gl.disable(gl.BLEND);
1821
+
1822
+ // depth and culling
1823
+ state.depthTest ? gl.enable(gl.DEPTH_TEST) : gl.disable(gl.DEPTH_TEST);
1824
+ gl.depthMask(state.depthWrite);
1825
+ state.cullBackFaces ? gl.enable(gl.CULL_FACE) : gl.disable(gl.CULL_FACE);
1826
+ gl.frontFace(state.mirrored ? gl.CCW : gl.CW); // the pass's strips read clockwise, a mirror turns that around
1827
+
1828
+ // lights, fog and shadows are scene state read at draw time, sent only when they change
1829
+ // the shader takes the way the sunlight travels, away from the sun
1830
+ const s = r.sunDirection, sl = -(s.length() || 1), lc = r.sunColor, ac = r.ambientColor, fc = r.fogColor || canvasClearColor;
1831
+ render3DUniform4f('lightDir', s.x / sl, s.y / sl, s.z / sl, state.lighting ? state.emissive : 1);
1832
+ render3DUniform4f('lightColor', lc.r, lc.g, lc.b, state.specular);
1833
+ render3DUniform4f('ambientFog', ac.r, ac.g, ac.b, r.fogEnd);
1834
+ const gc = r.ambientGroundColor;
1835
+ // its alpha says whether the ground color is on
1836
+ gc ? render3DUniform4f('ambientGround', gc.r, gc.g, gc.b, 1) : render3DUniform4f('ambientGround', 0, 0, 0, 0);
1837
+
1838
+ render3DUniform4f('fogColor', fc.r, fc.g, fc.b, r.fogStart);
1839
+ // how the fragment shader finishes: 1 drops see through texels and keeps the draw opaque,
1840
+ // 0 blends them away instead, and -1 is additive, which has to fade into fog differently
1841
+ const blendMode = state.blend ? (state.additive ? -1 : 0) : 1;
1842
+ const receives = r.shadows && r.passIsDefault && state.receiveShadow ? 1 : 0;
1843
+ render3DUniform4f('shadowParams', receives, r.shadowBias, r.shadowSoftness / r.shadowTextureSize, blendMode);
1844
+
1845
+ // a render target's texture holds premultiplied color, the blend writes it that way, so the shader undoes it
1846
+ const textureInfo = render3DTextureOf(tileInfo);
1847
+ render3DUniform1i('premultipliedTexture', +!!(textureInfo?.glTexture && glPremultipliedTextures.has(textureInfo.glTexture)));
1848
+ }
1849
+
1850
+ // the six flat sides of the camera's visible box, each as [x, y, z, w] facing inward
1851
+ // a point is inside when x*px + y*py + z*pz + w is zero or more
1852
+ function render3DFrustumPlanes(matrix)
1853
+ {
1854
+ const m = matrix.m, planes = [];
1855
+ for (let i = 0; i < 3; ++i)
1856
+ for (const sign of [1, -1])
1857
+ {
1858
+ const p = [m[3] + sign * m[i], m[7] + sign * m[4+i], m[11] + sign * m[8+i], m[15] + sign * m[12+i]];
1859
+ const l = hypot(p[0], p[1], p[2]) || 1;
1860
+ planes.push(p.map(v=> v / l));
1861
+ }
1862
+ return planes;
1863
+ }
1864
+
1865
+ // the preRender hook, before gameRender: the layer under the 2D scene
1866
+ function render3DPreRender()
1867
+ {
1868
+ const r = render3D;
1869
+ r.updateMatrices();
1870
+ r.shadowMapDrawn = false;
1871
+ render3DRenderPass(false);
1872
+ }
1873
+
1874
+ // the render hook, after gameRenderPost: the layer on top of the 2D scene
1875
+ function render3DRender()
1876
+ {
1877
+ render3DRenderPass(true);
1878
+ }
1879
+
1880
+ // one 3D pass for the objects of a layer: take over the gl state, draw the shadow map once a frame and the stages, hand
1881
+ // the state back
1882
+ // the layer matching render3D.renderAfter2D is the default and always runs, the other only when an object asks for it
1883
+ function render3DRenderPass(after2D)
1884
+ {
1885
+ const gl = glContext, r = render3D;
1886
+ if (!r.program || !glEnable) return; // headless, gl disabled, or context lost
1887
+ render3DUpdateSamplers();
1888
+ ASSERT(!r.fogEnd || r.fogStart < r.fogEnd, 'fogStart must be less than fogEnd');
1889
+ ASSERT(!glRenderTarget, 'the 3D pass needs the canvas depth buffer, it can not draw into a render target');
1890
+ const isDefault = after2D === !!r.renderAfter2D, objects = render3DLayerObjects(after2D);
1891
+ if (!isDefault && !objects.length) return;
1892
+ r.passIsDefault = isDefault;
1893
+ // the 2D sprites drawn so far go under this layer, and a batch a plugin left pending before the layer under
1894
+ // the 2D scene draws now, with the engine's own gl state, not after the pass with its own
1895
+ glFlush();
1896
+
1897
+ // a previous frame that threw must not leave anything pending
1898
+ r.streamCount = 0;
1899
+ r.capture = r.transparentQueue = undefined;
1900
+ render3DClearInstances();
1901
+
1902
+ // the Light3D objects, a directional one sends the direction toward it, from the origin, and a negative radius
1903
+ // gathered before the gl state is taken over, so an assert here leaves nothing to hand back
1904
+ const lights = render3DCollectLights();
1905
+ r.lightCount = lights.length;
1906
+ const positions = r.lightPositions, colors = r.lightColors;
1907
+ lights.forEach((light, i)=>
1908
+ {
1909
+ const p = light.directional ? light.getWorldPos3D().normalize() : light.getWorldPos3D();
1910
+ ASSERT(!light.directional || p.lengthSquared(),
1911
+ 'a directional light shines from its position toward the origin, so it cannot sit on the origin');
1912
+ const c = light.color, k = i * 4;
1913
+ positions[k] = p.x, positions[k+1] = p.y, positions[k+2] = p.z;
1914
+ positions[k+3] = light.directional ? -1 : max(0, light.radius); // a negative radius marks a direction
1915
+ colors[k] = c.r, colors[k+1] = c.g, colors[k+2] = c.b, colors[k+3] = c.a * light.intensity;
1916
+ });
1917
+
1918
+ // take over the gl state
1919
+ gl.bindVertexArray(r.vao);
1920
+ // the leading repeat on every strip shifts the triangles by one, which flips
1921
+ // which way they read, so tell WebGL that clockwise is the front here
1922
+ gl.frontFace(gl.CW);
1923
+ // every program's shadow sampler is on unit 1, where a 2D plugin like PostProcessPlugin or LightSystemPlugin
1924
+ // may have left its own texture, which fails every draw; the shadow map goes back there even with shadows off
1925
+ gl.activeTexture(gl.TEXTURE1);
1926
+ gl.bindTexture(gl.TEXTURE_2D, r.shadowTexture || null);
1927
+ gl.activeTexture(gl.TEXTURE0);
1928
+ gl.depthMask(true);
1929
+ gl.clear(gl.DEPTH_BUFFER_BIT);
1930
+
1931
+ r.isRendering = true;
1932
+ try
1933
+ {
1934
+ // the shadow map from the light once a frame, then the stages sample it
1935
+ if (r.shadows && !r.shadowMapDrawn)
1936
+ {
1937
+ render3DRenderShadowMap();
1938
+ r.shadowMapDrawn = true;
1939
+ }
1940
+ render3DUseProgram(r.program); // after the shadow map, so the light matrix it sends is this frame's
1941
+ r.renderStages(objects, isDefault);
1942
+ }
1943
+ finally
1944
+ {
1945
+ // hand the state back to the engine's 2D batching, even when a draw threw
1946
+ r.isRendering = false;
1947
+ r.currentProgram = undefined; // the engine's 2D program takes over below
1948
+ r.streamCount = 0;
1949
+ r.capture = r.transparentQueue = undefined;
1950
+ gl.disable(gl.DEPTH_TEST);
1951
+ gl.disable(gl.CULL_FACE);
1952
+ gl.depthMask(true);
1953
+ gl.frontFace(gl.CCW);
1954
+ gl.bindSampler(0, null); // back to the textures' own filtering for 2D
1955
+ if (glActiveTexture)
1956
+ gl.bindTexture(gl.TEXTURE_2D, glActiveTexture);
1957
+ // ARRAY_BUFFER is not part of VAO state in WebGL2, so bindVertexArray alone would not restore it
1958
+ gl.bindBuffer(gl.ARRAY_BUFFER, glArrayBuffer);
1959
+ glSetInstancedMode(true);
1960
+ }
1961
+ }
1962
+
1963
+ // create the shadow map depth texture and framebuffer at a size, or keep them when the size matches
1964
+ function render3DUpdateShadowMap(size)
1965
+ {
1966
+ const gl = glContext, r = render3D;
1967
+ ASSERT(size > 0, 'shadowMapSize must be positive');
1968
+ if (r.shadowTexture && r.shadowTextureSize === size) return;
1969
+ r.shadowTexture && gl.deleteTexture(r.shadowTexture);
1970
+ r.shadowFramebuffer && gl.deleteFramebuffer(r.shadowFramebuffer);
1971
+ const texture = r.shadowTexture = gl.createTexture();
1972
+ gl.activeTexture(gl.TEXTURE1);
1973
+ gl.bindTexture(gl.TEXTURE_2D, texture);
1974
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT24, size, size, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_INT, null);
1975
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); // smooth filtering softens shadow edges for free
1976
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1977
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1978
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1979
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_COMPARE_MODE, gl.COMPARE_REF_TO_TEXTURE);
1980
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_COMPARE_FUNC, gl.LEQUAL);
1981
+ gl.activeTexture(gl.TEXTURE0);
1982
+ const framebuffer = r.shadowFramebuffer = gl.createFramebuffer();
1983
+ gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
1984
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, texture, 0);
1985
+ gl.drawBuffers([gl.NONE]); // depth only
1986
+ gl.readBuffer(gl.NONE);
1987
+ ASSERT(gl.checkFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE,
1988
+ 'shadow map framebuffer is incomplete, try a smaller shadowMapSize');
1989
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
1990
+ r.shadowTextureSize = size;
1991
+ }
1992
+
1993
+ // draw the lit opaque casters from the light into the shadow map with the depth only shader
1994
+ function render3DRenderShadowMap()
1995
+ {
1996
+ const gl = glContext, r = render3D;
1997
+ render3DUpdateShadowMap(r.shadowMapSize | 0);
1998
+ r.updateShadowMatrix();
1999
+
2000
+ // the map can not be read while it is drawn
2001
+ gl.activeTexture(gl.TEXTURE1);
2002
+ gl.bindTexture(gl.TEXTURE_2D, null);
2003
+ gl.activeTexture(gl.TEXTURE0);
2004
+ gl.bindFramebuffer(gl.FRAMEBUFFER, r.shadowFramebuffer);
2005
+ gl.viewport(0, 0, r.shadowTextureSize, r.shadowTextureSize);
2006
+ gl.clear(gl.DEPTH_BUFFER_BIT);
2007
+ gl.useProgram(r.shadowShader);
2008
+ gl.uniformMatrix4fv(render3DUniform('viewProj', r.shadowShader), false, r.shadowMatrix.m);
2009
+ gl.enable(gl.DEPTH_TEST);
2010
+ gl.depthMask(true);
2011
+ gl.disable(gl.BLEND);
2012
+ gl.disable(gl.CULL_FACE);
2013
+
2014
+ r.shadowPass = true;
2015
+ try
2016
+ {
2017
+ // see through objects cast only when textured, their alpha cuts the shadow out
2018
+ const casters = render3DLayerObjects(!!r.renderAfter2D).filter(o=>
2019
+ o.castShadow && !o.additive && (!o.transparent || o.tileInfo));
2020
+ render3DDrawObjects(casters);
2021
+ r.onRenderOpaque?.();
2022
+ r.flush();
2023
+ render3DFlushInstances();
2024
+ }
2025
+ finally
2026
+ {
2027
+ // back to the frame with the map ready to sample
2028
+ r.shadowPass = false;
2029
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
2030
+
2031
+ // backing store size, mainCanvasSize is css pixels
2032
+ gl.viewport(0, 0, glCanvas.width, glCanvas.height);
2033
+ gl.activeTexture(gl.TEXTURE1);
2034
+ gl.bindTexture(gl.TEXTURE_2D, r.shadowTexture);
2035
+ gl.activeTexture(gl.TEXTURE0);
2036
+ }
2037
+ }
2038
+
2039
+ ///////////////////////////////////////////////////////////////////////////////
2040
+ // Strips: every strip repeats its first point once at the start and its last
2041
+ // point once at the end. Those repeats make flat triangles with no area, which
2042
+ // are invisible, and they let one strip run straight into the next.
2043
+ // An odd count gets one more repeat at the end. Triangles in a strip alternate
2044
+ // which way they face, so keeping the count even keeps every strip facing out.
2045
+
2046
+ // make room in the stream for a strip of count vertices under the current state and texture, flushing a batch that
2047
+ // differs first; returns the uv rect to map the vertices with, or undefined when nothing can be drawn
2048
+ function render3DBeginStrip(count, tileInfo)
2049
+ {
2050
+ const r = render3D;
2051
+ if (!render3DCanDraw()) return;
2052
+ if (r.shadowPass && !r.lighting) return; // unlit things cast no shadow
2053
+ ASSERT(count <= RENDER3D_MAX_STREAM_VERTS, 'strip is too large for the stream, bake it into a mesh');
2054
+ if (count > RENDER3D_MAX_STREAM_VERTS) return;
2055
+ const textureInfo = render3DTextureOf(tileInfo);
2056
+ if (r.streamCount && (textureInfo !== r.streamTileInfo || render3DStateChanged(r.streamState)
2057
+ || r.streamCount + count > RENDER3D_MAX_STREAM_VERTS))
2058
+ r.flush();
2059
+ render3DFlushBeforeOverlay();
2060
+ if (!r.streamCount)
2061
+ r.streamState = render3DCaptureBatchState();
2062
+ r.streamTileInfo = textureInfo;
2063
+ return render3DGetTileUVs(tileInfo);
2064
+ }
2065
+
2066
+ // a draw that is not depth tested goes over what is already drawn, so the batches drawn before it go first, or they
2067
+ // would draw at the end of the stage and cover it whatever its render order; a stream open with the same state is
2068
+ // drawn too, so each such draw splits the batches: one draw per object for meshes each with an overlay
2069
+ function render3DFlushBeforeOverlay()
2070
+ {
2071
+ const r = render3D;
2072
+ if (r.depthTest || r.shadowPass || !r.instanceMeshes.length) return;
2073
+ r.flush();
2074
+ render3DFlushInstances();
2075
+ }
2076
+
2077
+ // upload a mesh that is new, changed, or from before the context was lost
2078
+ function render3DMeshUpload(mesh)
2079
+ {
2080
+ if (!mesh.buffer || mesh.dirty || mesh.contextGeneration !== render3D.contextGeneration)
2081
+ mesh.upload();
2082
+ }
2083
+
2084
+ // an object's size3D in the world, grown by its scale and its parents', the size its sprite is drawn at and that
2085
+ // collecting, picking and collision measure it at
2086
+ function render3DWorldSize(o, m)
2087
+ {
2088
+ const s = o.size3D;
2089
+ return vec3(s.x * hypot(m[0], m[1], m[2]), s.y * hypot(m[4], m[5], m[6]), s.z * hypot(m[8], m[9], m[10]));
2090
+ }
2091
+
2092
+ // the surface normal of a height function at x, z, from the slope across a cell each way, clamped to the edges so a
2093
+ // border sample leans the same as its neighbor; buildGrid and HeightMap both light their slopes by it, and it makes
2094
+ // one vector, a big terrain has millions of vertices
2095
+ function render3DSlopeNormal(heightFunction, x, z, ex, ez, halfX, halfZ)
2096
+ {
2097
+ const x0 = max(x - ex, -halfX), x1 = min(x + ex, halfX), z0 = max(z - ez, -halfZ), z1 = min(z + ez, halfZ);
2098
+ const dx = (heightFunction(x1, z) - heightFunction(x0, z)) / (x1 - x0 || 1);
2099
+ const dz = (heightFunction(x, z1) - heightFunction(x, z0)) / (z1 - z0 || 1);
2100
+ const s = 1 / hypot(dx, 1, dz);
2101
+ return vec3(-dx * s, s, -dz * s);
2102
+ }
2103
+
2104
+ // the half axes of a camera facing quad of a size turned by an angle, right then up, in one shared array
2105
+ // so a particle costs no vectors; read it before calling again
2106
+ const render3DBillboardAxesScratch = new Float64Array(6);
2107
+ function render3DBillboardAxes(size, angle, upright)
2108
+ {
2109
+ const r = render3D.cameraRight, u = render3D.cameraUp;
2110
+ let rx = r.x, ry = r.y, rz = r.z, ux = u.x, uy = u.y, uz = u.z;
2111
+ if (upright)
2112
+ {
2113
+ // an upright quad stands on world up and only turns to face the camera
2114
+ const l = hypot(rx, rz); // a rolled camera has no flat right
2115
+ rx = l ? rx / l : 1, ry = 0, rz = l ? rz / l : 0;
2116
+ ux = 0, uy = 1, uz = 0;
2117
+ }
2118
+ const c = cos(angle), s = sin(angle), w = size.x / 2, h = size.y / 2, a = render3DBillboardAxesScratch;
2119
+ a[0] = (rx * c + ux * s) * w, a[1] = (ry * c + uy * s) * w, a[2] = (rz * c + uz * s) * w;
2120
+ a[3] = (ux * c - rx * s) * h, a[4] = (uy * c - ry * s) * h, a[5] = (uz * c - rz * s) * h;
2121
+ return a;
2122
+ }
2123
+
2124
+ // the four corners of a camera facing quad in strip order
2125
+ function render3DBillboardCorners(pos, size, angle, upright)
2126
+ {
2127
+ const a = render3DBillboardAxes(size, angle, upright);
2128
+ const rx = a[0], ry = a[1], rz = a[2], ux = a[3], uy = a[4], uz = a[5];
2129
+ return [
2130
+ vec3(pos.x - rx + ux, pos.y - ry + uy, pos.z - rz + uz), vec3(pos.x - rx - ux, pos.y - ry - uy, pos.z - rz - uz),
2131
+ vec3(pos.x + rx + ux, pos.y + ry + uy, pos.z + rz + uz), vec3(pos.x + rx - ux, pos.y + ry - uy, pos.z + rz - uz)];
2132
+ }
2133
+
2134
+ // how many vertices a strip of n points takes with its repeats, and which point vertex k of it is
2135
+ function render3DStripCount(n) { return n + 2 + (n & 1); }
2136
+ function render3DStripIndex(k, n) { return k < 1 ? 0 : k <= n ? k - 1 : n - 1; }
2137
+
2138
+ // walk a strip's vertices with the repeats applied, calling back with (point, normal, uv, color)
2139
+ // normals, uvs and colors may be one value for all points, an array per point, or undefined
2140
+ function render3DForEachStripVertex(points, normals, uvs, colors, callback)
2141
+ {
2142
+ ASSERT(isArray(points) && points.length > 2, 'strip needs at least 3 points');
2143
+ const n = points.length, count = render3DStripCount(n);
2144
+ const normalArray = isArray(normals), uvArray = isArray(uvs), colorArray = isArray(colors);
2145
+ for (let k = 0; k < count; ++k)
2146
+ {
2147
+ const i = render3DStripIndex(k, n);
2148
+ callback(points[i],
2149
+ normalArray ? normals[i] : normals || RENDER3D_DEFAULT_NORMAL,
2150
+ uvArray ? uvs[i] : uvs || RENDER3D_DEFAULT_UV,
2151
+ colorArray ? colors[i] : colors || WHITE);
2152
+ }
2153
+ }
2154
+
2155
+ // write one vertex into a packed buffer at float index j
2156
+ function render3DWriteVertex(floats, ints, j, x, y, z, n, u, v, rgba)
2157
+ {
2158
+ floats[j] = x; floats[j+1] = y; floats[j+2] = z;
2159
+ floats[j+3] = n.x; floats[j+4] = n.y; floats[j+5] = n.z;
2160
+ floats[j+6] = u; floats[j+7] = v;
2161
+ ints[j+8] = rgba;
2162
+ }
2163
+
2164
+ // turn every triangle of an index list the other way round, in place: the authoring form reads counter clockwise
2165
+ // from the front and the pass draws clockwise, as a strip's triangles come out after its leading repeat
2166
+ function render3DFlipTriangles(indices)
2167
+ {
2168
+ for (let t = 0; t < indices.length; t += 3)
2169
+ {
2170
+ const b = indices[t+1];
2171
+ indices[t+1] = indices[t+2], indices[t+2] = b;
2172
+ }
2173
+ return indices;
2174
+ }
2175
+
2176
+ // the triangles of a strip of count entries as an index list, given which vertex each entry maps to and which place
2177
+ // it is at: the strip's triangle i is (i-2, i-1, i), its odd ones read the other way as the GPU reads a strip, and a
2178
+ // triangle with two corners in one place has no area, it is a join between pieces or a sliver at a pole, and is left out
2179
+ function render3DStripTriangles(count, remap, place)
2180
+ {
2181
+ const indices = [];
2182
+ for (let i = 2; i < count; ++i)
2183
+ {
2184
+ const a = i & 1 ? i - 1 : i - 2, b = i & 1 ? i - 2 : i - 1;
2185
+ if (place[a] != place[b] && place[b] != place[i] && place[a] != place[i])
2186
+ indices.push(remap[a], remap[b], remap[i]);
2187
+ }
2188
+ return indices;
2189
+ }
2190
+
2191
+ // a mesh's packed vertex data for the GPU, one vertex per entry of a layout, which is the strip index of each,
2192
+ // written into data when given, the buffer an earlier call returned for the same layout
2193
+ function render3DMeshVertexData(mesh, vertices, data=new ArrayBuffer(vertices.length * RENDER3D_VERTEX_BYTES))
2194
+ {
2195
+ const count = vertices.length;
2196
+ const floats = new Float32Array(data), ints = new Uint32Array(data);
2197
+ for (let j = 0; j < count; ++j)
2198
+ {
2199
+ // a hand built mesh may leave normals, uvs and colors empty
2200
+ const i = vertices[j], p = mesh.points[i], uv = mesh.uvs[i] || RENDER3D_DEFAULT_UV;
2201
+ render3DWriteVertex(floats, ints, j * RENDER3D_VERTEX_FLOATS, p.x, p.y, p.z,
2202
+ mesh.normals[i] || RENDER3D_DEFAULT_NORMAL, uv.x, uv.y, (mesh.colors[i] || WHITE).rgbaInt());
2203
+ }
2204
+ return data;
2205
+ }
2206
+
2207
+ // reorder a convex polygon's points, counter clockwise from outside, into one triangle strip
2208
+ function render3DPolygonStrip(points)
2209
+ {
2210
+ const strip = [points[0]];
2211
+ for (let i = 1, j = points.length - 1; i <= j; ++i, --j)
2212
+ {
2213
+ strip.push(points[i]);
2214
+ if (i !== j)
2215
+ strip.push(points[j]);
2216
+ }
2217
+ return strip;
2218
+ }
2219
+
2220
+ // turn every triangle of a mesh the other way round, the normals left alone: an index list reads each one in the
2221
+ // other order, and a strip gets one extra point at each end, which flips every triangle and keeps the count even
2222
+ function render3DFlipWinding(mesh)
2223
+ {
2224
+ if (mesh.indices)
2225
+ return void render3DFlipTriangles(mesh.indices);
2226
+ for (const key of ['points', 'normals', 'uvs', 'colors'])
2227
+ {
2228
+ const a = mesh[key];
2229
+ if (a.length)
2230
+ a.unshift(a[0]), a.push(a[a.length - 1]);
2231
+ }
2232
+ mesh.vertexKeys = undefined;
2233
+ }
2234
+
2235
+ ///////////////////////////////////////////////////////////////////////////////
2236
+
2237
+ // frees the GPU buffer of a mesh that is garbage collected without dispose, some time after it goes; it holds the
2238
+ // buffer and its context, never the mesh, or the mesh could not be collected, and dispose unregisters the mesh
2239
+ const render3DMeshBuffers = typeof FinalizationRegistry == 'undefined' ? undefined :
2240
+ new FinalizationRegistry(({buffer, indexBuffer, generation})=>
2241
+ {
2242
+ if (generation !== render3D?.contextGeneration || !glContext) return;
2243
+ glContext.deleteBuffer(buffer);
2244
+ glContext.deleteBuffer(indexBuffer);
2245
+ });
2246
+
2247
+ // the place a point is at, to a hundred thousandth, which smooth normals are summed by
2248
+ function render3DPlaceKey(p) { return `${round(p.x * 1e5)},${round(p.y * 1e5)},${round(p.z * 1e5)}`; }
2249
+
2250
+ // add a face normal to the sum at a triangle's corner a, weighted by the angle there, so a cube corner averages its
2251
+ // three faces evenly however it is cut into triangles
2252
+ function render3DAddCornerNormal(sums, a, b, c, normal)
2253
+ {
2254
+ const u = b.subtract(a), v = c.subtract(a);
2255
+ const angle = Math.acos(clamp(u.dot(v) / (u.length() * v.length() || 1), -1, 1));
2256
+ const k = render3DPlaceKey(a);
2257
+ sums.set(k, (sums.get(k) || vec3()).add(normal.scale(angle)));
2258
+ }
2259
+
2260
+ /**
2261
+ * Mesh - Triangles with positions, normals, uvs and colors, uploaded once and drawn by matrix
2262
+ * - Build with addStrip, addQuad, combine or the shape builders, then render each frame
2263
+ * - Its back faces are skipped unless doubleSided is set, which the open builders like buildGrid do for you
2264
+ * - Two forms: a triangle strip, what the builders make, or an indexed list of triangles over their own vertices,
2265
+ * what addTriangles and the model loaders make; upload sends the GPU an indexed list either way, see getTriangles,
2266
+ * so a strip's joins between its pieces cost nothing to draw, and toIndexed turns a strip mesh into the list form
2267
+ * - The GPU buffer is created lazily on first render and dropped by dispose, or freed once the mesh is garbage
2268
+ * collected, so dispose is only needed to free it right away, like for a mesh rebuilt often
2269
+ * @memberof Render3D
2270
+ * @example
2271
+ * const mesh = buildLathe([[0, -1], [1, 0], [0, 1]], 4); // octahedron
2272
+ * mesh.render(buildMatrix(vec3(0, 1, 0)), undefined, RED);
2273
+ */
2274
+ class Mesh
2275
+ {
2276
+ /** Create an empty mesh */
2277
+ constructor()
2278
+ {
2279
+ /** @property {Array<Vector3>} - Vertex positions, in strip order or one per vertex of an indexed mesh
2280
+ * @type {Array<Vector3>} */
2281
+ this.points = [];
2282
+ /** @property {Array<Vector3>} - Vertex normals
2283
+ * @type {Array<Vector3>} */
2284
+ this.normals = [];
2285
+ /** @property {Array<Vector2>} - Vertex texture coords, 0-1 across the tile
2286
+ * @type {Array<Vector2>} */
2287
+ this.uvs = [];
2288
+ /** @property {Array<Color>} - Vertex colors
2289
+ * @type {Array<Color>} */
2290
+ this.colors = [];
2291
+ /** @property {WebGLBuffer|undefined} - GPU vertex buffer, created by upload
2292
+ * @type {WebGLBuffer|undefined} */
2293
+ this.buffer = undefined;
2294
+ /** @property {WebGLBuffer|undefined} - GPU index buffer, the triangles, created by upload
2295
+ * @type {WebGLBuffer|undefined} */
2296
+ this.indexBuffer = undefined;
2297
+ /** @property {number} - Indices in the GPU index buffer, three per triangle */
2298
+ this.bufferCount = 0;
2299
+ this.indexType = 0; // gl.UNSIGNED_SHORT, or UNSIGNED_INT past 65535 vertices
2300
+ /** @property {boolean} - The mesh changed and needs uploading again, set it yourself if you edit the arrays */
2301
+ this.dirty = false;
2302
+ /** @property {boolean|undefined} - Draw every use of this mesh in the opaque stage as one instanced call,
2303
+ * undefined follows render3D.instancing
2304
+ * @type {boolean|undefined} */
2305
+ this.instanced = undefined;
2306
+ /** @property {boolean} - Draw both sides, each lit as the side that is seen; off skips the faces pointing away,
2307
+ * which is faster and right for closed shapes, the open builders like buildGrid and buildRibbon turn it on */
2308
+ this.doubleSided = false;
2309
+ /** @property {boolean} - The values change often but the shape never does, for a water surface or a cloth: set
2310
+ * once, the mesh keeps its GPU layout and a dirty upload only rewrites the vertices into the buffer it has;
2311
+ * the strip must keep the same points in the same order, a new point count asserts; the layout is decided by
2312
+ * the first upload, so strip entries equal then stay one vertex and triangles with no area then stay dropped,
2313
+ * set vertexKeys or give it distinct values at the start, not a flat grid of one color or points all in one place */
2314
+ this.dynamicDraw = false;
2315
+ /** @property {Array<number>|undefined} - The mesh as an indexed triangle list instead of a strip: the arrays
2316
+ * hold each vertex once and this says how they join, three vertex numbers per triangle, counter clockwise seen
2317
+ * from the front like a strip's first triangle; addTriangles and the loaders fill it, toIndexed turns a strip
2318
+ * mesh into this form
2319
+ * @type {Array<number>|undefined} */
2320
+ this.indices = undefined;
2321
+ /** @property {Int32Array|undefined} - Which strip entries are one vertex, set by a builder that knows, one
2322
+ * whole number per entry with equal numbers meaning the same vertex; upload skips its search for them, then
2323
+ * drops the keys, since an edit after that may tell the entries apart; adding geometry or recomputing normals
2324
+ * drops them too
2325
+ * @type {Int32Array|undefined} */
2326
+ this.vertexKeys = undefined;
2327
+ /** @type {{vertices: Array<number>, pointCount: number, data: ArrayBuffer}|undefined} */
2328
+ // the strip index of each GPU vertex, the point count and packed data of the last upload, for a dynamicDraw mesh
2329
+ this.vertexLayout = undefined;
2330
+ this.instanceCount = 0; // draws waiting in this mesh's batch, with their values, texture and draw state
2331
+ /** @type {Float32Array|undefined} */
2332
+ this.instanceData = undefined;
2333
+ /** @property {number} - Bounding sphere radius around the origin, for culling and picking, computed by upload */
2334
+ this.radius = 0;
2335
+ /** @property {{min: Vector3, max: Vector3}|undefined} - Bounding box, for picking, measured with the radius
2336
+ * @type {{min: Vector3, max: Vector3}|undefined} */
2337
+ this.bounds = undefined;
2338
+ this.contextGeneration = 0; // the context the buffer belongs to, see render3D.contextGeneration
2339
+ }
2340
+
2341
+ /** Number of vertices in the mesh
2342
+ * @return {number} */
2343
+ get vertexCount() { return this.points.length; }
2344
+
2345
+ /** Add a triangle strip, joined to the previous one by invisible flat triangles so one mesh holds many strips
2346
+ * - Strip order: the first three points make a triangle, then each point makes another with the two before it
2347
+ * - List the first three points counter clockwise as seen from the front, or the face points away
2348
+ * and may vanish when back faces are culled
2349
+ * @param {Array<Vector3>} points - Strip order
2350
+ * @param {Vector3|Array<Vector3>} [normals] - One for all or one per point, default up
2351
+ * @param {Vector2|Array<Vector2>} [uvs] - One for all or one per point, default zero
2352
+ * @param {Color|Array<Color>} [colors] - One for all or one per point, default white
2353
+ * @return {Mesh} */
2354
+ addStrip(points, normals, uvs, colors)
2355
+ {
2356
+ if (this.indices)
2357
+ {
2358
+ // an indexed mesh takes the strip as the triangles it makes
2359
+ const part = new Mesh().addStrip(points, normals, uvs, colors).toIndexed();
2360
+ return this.addTriangles(part.points, part.indices, part.normals, part.uvs, part.colors);
2361
+ }
2362
+ render3DForEachStripVertex(points, normals, uvs, colors, (p, n, uv, c)=>
2363
+ {
2364
+ this.points.push(p);
2365
+ this.normals.push(n);
2366
+ this.uvs.push(uv);
2367
+ this.colors.push(c);
2368
+ });
2369
+ this.vertexKeys = undefined; // the new entries have no keys
2370
+ this.dirty = true;
2371
+ return this;
2372
+ }
2373
+
2374
+ /** Add triangles over their own vertices, the indexed form a model file comes in
2375
+ * - The mesh becomes indexed: a strip mesh is turned into triangles first, and strips added later join as triangles
2376
+ * - List each triangle counter clockwise as seen from the front, like a strip's first triangle
2377
+ * @param {Array<Vector3>} points - Each vertex once
2378
+ * @param {Array<number>} indices - Three vertex numbers per triangle, into points
2379
+ * @param {Vector3|Array<Vector3>} [normals] - One for all or one per point, default up
2380
+ * @param {Vector2|Array<Vector2>} [uvs] - One for all or one per point, default zero
2381
+ * @param {Color|Array<Color>} [colors] - One for all or one per point, default white
2382
+ * @return {Mesh} */
2383
+ addTriangles(points, indices, normals, uvs, colors)
2384
+ {
2385
+ ASSERT(isArray(points) && isArray(indices) && indices.length % 3 === 0,
2386
+ 'addTriangles takes points and three indices per triangle');
2387
+ ASSERT(indices.every(i=> i >= 0 && i < points.length && i % 1 === 0), 'an index points past the vertices given');
2388
+ this.toIndexed();
2389
+ const offset = this.points.length, normalArray = isArray(normals), uvArray = isArray(uvs), colorArray = isArray(colors);
2390
+ for (let i = 0; i < points.length; ++i)
2391
+ {
2392
+ this.points.push(points[i]);
2393
+ this.normals.push(normalArray ? normals[i] : normals || RENDER3D_DEFAULT_NORMAL);
2394
+ this.uvs.push(uvArray ? uvs[i] : uvs || RENDER3D_DEFAULT_UV);
2395
+ this.colors.push(colorArray ? colors[i] : colors || WHITE);
2396
+ }
2397
+ for (const i of indices)
2398
+ this.indices.push(i + offset);
2399
+ this.dirty = true;
2400
+ return this;
2401
+ }
2402
+
2403
+ /** Turn a strip mesh into the indexed form, each distinct vertex once and the real triangles over them, in place
2404
+ * - An indexed mesh is left as it is; the builders make strips and a loader makes this, and either draws the same
2405
+ * @return {Mesh} */
2406
+ toIndexed()
2407
+ {
2408
+ if (this.indices) return this;
2409
+ const {vertices, indices} = this.getTriangles();
2410
+ this.points = vertices.map(i=> this.points[i]);
2411
+ this.normals = vertices.map(i=> this.normals[i] || RENDER3D_DEFAULT_NORMAL);
2412
+ this.uvs = vertices.map(i=> this.uvs[i] || RENDER3D_DEFAULT_UV);
2413
+ this.colors = vertices.map(i=> this.colors[i] || WHITE);
2414
+ // the list upload sends reads clockwise, the pass draws it that way; the authoring form is counter clockwise
2415
+ this.indices = render3DFlipTriangles(indices);
2416
+ this.vertexKeys = undefined;
2417
+ this.dirty = true;
2418
+ return this;
2419
+ }
2420
+
2421
+ /** Add a flat quad from four corners in loop order, counter clockwise seen from the front, a is the top left of the texture
2422
+ * @param {Vector3} a
2423
+ * @param {Vector3} b
2424
+ * @param {Vector3} c
2425
+ * @param {Vector3} d
2426
+ * @param {Color|Array<Color>} [color] - One for all or one per corner
2427
+ * @param {Array<Vector2>} [uvs] - One per corner, default across the tile
2428
+ * @return {Mesh} */
2429
+ addQuad(a, b, c, d, color, uvs)
2430
+ {
2431
+ return this.addStrip(render3DQuadStrip(a, b, c, d), render3DFaceNormal(a, b, c, d),
2432
+ uvs ? render3DQuadValues(uvs) : RENDER3D_QUAD_UVS, render3DQuadValues(color));
2433
+ }
2434
+
2435
+ /** Append another mesh transformed by a matrix, for building one shape out of several
2436
+ * @param {Mesh} mesh
2437
+ * @param {Matrix4|Vector3} [matrix] - Transform, or just a position to move it to
2438
+ * @param {Color} [color] - Multiplies the appended vertex colors
2439
+ * @return {Mesh} */
2440
+ combine(mesh, matrix=RENDER3D_IDENTITY, color=WHITE)
2441
+ {
2442
+ matrix = render3DMatrix(matrix); // most parts only need moving into place
2443
+ const normalMatrix = render3DNormalMatrix(matrix), mirrors = render3DMirrors(matrix);
2444
+ let part = mesh, order;
2445
+ if (this.indices || mesh.indices)
2446
+ {
2447
+ // one of them is indexed, so both are: the part as a copy if it is a strip
2448
+ this.toIndexed();
2449
+ part = mesh.indices ? mesh : new Mesh().combine(mesh).toIndexed();
2450
+ const offset = this.points.length, indices = part.indices;
2451
+ // the count read once, a mesh combined with itself grows as it is read; a mirror turns every triangle the
2452
+ // other way round
2453
+ for (let t = 0, n = indices.length; t < n; t += 3)
2454
+ this.indices.push(indices[t] + offset, indices[t + (mirrors ? 2 : 1)] + offset,
2455
+ indices[t + (mirrors ? 1 : 2)] + offset);
2456
+ }
2457
+ else if (mirrors && part.points.length)
2458
+ {
2459
+ // a strip reads the other way round with one more point at each end, as render3DFlipWinding does
2460
+ const last = part.points.length - 1;
2461
+ order = [0, ...part.points.keys(), last];
2462
+ }
2463
+ for (let j = 0, count = order ? order.length : part.points.length; j < count; ++j)
2464
+ {
2465
+ const i = order ? order[j] : j;
2466
+ this.points.push(matrix.transformPoint(part.points[i]));
2467
+ this.normals.push(normalMatrix.transformDirection(part.normals[i] || RENDER3D_DEFAULT_NORMAL).normalize());
2468
+ this.uvs.push((part.uvs[i] || RENDER3D_DEFAULT_UV).copy());
2469
+ this.colors.push((part.colors[i] || WHITE).multiply(color));
2470
+ }
2471
+ this.doubleSided ||= mesh.doubleSided; // an open part leaves the whole mesh open
2472
+ this.vertexKeys = undefined; // the new entries have no keys
2473
+ this.dirty = true;
2474
+ return this;
2475
+ }
2476
+
2477
+ /** Returns a new mesh: this one and its mirror image across the plane through the origin facing axis
2478
+ * - For modeling half a shape against that plane, a part that crosses it overlaps its image
2479
+ * @param {Vector3} [axis] - Faces the mirror plane, vec3(1,0,0) mirrors across x
2480
+ * @return {Mesh} */
2481
+ mirror(axis=vec3(1, 0, 0))
2482
+ {
2483
+ ASSERT(isVector3(axis) && axis.lengthSquared() > 0, 'mirror needs an axis');
2484
+ const n = axis.normalize(), a = -2 * n.x, b = -2 * n.y, c = -2 * n.z;
2485
+ const reflect = new Matrix4([1 + a * n.x, b * n.x, c * n.x, 0, a * n.y, 1 + b * n.y, c * n.y, 0,
2486
+ a * n.z, b * n.z, 1 + c * n.z, 0, 0, 0, 0, 1]);
2487
+ return new Mesh().combine(this).combine(this, reflect); // combine turns the image's faces the right way out
2488
+ }
2489
+
2490
+ /** Returns a new mesh of count copies of this one, each turned further around an axis through the origin
2491
+ * @param {number} count - Copies, spaced evenly around the whole turn
2492
+ * @param {Vector3} [axis] - Up by default
2493
+ * @return {Mesh} */
2494
+ spin(count, axis=vec3(0, 1, 0))
2495
+ {
2496
+ ASSERT(count >= 1 && count % 1 === 0, 'spin count must be a whole number, 1 or more');
2497
+ ASSERT(isVector3(axis) && axis.lengthSquared() > 0, 'spin needs an axis');
2498
+ const mesh = new Mesh, unit = axis.normalize();
2499
+ for (let i = 0; i < count; ++i)
2500
+ mesh.combine(this, render3DAxisRotation(unit, i / count * 2 * PI));
2501
+ return mesh;
2502
+ }
2503
+
2504
+ /** Returns a new mesh of everything in this mesh or the other, see subtract
2505
+ * @param {Mesh} mesh - Closed, as the builders make them apart from the open ones like buildGrid
2506
+ * @param {Matrix4|Vector3} [matrix] - Places the other mesh, or just a position to move it to
2507
+ * @return {Mesh} */
2508
+ union(mesh, matrix) { return render3DMeshCSG(this, mesh, matrix, 0); }
2509
+
2510
+ /** Returns a new mesh of this one with the other cut out of it, CSG with BSP trees
2511
+ * - Both must be closed, every edge shared by two triangles, as the builders make them apart from the open
2512
+ * ones like buildGrid and buildRibbon; the result is closed and indexed, and neither mesh changes
2513
+ * - The faces a cut makes come from the other mesh's surface, turned to face out, with its normals, uvs and
2514
+ * colors, so a smooth cylinder drills a round hole
2515
+ * - Parts that overlap must be joined with union to be one solid, not with combine, mirror or spin, which
2516
+ * leave them overlapping, and CSG then gives a wrong shape
2517
+ * - Cuts split the triangles near them, so the result has more: a few thousand triangles take tens to a few
2518
+ * hundred milliseconds, so build shapes this way at load time, not every frame; joining several cutters
2519
+ * with union and cutting once is quicker than cutting with each in turn
2520
+ * - Details closer than about 1e-4 are made one, so build a very small part larger and scale it after
2521
+ * @param {Mesh} mesh - Closed, as the builders make them apart from the open ones like buildGrid
2522
+ * @param {Matrix4|Vector3} [matrix] - Places the other mesh, or just a position to move it to
2523
+ * @return {Mesh}
2524
+ * @example
2525
+ * const wall = buildBox(vec3(4, 3, .5)).subtract(buildBox(vec3(1, 2, 1)), vec3(0, -.5, 0)); // a doorway */
2526
+ subtract(mesh, matrix) { return render3DMeshCSG(this, mesh, matrix, 1); }
2527
+
2528
+ /** Returns a new mesh of only what is in both this mesh and the other, see subtract
2529
+ * @param {Mesh} mesh - Closed, as the builders make them apart from the open ones like buildGrid
2530
+ * @param {Matrix4|Vector3} [matrix] - Places the other mesh, or just a position to move it to
2531
+ * @return {Mesh} */
2532
+ intersect(mesh, matrix) { return render3DMeshCSG(this, mesh, matrix, 2); }
2533
+
2534
+ /** Scale every uv, so a whole texture repeats across the mesh when its TextureInfo wraps
2535
+ * @param {Vector2|number} scale - Repeats across and up, a number for both
2536
+ * @return {Mesh} */
2537
+ scaleUVs(scale)
2538
+ {
2539
+ const s = render3DSize2(scale);
2540
+ // new vectors, builders share uv objects between faces
2541
+ this.uvs = this.uvs.map(uv=> vec2(uv.x * s.x, uv.y * s.y));
2542
+ this.dirty = true;
2543
+ return this;
2544
+ }
2545
+
2546
+ /** Move, turn or scale every vertex in place, normals follow along
2547
+ * @param {Matrix4|Vector3} matrix - Transform, or just an offset to move by
2548
+ * @return {Mesh} */
2549
+ transform(matrix)
2550
+ {
2551
+ matrix = render3DMatrix(matrix);
2552
+ const normalMatrix = render3DNormalMatrix(matrix);
2553
+ for (let i = 0; i < this.points.length; ++i)
2554
+ {
2555
+ this.points[i] = matrix.transformPoint(this.points[i]);
2556
+ // a mesh built by hand may have no normals yet, and then there is nothing to turn
2557
+ this.normals[i] &&= normalMatrix.transformDirection(this.normals[i]).normalize();
2558
+ }
2559
+ render3DMirrors(matrix) && render3DFlipWinding(this); // a mirror would leave the faces pointing in
2560
+ this.dirty = true;
2561
+ return this;
2562
+ }
2563
+
2564
+ /** Turn the mesh inside out so it is lit and drawn from within, for rooms and domes
2565
+ * @return {Mesh} */
2566
+ flipNormals()
2567
+ {
2568
+ render3DFlipWinding(this);
2569
+ this.normals = this.normals.map(n=> n.scale(-1));
2570
+ this.dirty = true;
2571
+ return this;
2572
+ }
2573
+
2574
+ /** Set every vertex color
2575
+ * @param {Color} color
2576
+ * @return {Mesh} */
2577
+ setColor(color)
2578
+ {
2579
+ // one per point, not one per color already there, so a mesh built by hand with no
2580
+ // colors gets them instead of quietly staying white
2581
+ this.colors = this.points.map(()=> color);
2582
+ this.dirty = true;
2583
+ return this;
2584
+ }
2585
+
2586
+ /** Measure the axis aligned box around the vertices
2587
+ * @return {{min: Vector3, max: Vector3}} */
2588
+ getBounds()
2589
+ {
2590
+ if (!this.points.length)
2591
+ return {min: vec3(), max: vec3()};
2592
+ const lo = vec3(Infinity), hi = vec3(-Infinity);
2593
+ for (const p of this.points)
2594
+ {
2595
+ lo.x = min(lo.x, p.x); lo.y = min(lo.y, p.y); lo.z = min(lo.z, p.z);
2596
+ hi.x = max(hi.x, p.x); hi.y = max(hi.y, p.y); hi.z = max(hi.z, p.z);
2597
+ }
2598
+ return {min: lo, max: hi};
2599
+ }
2600
+
2601
+ /** Move the mesh so the center of its bounds is on the origin
2602
+ * @return {Mesh} */
2603
+ center()
2604
+ {
2605
+ const bounds = this.getBounds();
2606
+ return this.transform(bounds.min.add(bounds.max).scale(-.5));
2607
+ }
2608
+
2609
+ /** Scale the mesh evenly so its largest extent is a size, for loaded models of unknown units
2610
+ * @param {number} [size]
2611
+ * @return {Mesh} */
2612
+ fit(size=1)
2613
+ {
2614
+ const bounds = this.getBounds();
2615
+ const extent = bounds.max.subtract(bounds.min);
2616
+ const scale = size / (max(extent.x, extent.y, extent.z) || 1);
2617
+ return this.transform(Matrix4.scaling(vec3(scale)));
2618
+ }
2619
+
2620
+ /** Measure the bounding sphere around the origin into radius, called by upload
2621
+ * @return {number} */
2622
+ computeRadius()
2623
+ {
2624
+ let r = 0;
2625
+ for (const p of this.points)
2626
+ r = max(r, p.lengthSquared());
2627
+ this.bounds = this.getBounds(); // measured with it, a pick tests a mesh's box after its sphere
2628
+ return this.radius = r ** .5;
2629
+ }
2630
+
2631
+ /** Derive normals from the triangles, of the strip or of the index list
2632
+ * @param {boolean} [smooth] - Round the lighting across faces instead of giving each face a hard edge; flat
2633
+ * normals on an indexed mesh give every corner its own vertex
2634
+ * @return {Mesh} */
2635
+ computeNormals(smooth=false)
2636
+ {
2637
+ if (this.indices)
2638
+ {
2639
+ // the triangles are listed: smooth normals add up around each position, weighted by the corner angle
2640
+ // like the strip's, so vertices split apart at one place smooth back together and a mesh can go flat and
2641
+ // smooth again; flat ones need a vertex per corner, so the vertices are split up first
2642
+ if (!smooth)
2643
+ {
2644
+ const split = (a)=> this.indices.map(i=> a[i]);
2645
+ this.points = split(this.points), this.normals = split(this.normals);
2646
+ this.uvs = split(this.uvs), this.colors = split(this.colors);
2647
+ this.indices = this.indices.map((_, i)=> i);
2648
+ }
2649
+ const points = this.points, indices = this.indices, sums = new Map;
2650
+ const normals = points.map(()=> RENDER3D_DEFAULT_NORMAL);
2651
+ for (let t = 0; t < indices.length; t += 3)
2652
+ {
2653
+ const a = points[indices[t]], b = points[indices[t+1]], c = points[indices[t+2]];
2654
+ const cross = b.subtract(a).cross(c.subtract(a));
2655
+ if (!cross.lengthSquared()) continue;
2656
+ const normal = cross.normalize();
2657
+ if (!smooth)
2658
+ {
2659
+ // its own three corners
2660
+ normals[indices[t]] = normals[indices[t+1]] = normals[indices[t+2]] = normal;
2661
+ continue;
2662
+ }
2663
+ for (let j = 0; j < 3; ++j)
2664
+ render3DAddCornerNormal(sums, points[indices[t+j]], points[indices[t+(j+1)%3]],
2665
+ points[indices[t+(j+2)%3]], normal);
2666
+ }
2667
+ this.normals = !smooth ? normals : points.map(p=>
2668
+ {
2669
+ const s = sums.get(render3DPlaceKey(p));
2670
+ return s && s.lengthSquared() ? s.normalize() : RENDER3D_DEFAULT_NORMAL;
2671
+ });
2672
+ this.dirty = true;
2673
+ return this;
2674
+ }
2675
+
2676
+ // the outward normal of each triangle in the strip
2677
+ const points = this.points, n = points.length;
2678
+ const faceNormals = [];
2679
+ for (let i = 0; i + 2 < n; ++i)
2680
+ {
2681
+ const a = points[i], b = points[i+1], c = points[i+2];
2682
+ const normal = b.subtract(a).cross(c.subtract(a));
2683
+ // triangles in a strip alternate which way they wind, so every other one is flipped back
2684
+ // a zero normal means a flat triangle joining two strips, so skip it
2685
+ faceNormals.push(normal.lengthSquared() ? normal.normalize(i & 1 ? 1 : -1) : undefined);
2686
+ }
2687
+
2688
+ // then hand those to the vertices, shared around a position or kept per face
2689
+ const normals = points.map(()=> RENDER3D_DEFAULT_NORMAL);
2690
+ if (smooth)
2691
+ {
2692
+ // add up the face normals meeting at each position, each weighted by its corner angle so a cube
2693
+ // corner averages its three faces evenly however the strips cut them, then normalize
2694
+ const sums = new Map;
2695
+ faceNormals.forEach((f, i)=> f && [0, 1, 2].forEach(j=>
2696
+ render3DAddCornerNormal(sums, points[i+j], points[i+(j+1)%3], points[i+(j+2)%3], f)));
2697
+ for (let i = 0; i < n; ++i)
2698
+ normals[i] = (sums.get(render3DPlaceKey(points[i])) || RENDER3D_DEFAULT_NORMAL).normalize();
2699
+ }
2700
+ else
2701
+ // every triangle writes its own three corners, so the only vertices left with the default
2702
+ // are the repeats at the ends of a strip, which no triangle with any area uses
2703
+ faceNormals.forEach((f, i)=> f && (normals[i] = normals[i+1] = normals[i+2] = f));
2704
+
2705
+ this.normals = normals;
2706
+ this.vertexKeys = undefined; // flat normals tell entries at one place apart
2707
+ this.dirty = true;
2708
+ return this;
2709
+ }
2710
+
2711
+ /** Pack the vertices and create the GPU buffer, called automatically by render
2712
+ * @return {Mesh} */
2713
+ upload()
2714
+ {
2715
+ this.computeRadius();
2716
+ if (!render3D?.program || !glContext) return this;
2717
+ const gl = glContext, layout = this.vertexLayout;
2718
+ // no layout when dynamicDraw was turned on after an upload, the next upload makes one
2719
+ if (this.dynamicDraw && layout && this.buffer && this.contextGeneration === render3D.contextGeneration)
2720
+ {
2721
+ // the layout of the last upload stands, only the values are written again into the buffer it has,
2722
+ // packed into the same memory each time so a mesh uploaded every frame makes no garbage
2723
+ ASSERT(layout.pointCount === this.points.length,
2724
+ 'a dynamicDraw mesh keeps its shape, the same points in the same order; ' +
2725
+ 'for a new shape make a new mesh or turn dynamicDraw off', this.points.length);
2726
+ if (layout.pointCount === this.points.length)
2727
+ {
2728
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
2729
+ gl.bufferSubData(gl.ARRAY_BUFFER, 0, render3DMeshVertexData(this, layout.vertices, layout.data));
2730
+ gl.bindBuffer(gl.ARRAY_BUFFER, glArrayBuffer);
2731
+ this.dirty = false;
2732
+ return this;
2733
+ }
2734
+ }
2735
+ this.dispose();
2736
+ const {vertices, indices} = this.getTriangles(), count = vertices.length, wide = count > 65535;
2737
+ const data = render3DMeshVertexData(this, vertices);
2738
+ this.vertexLayout = this.dynamicDraw ? {vertices, pointCount: this.points.length, data} : undefined;
2739
+ this.vertexKeys = undefined; // used once: an edit after this may tell entries apart
2740
+ this.buffer = gl.createBuffer();
2741
+ this.indexBuffer = gl.createBuffer();
2742
+ this.bufferCount = indices.length;
2743
+ this.indexType = wide ? gl.UNSIGNED_INT : gl.UNSIGNED_SHORT;
2744
+ this.dirty = false;
2745
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
2746
+ gl.bufferData(gl.ARRAY_BUFFER, data, this.dynamicDraw ? gl.DYNAMIC_DRAW : gl.STATIC_DRAW);
2747
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
2748
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, wide ? new Uint32Array(indices) : new Uint16Array(indices), gl.STATIC_DRAW);
2749
+ this.contextGeneration = render3D.contextGeneration;
2750
+ const buffers = {buffer: this.buffer, indexBuffer: this.indexBuffer, generation: this.contextGeneration};
2751
+ render3DMeshBuffers?.register(this, buffers, this);
2752
+ gl.bindBuffer(gl.ARRAY_BUFFER, glArrayBuffer); // the engine's 2D batch writes through this binding
2753
+ return this;
2754
+ }
2755
+
2756
+ /** The mesh as an indexed triangle list, what upload sends to the GPU: the strip's real triangles over its
2757
+ * distinct vertices, the joins between its pieces dropped and every triangle facing the way it did in the strip
2758
+ * - Vertices are compared to a millionth, so two at one place with the same normal, uv and color are one
2759
+ * @return {{vertices: Array<number>, indices: Array<number>}} - vertices are strip indices, one per distinct
2760
+ * vertex; indices are the triangles, three per triangle, into vertices */
2761
+ getTriangles()
2762
+ {
2763
+ const count = this.points.length, vertices = [];
2764
+ if (this.indices)
2765
+ {
2766
+ // already a list: every vertex as it is, and the triangles read the way the pass draws, clockwise
2767
+ for (let i = 0; i < count; ++i)
2768
+ vertices.push(i);
2769
+ return {vertices, indices: render3DFlipTriangles(this.indices.slice())};
2770
+ }
2771
+ const remap = new Int32Array(count), place = new Int32Array(count), keys = this.vertexKeys;
2772
+ if (keys && keys.length === count)
2773
+ {
2774
+ // a builder said which entries are one vertex: the first entry with a key stands for every entry with it,
2775
+ // and is their place, so nothing is searched
2776
+ const first = new Int32Array(count).fill(-1);
2777
+ for (let i = 0; i < count; ++i)
2778
+ {
2779
+ const key = keys[i];
2780
+ ASSERT(key >= 0 && key < count, 'vertexKeys must be whole numbers below the entry count', key);
2781
+ const j = first[key];
2782
+ if (j < 0)
2783
+ first[key] = i, remap[i] = vertices.length, vertices.push(i);
2784
+ else
2785
+ remap[i] = remap[j];
2786
+ place[i] = first[key];
2787
+ }
2788
+ return {vertices, indices: render3DStripTriangles(count, remap, place)};
2789
+ }
2790
+
2791
+ // each vertex as nine whole numbers, its values in millionths and its color, so vertices hash and compare as
2792
+ // numbers; a hash table over all nine finds the distinct vertices and one over the first three the places,
2793
+ // each slot holding a vertex index plus one and a taken slot moving on to the next
2794
+ const values = new Float64Array(count * 9);
2795
+ let size = 1;
2796
+ while (size < count * 2) size *= 2;
2797
+ const mask = size - 1, seen = new Int32Array(size), places = new Int32Array(size);
2798
+ const mix = (h, v)=> Math.imul(h ^ v, 0x9e3779b1) >>> 0;
2799
+ for (let i = 0; i < count; ++i)
2800
+ {
2801
+ const p = this.points[i], n = this.normals[i] || RENDER3D_DEFAULT_NORMAL;
2802
+ const uv = this.uvs[i] || RENDER3D_DEFAULT_UV, k = i * 9;
2803
+ const x = values[k] = round(p.x * 1e6), y = values[k+1] = round(p.y * 1e6), z = values[k+2] = round(p.z * 1e6);
2804
+ values[k+3] = round(n.x * 1e6), values[k+4] = round(n.y * 1e6), values[k+5] = round(n.z * 1e6);
2805
+ values[k+6] = round(uv.x * 1e6), values[k+7] = round(uv.y * 1e6);
2806
+ values[k+8] = (this.colors[i] || WHITE).rgbaInt();
2807
+ let h = mix(mix(mix(0x811c9dc5, x), y), z);
2808
+
2809
+ // the place, shared with a vertex there that has another normal or uv, as at a lathe's poles
2810
+ for (let slot = h & mask;; slot = (slot + 1) & mask)
2811
+ {
2812
+ const o = places[slot] - 1;
2813
+ if (o < 0) { places[slot] = i + 1, place[i] = i; break; }
2814
+ if (values[o*9] === x && values[o*9+1] === y && values[o*9+2] === z) { place[i] = o; break; }
2815
+ }
2816
+
2817
+ // the whole vertex
2818
+ for (let m = 3; m < 9; ++m)
2819
+ h = mix(h, values[k+m]);
2820
+ for (let slot = h & mask;; slot = (slot + 1) & mask)
2821
+ {
2822
+ const o = seen[slot] - 1;
2823
+ if (o < 0) { seen[slot] = i + 1, remap[i] = vertices.length, vertices.push(i); break; }
2824
+ let same = true;
2825
+ for (let m = 0; same && m < 9; ++m)
2826
+ same = values[o*9+m] === values[k+m];
2827
+ if (same) { remap[i] = remap[o]; break; }
2828
+ }
2829
+ }
2830
+ return {vertices, indices: render3DStripTriangles(count, remap, place)};
2831
+ }
2832
+
2833
+ /** Draw the mesh with the current draw state, batched with its other uses in the opaque stage
2834
+ * @param {Matrix4|Vector3} [matrix] - Object transform, or just a position to draw it at
2835
+ * @param {TileInfo|TextureInfo} [tileInfo] - Texture, mesh uvs map across the tile or the whole texture
2836
+ * @param {Color} [color] - Tint */
2837
+ render(matrix, tileInfo, color) { render3D?.drawMesh(this, matrix, tileInfo, color); }
2838
+
2839
+ /** Delete the GPU buffer now, the CPU arrays stay so the mesh can be rendered again
2840
+ * - Optional, the buffer is freed anyway once the mesh is garbage collected, this frees it right away */
2841
+ dispose()
2842
+ {
2843
+ if (!this.buffer) return;
2844
+ render3DMeshBuffers?.unregister(this); // freed here, so not again when the mesh is collected
2845
+ // a buffer from a context that was lost is gone with it, and the new context refuses to delete it
2846
+ if (this.contextGeneration === render3D?.contextGeneration)
2847
+ glContext?.deleteBuffer(this.buffer), glContext?.deleteBuffer(this.indexBuffer);
2848
+ this.buffer = this.indexBuffer = undefined;
2849
+ this.bufferCount = 0;
2850
+ }
2851
+ }
2852
+
2853
+ ///////////////////////////////////////////////////////////////////////////////
2854
+ // Shape builders, all centered on the origin so buildMatrix does placement
2855
+ // Sizes are full sizes like buildBox and the 2D drawCircle, sides go around an axis and rings along it
2856
+
2857
+ /**
2858
+ * Spin a flat outline around the Y axis to make a round shape, like a vase or a wheel
2859
+ * - profile is [[radius, y], ...] from bottom to top
2860
+ * - A profile that ends where it starts makes a closed ring like a donut
2861
+ * - An end left open, with a radius and no cap, makes the mesh doubleSided so its inside shows
2862
+ * - An end on the axis smooth shades as a round pole like a sphere's when its segment is within 45 degrees of
2863
+ * level, and as a point like a cone's tip when it is steeper
2864
+ * @param {Array<Array<number>>} profile
2865
+ * @param {number} [sides] - Around the axis
2866
+ * @param {boolean} [smooth] - Defaults to render3D.smoothShading
2867
+ * @param {boolean} [capped] - Close the ends that have a radius with flat discs
2868
+ * @return {Mesh}
2869
+ * @memberof Render3D
2870
+ * @example
2871
+ * const vase = buildLathe([[0, -1], [.8, -.3], [.9, .2], [.4, .6], [0, 1]], 12);
2872
+ */
2873
+ function buildLathe(profile, sides=16, smooth=render3D?.smoothShading, capped=true)
2874
+ {
2875
+ ASSERT(isArray(profile) && profile.length > 1, 'lathe profile needs at least 2 points');
2876
+ sides |= 0;
2877
+ ASSERT(sides > 2, 'lathe needs at least 3 sides');
2878
+ const mesh = new Mesh;
2879
+ const rings = profile.length;
2880
+ const point = (i, a)=> vec3(sin(a) * profile[i][0], profile[i][1], cos(a) * profile[i][0]);
2881
+
2882
+ // 2D outward normal of each profile segment, in (radius, y) space
2883
+ const segmentNormal = (i)=>
2884
+ {
2885
+ const [r0, y0] = profile[i], [r1, y1] = profile[i+1];
2886
+ const n = vec2(y1 - y0, r0 - r1);
2887
+ return n.length() ? n.normalize() : vec2(1, 0);
2888
+ };
2889
+ // vertex normal: average of the adjacent segment normals, across the seam when the profile is closed,
2890
+ // and a closed profile needs no caps
2891
+ const closed = rings > 2 && abs(profile[0][0] - profile[rings-1][0]) < 1e-9 &&
2892
+ abs(profile[0][1] - profile[rings-1][1]) < 1e-9;
2893
+ const segmentLength = (i)=> hypot(profile[i+1][0] - profile[i][0], profile[i+1][1] - profile[i][1]);
2894
+ const vertexNormal = (i)=>
2895
+ {
2896
+ // an open end on the axis is a pole and points along it, like a sphere's, when the surface there is
2897
+ // within 45 degrees of level; a steeper one is a point like a cone's tip and takes its side's normal
2898
+ if (!closed && (!i || i == rings - 1) && abs(profile[i][0]) < 1e-9)
2899
+ {
2900
+ const up = i ? 1 : -1;
2901
+ if (segmentNormal(i ? i - 1 : 0).y * up > Math.SQRT1_2 - 1e-9)
2902
+ return vec2(0, up);
2903
+ }
2904
+ // otherwise the neighbors weighted by their length, so a short band does not tilt a long wall
2905
+ let n = vec2();
2906
+ const add = (s)=> n = n.add(segmentNormal(s).scale(segmentLength(s)));
2907
+ if (i > 0) add(i - 1);
2908
+ else if (closed) add(rings - 2);
2909
+ if (i < rings - 1) add(i);
2910
+ else if (closed) add(0);
2911
+ return n.length() ? n.normalize() : vec2(1, 0);
2912
+ };
2913
+ const normal3D = (n, a)=> vec3(sin(a) * n.x, n.y, cos(a) * n.x);
2914
+
2915
+ // v runs along the profile by arc length
2916
+ const lengths = [0];
2917
+ for (let i = 1; i < rings; ++i)
2918
+ lengths[i] = lengths[i-1] + hypot(profile[i][0] - profile[i-1][0], profile[i][1] - profile[i-1][1]);
2919
+ const total = lengths[rings - 1] || 1;
2920
+ const v = (i)=> 1 - lengths[i] / total;
2921
+
2922
+ for (let i = 0; i + 1 < rings; ++i)
2923
+ {
2924
+ if (smooth)
2925
+ {
2926
+ // one ribbon around the ring pair, top point then bottom point per column
2927
+ const points = [], normals = [], uvs = [];
2928
+ const n0 = vertexNormal(i), n1 = vertexNormal(i + 1);
2929
+ // a point on the axis is one per column, each drawn by the face beside it, so its normal turns half
2930
+ // a side toward the middle of that face; a pole's points along the axis and does not turn
2931
+ const half = PI / sides;
2932
+ const turn1 = abs(profile[i + 1][0]) < 1e-9 ? -half : 0, turn0 = abs(profile[i][0]) < 1e-9 ? half : 0;
2933
+ for (let j = 0; j <= sides; ++j)
2934
+ {
2935
+ const a = j / sides * 2 * PI, u = j / sides;
2936
+ points.push(point(i + 1, a), point(i, a));
2937
+ normals.push(normal3D(n1, a + turn1), normal3D(n0, a + turn0));
2938
+ uvs.push(vec2(u, v(i + 1)), vec2(u, v(i)));
2939
+ }
2940
+ mesh.addStrip(points, normals, uvs);
2941
+ }
2942
+ else
2943
+ {
2944
+ // one quad per side with its face normal
2945
+ const n = segmentNormal(i);
2946
+ for (let j = 0; j < sides; ++j)
2947
+ {
2948
+ const a0 = j / sides * 2 * PI, a1 = (j + 1) / sides * 2 * PI;
2949
+ const u0 = j / sides, u1 = (j + 1) / sides;
2950
+ mesh.addStrip(
2951
+ [point(i + 1, a0), point(i, a0), point(i + 1, a1), point(i, a1)],
2952
+ normal3D(n, (a0 + a1) / 2),
2953
+ [vec2(u0, v(i + 1)), vec2(u0, v(i)), vec2(u1, v(i + 1)), vec2(u1, v(i))]);
2954
+ }
2955
+ }
2956
+ }
2957
+
2958
+ // flat discs close the ends that have a radius, a hard edge even when the sides are smooth
2959
+ if (capped && !closed)
2960
+ for (const [i, up] of /** @type {Array<[number, boolean]>} */ ([[0, false], [rings - 1, true]]))
2961
+ {
2962
+ if (abs(profile[i][0]) < 1e-9) continue; // a pole has no cap
2963
+ const points = [], uvs = [];
2964
+ for (let j = 0; j < sides; ++j)
2965
+ {
2966
+ const a = (up ? j : -j) / sides * 2 * PI; // counter clockwise seen from outside
2967
+ points.push(point(i, a));
2968
+ uvs.push(vec2(sin(a) * .5 + .5, cos(a) * .5 + .5));
2969
+ }
2970
+ mesh.addStrip(render3DPolygonStrip(points), vec3(0, up ? 1 : -1, 0), render3DPolygonStrip(uvs));
2971
+ }
2972
+
2973
+ // an end left open shows the inside, so it is seen from both sides
2974
+ mesh.doubleSided = !closed && !capped && (abs(profile[0][0]) > 1e-9 || abs(profile[rings-1][0]) > 1e-9);
2975
+ return mesh;
2976
+ }
2977
+
2978
+ /**
2979
+ * Build a sphere centered on the origin
2980
+ * @param {number} [size] - Diameter
2981
+ * @param {number} [sides] - Around
2982
+ * @param {number} [rings] - Top to bottom
2983
+ * @param {boolean} [smooth] - Defaults to render3D.smoothShading
2984
+ * @return {Mesh}
2985
+ * @memberof Render3D
2986
+ */
2987
+ function buildSphere(size=1, sides=16, rings=8, smooth=render3D?.smoothShading)
2988
+ {
2989
+ ASSERT(rings > 1, 'sphere needs at least 2 rings');
2990
+ const profile = [];
2991
+ for (let i = 0; i <= rings; ++i)
2992
+ {
2993
+ const a = i / rings * PI - PI/2;
2994
+ profile.push([cos(a) * size / 2, sin(a) * size / 2]);
2995
+ }
2996
+ return buildLathe(profile, sides, smooth);
2997
+ }
2998
+
2999
+ // a box with its edges cut by a bevel of size t: each face shrunk by t, a strip along each edge around a quarter
3000
+ // circle, and an eighth of a sphere at each corner; one segment makes them flat, a chamfer
3001
+ function render3DBevelBox(half, t, segments, faces)
3002
+ {
3003
+ const mesh = new Mesh, inner = half.subtract(vec3(t)), n = max(1, segments | 0);
3004
+
3005
+ // a triangle facing out, since the box is convex around the origin, flat shaded or with the normals of the
3006
+ // round, and uvs from the face it faces most, as a plain box maps that face
3007
+ const triangle = (a, b, c, na, nb, nc)=>
3008
+ {
3009
+ let normal = b.subtract(a).cross(c.subtract(a));
3010
+ if (normal.lengthSquared() < 1e-20) return; // an edge or face with no size
3011
+ if (normal.dot(a.add(b).add(c)) < 0)
3012
+ [b, c, nb, nc, normal] = [c, b, nc, nb, normal.scale(-1)];
3013
+ normal = normal.normalize();
3014
+ let face = faces[0];
3015
+ for (const f of faces)
3016
+ if (f[0].dot(normal) > face[0].dot(normal))
3017
+ face = f;
3018
+ const [, r, u] = face, hr = abs(half.dot(r)), hu = abs(half.dot(u));
3019
+ const uv = (p)=> vec2(.5 + p.dot(r) / (2 * hr), .5 - p.dot(u) / (2 * hu));
3020
+ mesh.addStrip([a, b, c], n > 1 && na ? [na, nb, nc] : normal, [uv(a), uv(b), uv(c)]);
3021
+ };
3022
+
3023
+ // the faces, shrunk by the bevel
3024
+ for (const [normal, r, u] of faces)
3025
+ {
3026
+ const [a, b, c, d] = render3DQuadAxes(normal.multiply(half), r.multiply(inner), u.multiply(inner));
3027
+ triangle(a, b, c);
3028
+ triangle(c, b, d);
3029
+ }
3030
+
3031
+ // the edges, each a strip around a quarter circle from one face to the next
3032
+ const axes = [vec3(1, 0, 0), vec3(0, 1, 0), vec3(0, 0, 1)];
3033
+ for (let k = 0; k < 3; ++k)
3034
+ for (const si of [-1, 1])
3035
+ for (const sj of [-1, 1])
3036
+ {
3037
+ const n1 = axes[(k + 1) % 3].scale(si), n2 = axes[(k + 2) % 3].scale(sj);
3038
+ const middle = n1.multiply(inner).add(n2.multiply(inner)), along = axes[k].multiply(inner);
3039
+ const start = middle.subtract(along), end = middle.add(along);
3040
+ const dir = (m)=> n1.scale(cos(m / n * PI / 2)).add(n2.scale(sin(m / n * PI / 2)));
3041
+ for (let m = 0; m < n; ++m)
3042
+ {
3043
+ const d0 = dir(m), d1 = dir(m + 1);
3044
+ const a = start.add(d0.scale(t)), b = end.add(d0.scale(t));
3045
+ const c = start.add(d1.scale(t)), d = end.add(d1.scale(t));
3046
+ triangle(a, b, c, d0, d0, d1);
3047
+ triangle(c, b, d, d1, d0, d1);
3048
+ }
3049
+ }
3050
+
3051
+ // the corners, an eighth of a sphere in rows from one face's axis down to the edge between the other two
3052
+ for (const sx of [-1, 1])
3053
+ for (const sy of [-1, 1])
3054
+ for (const sz of [-1, 1])
3055
+ {
3056
+ const nx = vec3(sx, 0, 0), ny = vec3(0, sy, 0), nz = vec3(0, 0, sz), corner = inner.multiply(vec3(sx, sy, sz));
3057
+ const dir = (i, j)=>
3058
+ {
3059
+ const polar = i / n * PI / 2, around = i ? j / i * PI / 2 : 0;
3060
+ return nz.scale(cos(polar)).add(nx.scale(cos(around) * sin(polar))).add(ny.scale(sin(around) * sin(polar)));
3061
+ };
3062
+ const point = (i, j)=> corner.add(dir(i, j).scale(t));
3063
+ for (let i = 0; i < n; ++i)
3064
+ {
3065
+ for (let j = 0; j <= i; ++j)
3066
+ triangle(point(i, j), point(i + 1, j), point(i + 1, j + 1), dir(i, j), dir(i + 1, j), dir(i + 1, j + 1));
3067
+ for (let j = 0; j < i; ++j)
3068
+ triangle(point(i, j), point(i + 1, j + 1), point(i, j + 1), dir(i, j), dir(i + 1, j + 1), dir(i, j + 1));
3069
+ }
3070
+ }
3071
+ return mesh;
3072
+ }
3073
+
3074
+ /**
3075
+ * Build a box centered on the origin, six flat faces with uvs covering each face
3076
+ * - bevel cuts its edges and corners: 1 segment is a flat chamfer, more round them, and the biggest bevel, half
3077
+ * the smallest side, rounds a cube into a ball
3078
+ * @param {Vector3|number} [size] - Full size, a number for a cube
3079
+ * @param {number} [bevel] - Size of the cut on each edge, clamped to half the smallest side
3080
+ * @param {number} [bevelSegments] - Steps around each edge, 1 for a flat chamfer
3081
+ * @return {Mesh}
3082
+ * @memberof Render3D
3083
+ */
3084
+ function buildBox(size=1, bevel=0, bevelSegments=1)
3085
+ {
3086
+ ASSERT(isNumber(bevel) && bevel >= 0, 'bevel must be a number, 0 or more');
3087
+ const half = render3DSize3(size).scale(.5);
3088
+ // each face: normal, right axis, up axis (right cross up = normal)
3089
+ const faces = [
3090
+ [vec3(0, 0, 1), vec3(1, 0, 0), vec3(0, 1, 0)],
3091
+ [vec3(0, 0, -1), vec3(-1, 0, 0), vec3(0, 1, 0)],
3092
+ [vec3(1, 0, 0), vec3(0, 0, -1), vec3(0, 1, 0)],
3093
+ [vec3(-1, 0, 0), vec3(0, 0, 1), vec3(0, 1, 0)],
3094
+ [vec3(0, 1, 0), vec3(1, 0, 0), vec3(0, 0, -1)],
3095
+ [vec3(0, -1, 0), vec3(1, 0, 0), vec3(0, 0, 1)],
3096
+ ];
3097
+ const t = min(bevel, half.x, half.y, half.z);
3098
+ if (t > 0)
3099
+ return render3DBevelBox(half, t, bevelSegments, faces);
3100
+ const mesh = new Mesh;
3101
+ for (const [n, r, u] of faces)
3102
+ {
3103
+ const center = n.multiply(half);
3104
+ const right = r.multiply(half), up = u.multiply(half);
3105
+ mesh.addStrip(render3DQuadAxes(center, right, up), n, RENDER3D_QUAD_UVS);
3106
+ }
3107
+ return mesh;
3108
+ }
3109
+
3110
+ /**
3111
+ * Build a heightfield grid in the XZ plane centered on the origin
3112
+ * - smooth rounds the lighting across cells and colors each corner
3113
+ * - flat lights and colors each cell on its own, so a checkerboard stays crisp
3114
+ * - doubleSided, a sheet seen from both sides; turn it off for ground only ever seen from above
3115
+ * - One cell is a plain square, render3D.planeMesh and planeMeshDoubleSided are shared ones
3116
+ * @param {Vector2|number} [size] - World size along X and Z, a number for a square
3117
+ * @param {Vector2|number} [segments] - Cells along X and Z, a number for both
3118
+ * @param {Color|function(number, number): Color} [color] - One Color for the whole grid, or (x, z) => Color
3119
+ * @param {function(number, number): number} [heightFunction] - (x, z) => y, default flat
3120
+ * @param {boolean} [smooth] - Defaults to render3D.smoothShading with a heightFunction; a flat grid is flat shaded,
3121
+ * since its light is even anyway and smoothing would only blend its cell colors; pass smooth to blend a gradient
3122
+ * @return {Mesh}
3123
+ * @memberof Render3D
3124
+ * @example
3125
+ * const ground = buildGrid(vec2(20), 10, (x, z)=> (floor(x / 2) + floor(z / 2)) & 1 ? GRAY : WHITE); // 2 unit checks
3126
+ */
3127
+ function buildGrid(size=vec2(1), segments=1, color, heightFunction, smooth=heightFunction && render3D?.smoothShading)
3128
+ {
3129
+ heightFunction ||= ()=> 0;
3130
+ size = render3DSize2(size);
3131
+ segments = render3DSize2(segments);
3132
+ ASSERT(segments.x > 0 && segments.y > 0 && segments.x % 1 === 0 && segments.y % 1 === 0,
3133
+ 'grid segments must be whole numbers above zero');
3134
+ const mesh = new Mesh;
3135
+ const segmentsX = segments.x, segmentsZ = segments.y;
3136
+ const cellX = size.x / segmentsX, cellZ = size.y / segmentsZ;
3137
+ const halfX = size.x / 2, halfZ = size.y / 2, ex = cellX / 2, ez = cellZ / 2;
3138
+ const px = (i)=> i * cellX - halfX, pz = (j)=> j * cellZ - halfZ;
3139
+ const colorAt = /** @type {function(number, number): Color} */ (color);
3140
+ const cellColor = (i, j)=> !color ? WHITE : isColor(color) ? /** @type {Color} */ (color) : colorAt(px(i), pz(j));
3141
+ // a big terrain has millions of vertices, so each one is made once, shared by the rows above and below it
3142
+ const row = (j)=>
3143
+ {
3144
+ const points = [], normals = [], uvs = [], colors = [], z = pz(j);
3145
+ for (let i = 0; i <= segmentsX; ++i)
3146
+ {
3147
+ const x = px(i);
3148
+ points.push(vec3(x, heightFunction(x, z), z));
3149
+ uvs.push(vec2(i / segmentsX, j / segmentsZ));
3150
+ if (!smooth) continue;
3151
+ normals.push(render3DSlopeNormal(heightFunction, x, z, ex, ez, halfX, halfZ));
3152
+ colors.push(cellColor(i, j));
3153
+ }
3154
+ return {points, normals, uvs, colors};
3155
+ };
3156
+ let above = row(0);
3157
+ for (let j = 0; j < segmentsZ; ++j)
3158
+ {
3159
+ const below = row(j + 1);
3160
+ if (smooth)
3161
+ {
3162
+ // one ribbon per row with vertex normals from the slope
3163
+ const points = [], normals = [], uvs = [], colors = [];
3164
+ for (let i = 0; i <= segmentsX; ++i)
3165
+ {
3166
+ points.push(above.points[i], below.points[i]);
3167
+ normals.push(above.normals[i], below.normals[i]);
3168
+ uvs.push(above.uvs[i], below.uvs[i]);
3169
+ colors.push(above.colors[i], below.colors[i]);
3170
+ }
3171
+ mesh.addStrip(points, normals, uvs, colors);
3172
+ }
3173
+ else
3174
+ {
3175
+ // one quad per cell with its face normal and one color sampled at its center
3176
+ for (let i = 0; i < segmentsX; ++i)
3177
+ mesh.addQuad(above.points[i], below.points[i], below.points[i+1], above.points[i+1], cellColor(i + .5, j + .5),
3178
+ [above.uvs[i], below.uvs[i], below.uvs[i+1], above.uvs[i+1]]);
3179
+ }
3180
+ above = below;
3181
+ }
3182
+ if (smooth)
3183
+ {
3184
+ // the grid knows which strip entries are one vertex, each shared by the rows above and below it and by the
3185
+ // repeats at the ends of its ribbons, so the upload of a big terrain skips searching millions of entries
3186
+ const n = 2 * (segmentsX + 1) + 2, keys = mesh.vertexKeys = new Int32Array(mesh.points.length);
3187
+ const id = (i, j)=> j * (segmentsX + 1) + i;
3188
+ for (let j = 0; j < segmentsZ; ++j)
3189
+ {
3190
+ const start = j * n;
3191
+ keys[start] = id(0, j); // the leading repeat
3192
+ for (let i = 0; i <= segmentsX; ++i)
3193
+ keys[start + 1 + 2*i] = id(i, j), keys[start + 2 + 2*i] = id(i, j + 1);
3194
+ keys[start + n - 1] = id(segmentsX, j + 1); // the trailing repeat
3195
+ }
3196
+ }
3197
+ mesh.doubleSided = true; // a sheet, seen from both sides; terrain seen only from above can turn it off
3198
+ return mesh;
3199
+ }
3200
+
3201
+ /**
3202
+ * Build a sky dome: a sphere colored by direction, wound to be seen from inside
3203
+ * - set it as render3D.sky and the pass draws it around the camera behind everything
3204
+ * @param {Color} [topColor] - Straight up
3205
+ * @param {Color} [horizonColor] - Level with the camera
3206
+ * @param {Color} [bottomColor] - Straight down, what a camera looking at the ground sees past its edge; defaults to the
3207
+ * horizon color
3208
+ * @param {number} [sides] - Around
3209
+ * @param {number} [rings] - Top to bottom
3210
+ * @return {Mesh}
3211
+ * @memberof Render3D
3212
+ */
3213
+ function buildSky(topColor=hsl(.6, .8, .55), horizonColor=hsl(.6, 1, .9), bottomColor=horizonColor, sides=16, rings=8)
3214
+ {
3215
+ // a sphere turned inside out so it is seen from within, each point colored by how high it is
3216
+ const mesh = buildSphere(2, sides, rings, true).flipNormals();
3217
+ mesh.colors = mesh.points.map(p=> p.y < 0 ? horizonColor.lerp(bottomColor, -p.y) : horizonColor.lerp(topColor, p.y));
3218
+ return mesh;
3219
+ }
3220
+
3221
+ ///////////////////////////////////////////////////////////////////////////////
3222
+ /**
3223
+ * EngineObject3D - An EngineObject with a 3D transform and a mesh
3224
+ * - Set pos3D, rotation3D and scale3D instead of the 2D pos, size and angle
3225
+ * - Gets update, children, timers, destroy and renderOrder from EngineObject
3226
+ * - velocity3D is added to pos3D each frame, along with render3D.gravity and damping once it has a mass
3227
+ * - Objects face -Z, the same way the camera does, so lookAt turns them to face a point
3228
+ * - The 2D pos and velocity are still there but nothing draws them
3229
+ * - These inherited fields are 2D only and do nothing here: angle, angleVelocity, angleDamping,
3230
+ * additiveColor, drawSize, mirror, clampSpeed, friction and groundObject
3231
+ * - The inherited shader works here as in 2D, and with emissive at 1 its snippet does its own lighting
3232
+ * - Set sync2D for a 2D game with 3D looks, pos and angle then drive pos3D and rotation3D,
3233
+ * which is the one way those 2D fields reach a 3D object
3234
+ * - setCollision takes the same flags as in 2D, but the solid collision happens in 3D against size3D
3235
+ * - The solid box is axis aligned in the world, rotation3D is ignored like angle is in 2D, so give a turned wall a
3236
+ * size3D along the world axes
3237
+ * - Its tile and raycast halves are 2D only so they default off here, and a child sits solid collision out
3238
+ * - A sync2D object collides in 2D instead, which needs the 2D size set as well as size3D
3239
+ * - setMesh swaps the mesh and frees the old one, for text and terrain that get built again
3240
+ * - addChild attaches the 3D transform, and pos3D becomes an offset from the parent; attach keeps the child where
3241
+ * it is and works the offset out, and removeChild leaves it where it was in the world
3242
+ * - The 2D offset arguments of addChild do nothing here, set the child's pos3D
3243
+ * @extends EngineObject
3244
+ * @memberof Render3D
3245
+ * @example
3246
+ * class Spinner extends EngineObject3D
3247
+ * {
3248
+ * constructor(pos) { super(pos, buildBox(), undefined, RED); }
3249
+ * update() { this.rotation3D.y += .02; }
3250
+ * }
3251
+ */
3252
+ class EngineObject3D extends EngineObject
3253
+ {
3254
+ /** Create a 3D object and add it to the object list
3255
+ * @param {Vector3} [pos3D] - World space position
3256
+ * @param {Mesh} [mesh] - Mesh to draw, undefined draws nothing
3257
+ * @param {TileInfo|TextureInfo} [tileInfo] - Texture, mesh uvs map across the tile; a whole TextureInfo becomes
3258
+ * the tile that covers it
3259
+ * @param {Color} [color] - Tint */
3260
+ constructor(pos3D=vec3(), mesh, tileInfo, color=WHITE)
3261
+ {
3262
+ ASSERT(!tileInfo || tileInfo instanceof TileInfo || tileInfo instanceof TextureInfo,
3263
+ 'tileInfo must be a TileInfo or TextureInfo, it comes before color');
3264
+ // a whole texture is stored as the tile that covers it, with no padding or bleed to trim
3265
+ // the edges, so this is always a TileInfo like the 2D one and the object stays an EngineObject
3266
+ if (tileInfo instanceof TextureInfo)
3267
+ render3DWholeTiles.add(tileInfo = new TileInfo(vec2(), tileInfo.size, tileInfo, 0, 0));
3268
+ // the 2D pos starts where the object is, so turning on sync2D keeps it there
3269
+ super(vec2(pos3D.x, pos3D.y), vec2(), tileInfo, 0, color);
3270
+ ASSERT(isVector3(pos3D), 'pos3D must be a vec3');
3271
+ ASSERT(!mesh || mesh instanceof Mesh, 'mesh must be a Mesh or undefined');
3272
+ this.mass = 0; // static: no 2D physics, and no 3D gravity until a mass is set
3273
+
3274
+ /** @property {Vector3} - World space position, local to the parent when attached to an EngineObject3D */
3275
+ this.pos3D = pos3D.copy();
3276
+ /** @property {Vector3} - Rotation vec3(pitch, yaw, roll) in radians, local to the parent when attached to an
3277
+ * EngineObject3D */
3278
+ this.rotation3D = vec3();
3279
+ /** @property {Vector3} - Scale, local to the parent when attached to an EngineObject3D */
3280
+ this.scale3D = vec3(1);
3281
+ /** @property {Vector3} - Added to pos3D each frame by the engine before update, like the 2D velocity, no super
3282
+ * call needed; damping and render3D.gravity act on it once the object has a mass */
3283
+ this.velocity3D = vec3();
3284
+ /** @property {Vector3} - Added to rotation3D each frame by the engine before update, angleDamping is 2D only */
3285
+ this.angleVelocity3D = vec3();
3286
+ /** @property {Mesh|undefined} - Mesh to draw
3287
+ * @type {Mesh|undefined} */
3288
+ this.mesh = mesh;
3289
+ /** @property {Vector3} - Size for solid collision and the collect and callback helpers, and of the sprite when
3290
+ * there is a tileInfo and no mesh; starts at the size of the mesh's box, or 1 with no mesh, and setMesh leaves
3291
+ * it as it is; the box is centered on pos3D, so center() a mesh whose origin is not its middle, like a model
3292
+ * standing on its feet, or set size3D; scale3D and any parent's scale grow it, so drawing and picking agree */
3293
+ // a shared mesh measured since it last changed is not walked again for each object made from it
3294
+ const bounds = mesh && mesh.points.length ? !mesh.dirty && mesh.bounds || mesh.getBounds() : undefined;
3295
+ this.size3D = bounds ? bounds.max.subtract(bounds.min) : vec3(1);
3296
+ /** @property {number} - Diameter of a soft shadow drawn under the object on render3D.softShadowHeight, 0 for
3297
+ * none; scale3D and a parent's scale grow it, so set it once for the unscaled object */
3298
+ this.softShadow = 0;
3299
+ /** @property {boolean} - A sprite stands on world up instead of tilting toward the camera */
3300
+ this.upright = false;
3301
+ /** @property {boolean} - Keep this object's texture pixels hard edged, for pixel art that should not blur or bleed */
3302
+ this.pixelated = false;
3303
+ /** @property {boolean} - Copy the 2D pos and angle into pos3D and rotation3D each frame, for 2D games with 3D
3304
+ * looks; set mass to use 2D physics, and pos3D.z stays yours to set or move with velocity3D.z */
3305
+ this.sync2D = false;
3306
+ /** @property {boolean} - Draw in the transparent stage, blended and sorted far to near with depth writes off;
3307
+ * on for a sprite */
3308
+ this.transparent = !mesh && !!tileInfo;
3309
+ /** @property {boolean} - Additive blending, in the transparent stage */
3310
+ this.additive = false;
3311
+ /** @property {number} - How much it lights itself: 0 is lit as normal, 1 is its own color with no shading, for
3312
+ * lamps and glowing things, between is partly self lit, and above 1 is brighter than its color, for bloom */
3313
+ this.emissive = 0;
3314
+ /** @property {number} - Strength of the highlight where the sun and the Light3D objects reflect, 0 is none and
3315
+ * 1 adds a light's full color at its brightest; its size is fixed */
3316
+ this.specular = 0;
3317
+ /** @property {boolean} - Draw into the shadow map when render3D.shadows is on; sprites and cut out textures
3318
+ * cast their outline, an object faded below half its alpha casts nothing, a see through one casts only when
3319
+ * textured, and additive objects never cast */
3320
+ this.castShadow = true;
3321
+ /** @property {boolean} - Collide as the sphere that fits size3D instead of as the size3D box, so it rolls
3322
+ * around corners */
3323
+ this.collideAsSphere3D = false;
3324
+ /** @property {boolean} - Darkened by the shadow map when render3D.shadows is on */
3325
+ this.receiveShadow = true;
3326
+ /** @property {boolean|undefined} - Draw this object over the 2D scene, undefined uses render3D.renderAfter2D
3327
+ * @type {boolean|undefined} */
3328
+ this.renderAfter2D = undefined;
3329
+ /** @property {Matrix4|undefined} - The transform from its parent, used in place of pos3D, rotation3D and
3330
+ * scale3D when set, for one they cannot hold like a glTF pose with shear; read every frame it is set
3331
+ * @type {Matrix4|undefined} */
3332
+ this.localMatrix = undefined;
3333
+ // the world transform, kept up to date by render3DObjectMatrix; getMatrix returns a copy
3334
+ this.worldMatrix = new Matrix4;
3335
+ this.matrixBuilt = new Float64Array(9).fill(NaN); // the position, rotation and scale it was built from
3336
+ this.matrixVersion = 0; // counts the rebuilds, so a child knows when its parent's changed
3337
+ /** @type {EngineObject3D|undefined} */
3338
+ this.matrixParent = undefined; // the parent it was built under, and that parent's version then
3339
+ this.matrixParentVersion = 0;
3340
+ this.movePass = engineObjectsUpdateCount; // the engine pass a child last moved in, so a refresh is not a step
3341
+ }
3342
+
3343
+ /** Move by the 3D velocities and push out of solids, called automatically each frame before update, like the 2D physics
3344
+ * - update runs once every object has moved and collided, so bounce off anything else there, it lands before the draw
3345
+ * - Override this and call super to change how the object moves itself
3346
+ * - A sync2D object runs the 2D physics as well, and collides there instead */
3347
+ updatePhysics()
3348
+ {
3349
+ // a sync2D object collides in 2D, which measures the 2D size, and that starts at zero on a 3D object
3350
+ ASSERT(!this.sync2D || !this.collideSolidObjects || (this.size.x && this.size.y),
3351
+ 'a sync2D object collides in 2D, so give it a 2D size as well as a size3D', this.size);
3352
+ if (this.sync2D)
3353
+ super.updatePhysics();
3354
+ // a moving object keeps out of the level, the height maps and voxel maps, from where it was before it moved
3355
+ const oldPos = this.collideLevel && this.mass && !this.sync2D ? this.pos3D.copy() : undefined;
3356
+ render3DMove(this);
3357
+ oldPos && render3DCollideLevel(this, oldPos);
3358
+ // the engine only runs this for objects that own where they are, a child rides along with its parent
3359
+ if (this.collideSolidObjects && !this.sync2D)
3360
+ render3DCollideSolid(this);
3361
+ }
3362
+
3363
+ /** Move a child by its own velocities, bring a sync2D object's pos3D up to its 2D pos, then update the children,
3364
+ * called automatically each frame
3365
+ * @param {boolean} [updateChildren] - Also update the children's transforms */
3366
+ updateTransforms(updateChildren=true)
3367
+ {
3368
+ if (!paused)
3369
+ {
3370
+ // a child is never given updatePhysics, so it moves here, as an offset from its parent: once per
3371
+ // engine pass, since addChild, attach and a game bring the transforms up to date too
3372
+ if (this.parent && this.movePass !== engineObjectsUpdateCount)
3373
+ {
3374
+ this.movePass = engineObjectsUpdateCount;
3375
+ render3DMove(this);
3376
+ }
3377
+ }
3378
+
3379
+ // placed from its parent first, so a sync2D object copies where it is now, then its children from it
3380
+ super.updateTransforms(false);
3381
+ if (!paused && this.sync2D)
3382
+ this.pos3D.x = this.pos.x, this.pos3D.y = this.pos.y, this.rotation3D.z = -this.angle;
3383
+ if (updateChildren)
3384
+ for (const child of this.children)
3385
+ child.updateTransforms();
3386
+ }
3387
+
3388
+ /** Set how this object collides, the same flags as in 2D
3389
+ * - Solid collision happens in 3D here, against size3D boxes or spheres; a child sits it out
3390
+ * - The boxes are axis aligned in the world, rotation3D is ignored
3391
+ * - A sync2D object collides in 2D instead, against the 2D size, so set that as well as size3D
3392
+ * @param {boolean} [collideSolidObjects] - Take part in solid collision
3393
+ * @param {boolean} [isSolid] - Block other objects, a pair where neither one blocks passes through;
3394
+ * blocking needs collideSolidObjects, so isSolid on its own is not allowed
3395
+ * @param {boolean} [collideLevel] - Collide with the level, the height maps and voxel maps, or the 2D tile layers
3396
+ * for a sync2D object
3397
+ * @param {boolean} [collideRaycast] - Raycasts, 2D only; 3D has render3D.pick and engineObjectsRaycast3D */
3398
+ setCollision(collideSolidObjects=true, isSolid=true, collideLevel=true, collideRaycast=false)
3399
+ { super.setCollision(collideSolidObjects, isSolid, collideLevel, collideRaycast); }
3400
+
3401
+ /** Called by a VoxelMap to ask whether a block stops this object, a hook to let one through or react to it
3402
+ * @param {number} type - The block's type, 1 to 255
3403
+ * @param {Vector3} cell - The block's cell in the map
3404
+ * @return {boolean} - true to be stopped by it, every block stops it by default */
3405
+ collideWithVoxel(type, cell) { return true; }
3406
+
3407
+ /** Returns the world position
3408
+ * @return {Vector3} */
3409
+ getWorldPos3D() { return render3DObjectMatrix(this).getTranslation(); }
3410
+
3411
+ /** Returns the direction the object faces, its -Z axis in the world
3412
+ * @return {Vector3} */
3413
+ getForward3D() { return render3DAxis(render3DObjectMatrix(this).m, 8).normalize(-1); }
3414
+
3415
+ /** Returns the object's right axis in the world
3416
+ * @return {Vector3} */
3417
+ getRight3D() { return render3DAxis(render3DObjectMatrix(this).m, 0).normalize(); }
3418
+
3419
+ /** Returns the object's up axis in the world
3420
+ * @return {Vector3} */
3421
+ getUp3D() { return render3DAxis(render3DObjectMatrix(this).m, 4).normalize(); }
3422
+
3423
+ /** Returns a copy of the object's world transform, the parent's included when attached to an EngineObject3D
3424
+ * - The object keeps its matrix and rebuilds it only when its position, rotation or scale changed, so this is
3425
+ * cheap to call
3426
+ * @return {Matrix4} */
3427
+ getMatrix() { return render3DObjectMatrix(this).copy(); }
3428
+
3429
+ /** Turn the object so its -Z axis points at a world space target, sets pitch and yaw and clears roll
3430
+ * @param {Vector3} target */
3431
+ lookAt(target)
3432
+ {
3433
+ // rotation3D is local to the parent, so a child has to aim at the target from the parent's point of view
3434
+ const parent = this.parent instanceof EngineObject3D ? this.parent : undefined;
3435
+ const local = parent ? parent.getMatrix().invert().transformPoint(target) : target;
3436
+ this.rotation3D = render3DLookRotation(local.subtract(this.pos3D), this.rotation3D);
3437
+ }
3438
+
3439
+ /** Attaches a child, its pos3D, rotation3D and scale3D taken as the offset from this one; returns child for chaining
3440
+ * - The 2D offset arguments do nothing for an EngineObject3D child, set its pos3D
3441
+ * @param {EngineObject} child
3442
+ * @param {Vector2} [localPos]
3443
+ * @param {number} [localAngle]
3444
+ * @return {EngineObject} The child object attached */
3445
+ addChild(child, localPos, localAngle)
3446
+ {
3447
+ // addChild brings the child's transform up to date, which is not a step: it already moved this pass as
3448
+ // a root, or moves in the next one, so it must not move by its velocity again here
3449
+ if (child instanceof EngineObject3D)
3450
+ child.movePass = engineObjectsUpdateCount;
3451
+ return super.addChild(child, localPos, localAngle);
3452
+ }
3453
+
3454
+ /** Attaches a child without moving it: its pos3D, rotation3D and scale3D become what they have to be under this
3455
+ * parent to keep its world transform, where addChild takes them as the offset; returns child for chaining
3456
+ * - A parent scaled unevenly and a child turned under it make a shear, which those three values cannot hold,
3457
+ * so the child comes out as close as they can get; a uniform scale is exact
3458
+ * @param {EngineObject} child
3459
+ * @return {EngineObject} The child object attached */
3460
+ attach(child)
3461
+ {
3462
+ if (!(child instanceof EngineObject3D))
3463
+ return super.attach(child); // a 2D child only has the 2D transform to keep
3464
+ child.parent?.removeChild(child); // keeps its world values, so its own matrix is its world matrix
3465
+ const local = render3DObjectMatrix(this).copy().invert().multiply(render3DObjectMatrix(child));
3466
+ super.attach(child);
3467
+ child.pos3D = local.getTranslation();
3468
+ child.rotation3D = local.getRotation();
3469
+ child.scale3D = local.getScale();
3470
+ if (child.localMatrix)
3471
+ child.localMatrix = local; // a matrix given whole stays whole, shear and all
3472
+ return child;
3473
+ }
3474
+
3475
+ /** Removes a child from this one, it stays where it is in the world: its pos3D, rotation3D and scale3D become
3476
+ * its world values, with the same shear caveat as attach; a child being destroyed is let go as it is
3477
+ * @param {EngineObject} child */
3478
+ removeChild(child)
3479
+ {
3480
+ if (child instanceof EngineObject3D && !child.destroyed)
3481
+ {
3482
+ const world = render3DObjectMatrix(child);
3483
+ child.pos3D = world.getTranslation();
3484
+ child.rotation3D = world.getRotation();
3485
+ child.scale3D = world.getScale();
3486
+ if (child.localMatrix)
3487
+ child.localMatrix = world.copy(); // a matrix given whole stays whole, shear and all
3488
+ }
3489
+ super.removeChild(child);
3490
+ }
3491
+
3492
+ /** Draw a different mesh and free the GPU buffer of the one it replaces
3493
+ * - For a mesh built again when something changes, like a score, a rebuilt terrain or a loaded model
3494
+ * - A mesh another object is still drawing is left alone, since builders are often shared
3495
+ * - Freeing one held somewhere else only costs it an upload, the points it was built from stay
3496
+ * @param {Mesh} [mesh] - The mesh to draw from now on, undefined to draw nothing
3497
+ * @return {Mesh|undefined} - The mesh passed in */
3498
+ setMesh(mesh)
3499
+ {
3500
+ ASSERT(!mesh || mesh instanceof Mesh, 'mesh must be a Mesh or undefined');
3501
+ const old = this.mesh;
3502
+ this.mesh = mesh;
3503
+ // nothing to free and nothing to look for when it was never uploaded
3504
+ if (old && old !== mesh && old.buffer && !engineObjects.some(o=> /** @type {EngineObject3D} */ (o).mesh === old))
3505
+ old.dispose();
3506
+ return mesh;
3507
+ }
3508
+
3509
+ /** 2D rendering is skipped, the mesh is drawn by render3D during the 3D pass */
3510
+ render() {}
3511
+
3512
+ /** 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
3513
+ * @return {void} */
3514
+ render3D()
3515
+ {
3516
+ // an opaque draw comes out solid however low its alpha is, so a fade with no flag looks like nothing happened
3517
+ ASSERT(this.transparent || this.additive || this.color.a >= 1,
3518
+ 'an object that fades needs its transparent flag, an opaque draw ignores the color alpha', this.color);
3519
+ // the matrix the object keeps, rebuilt only when it moved, the same one for the shadow pass and the main pass
3520
+ const matrix = render3DObjectMatrix(this);
3521
+ if (this.mesh)
3522
+ render3D.drawMesh(this.mesh, matrix, this.tileInfo, this.color);
3523
+ else if (this.tileInfo)
3524
+ {
3525
+ // a sprite, at the world size the collect, pick and solid collision helpers measure it at
3526
+ const m = matrix.m, size = render3DWorldSize(this, m);
3527
+ render3D.drawBillboard(vec3(m[12], m[13], m[14]), vec2(size.x, size.y), this.tileInfo, this.color,
3528
+ this.rotation3D.z, this.upright);
3529
+ }
3530
+ }
3531
+ }
3532
+
3533
+ // an object's world matrix, the one it keeps: rebuilt only when its position, rotation or scale changed since the
3534
+ // last build, or its parent's matrix did, so an object that stands still costs nine compares a frame instead of
3535
+ // the trig and a new matrix; the matrix returned is the object's own, read it and never change it
3536
+ const render3DLocalMatrix = new Matrix4;
3537
+ function render3DObjectMatrix(o)
3538
+ {
3539
+ const parent = o.parent instanceof EngineObject3D ? o.parent : undefined;
3540
+ const parentMatrix = parent && render3DObjectMatrix(parent); // the parent first, so its version is current
3541
+ const p = o.pos3D, r = o.rotation3D, s = o.scale3D, k = o.matrixBuilt, local = o.localMatrix;
3542
+ if (local)
3543
+ {
3544
+ // a matrix given whole is taken as it is each time, it can change in place
3545
+ if (parent)
3546
+ {
3547
+ o.worldMatrix.m.set(parentMatrix.m);
3548
+ o.worldMatrix.multiply(local);
3549
+ o.matrixParentVersion = parent.matrixVersion;
3550
+ }
3551
+ else
3552
+ o.worldMatrix.m.set(local.m);
3553
+ k[0] = NaN; // built from the matrix, so going back to pos3D builds again
3554
+ o.matrixParent = parent;
3555
+ ++o.matrixVersion;
3556
+ }
3557
+ 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
3558
+ || k[6] !== s.x || k[7] !== s.y || k[8] !== s.z || o.matrixParent !== parent
3559
+ || parent && o.matrixParentVersion !== parent.matrixVersion)
3560
+ {
3561
+ 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;
3562
+ if (parent)
3563
+ {
3564
+ // the parent's world matrix times the local one
3565
+ buildMatrix(p, r, s, render3DLocalMatrix);
3566
+ o.worldMatrix.m.set(parentMatrix.m);
3567
+ o.worldMatrix.multiply(render3DLocalMatrix);
3568
+ o.matrixParentVersion = parent.matrixVersion;
3569
+ }
3570
+ else
3571
+ buildMatrix(p, r, s, o.worldMatrix);
3572
+ o.matrixParent = parent;
3573
+ ++o.matrixVersion;
3574
+ }
3575
+ return o.worldMatrix;
3576
+ }
3577
+
3578
+ // move an object by its 3D velocities, an object with mass falling with render3D.gravity and slowing by its damping
3579
+ function render3DMove(o)
3580
+ {
3581
+ // the vectors change in place, as the 2D object's do: this runs for every object every frame
3582
+ const p = o.pos3D, v = o.velocity3D, r = o.rotation3D, a = o.angleVelocity3D;
3583
+ if (o.mass && !o.sync2D) // a 2D driven object gets the 2D gravity instead
3584
+ {
3585
+ // damped first and gravity added after, the order EngineObject.updatePhysics uses,
3586
+ // so the same mass, damping and gravity fall the same way in both
3587
+ const g = render3D.gravity, s = o.gravityScale, d = o.damping;
3588
+ v.x = v.x * d + g.x * s, v.y = v.y * d + g.y * s, v.z = v.z * d + g.z * s;
3589
+ }
3590
+ p.x += v.x, p.y += v.y, p.z += v.z;
3591
+ r.x += a.x, r.y += a.y, r.z += a.z;
3592
+ }
3593
+
3594
+ // keep an object that moved out of the level's solid geometry, clearing what it stood on for the level to set again
3595
+ function render3DCollideLevel(o, oldPos)
3596
+ {
3597
+ const ground = o.groundObject;
3598
+ o.groundObject = undefined;
3599
+ for (const level of render3DLevel)
3600
+ level.destroyed || level.levelCollide3D(o, oldPos, level === ground);
3601
+ }
3602
+
3603
+ // where a solid object is in the world and what it collides as: the sphere that fits size3D, or the size3D box,
3604
+ // each grown by the object's scale
3605
+ // only objects that own where they are take part, so pos3D is already world space, and however the object is
3606
+ // turned its axes come out as long as its scale makes them; building the transform to read that back off it
3607
+ // costs six trig calls and a matrix for every pair tested, which is the whole cost of a crowded scene
3608
+ function render3DSolidShape(o)
3609
+ {
3610
+ ASSERT(!o.parent, 'a child rides along with its parent, it has no world pos3D of its own to collide with');
3611
+ const s = o.size3D, k = o.scale3D;
3612
+ const kx = abs(k.x), ky = abs(k.y), kz = abs(k.z);
3613
+ if (o.collideAsSphere3D)
3614
+ return {pos: o.pos3D.copy(), radius: max(s.x, s.y, s.z) / 2 * max(kx, ky, kz)};
3615
+ return {pos: o.pos3D.copy(), size: vec3(s.x * kx, s.y * ky, s.z * kz)};
3616
+ }
3617
+
3618
+ // how far a solid shape can reach from its own center, for a quick reject before the exact test
3619
+ // it has to be the shape's own radius, or a wider one: a box reaches to its corner, and a sphere
3620
+ // takes the largest scale the same way render3DSolidShape does, or the reject would skip real touches
3621
+ function render3DSolidReach(o)
3622
+ {
3623
+ const s = o.size3D, k = o.scale3D;
3624
+ const kx = abs(k.x), ky = abs(k.y), kz = abs(k.z);
3625
+ if (o.collideAsSphere3D)
3626
+ return max(s.x, s.y, s.z) / 2 * max(kx, ky, kz);
3627
+ return hypot(s.x * kx, s.y * ky, s.z * kz) / 2;
3628
+ }
3629
+
3630
+ // what it takes to move shape a clear of shape b, whichever pair of shapes they are, or undefined for no touch
3631
+ function render3DSolidPush(a, b)
3632
+ {
3633
+ if (!a.size) // a is a sphere
3634
+ return b.size ? collideSphereBox(a.pos, a.radius, b.pos, b.size)
3635
+ : collideSphereSphere(a.pos, a.radius, b.pos, b.radius);
3636
+ if (!b.size) // only b is, so push b out of a and turn it around
3637
+ {
3638
+ const push = collideSphereBox(b.pos, b.radius, a.pos, a.size);
3639
+ return push && push.scale(-1);
3640
+ }
3641
+ return collideBoxBox3D(a.pos, a.size, b.pos, b.size);
3642
+ }
3643
+
3644
+ // push a solid object out of the solids before it in the engine's list of them, so each pair is resolved once:
3645
+ // the ones after it update later and test against it then, and an object that is not in the list yet, because it
3646
+ // turned collision on this frame, tests them all itself and is not tested back
3647
+ // one pair per test is half the work of the 2D solver, which tests both directions; the difference only shows
3648
+ // when a collideWithObject destroys some third object, whose own turn then finds the pair already gone
3649
+ function render3DCollideSolid(a)
3650
+ {
3651
+ let shapeA = render3DSolidShape(a);
3652
+ const reachA = render3DSolidReach(a);
3653
+ for (const b of engineObjectsCollide)
3654
+ {
3655
+ if (b === a) break;
3656
+ // a child is part of its parent
3657
+ if (b.destroyed || !(b instanceof EngineObject3D) || b.parent || b.sync2D) continue;
3658
+ if (!a.isSolid && !b.isSolid) continue; // neither one blocks, so they pass through each other
3659
+
3660
+ // the pairs nowhere near each other are almost all of them in a scene of any size, so
3661
+ // settle those with one distance check instead of building a shape for each
3662
+ const p = shapeA.pos, q = b.pos3D, reach = reachA + render3DSolidReach(b);
3663
+ const dx = p.x - q.x, dy = p.y - q.y, dz = p.z - q.z;
3664
+ if (dx*dx + dy*dy + dz*dz > reach*reach)
3665
+ continue;
3666
+
3667
+ const push = render3DSolidPush(shapeA, render3DSolidShape(b));
3668
+ if (!push) continue;
3669
+
3670
+ // both objects hear about it, and either one can take the touch over
3671
+ const resolveA = a.collideWithObject(b, push);
3672
+ const resolveB = b.collideWithObject(a, push.scale(-1));
3673
+ if (!resolveA || !resolveB) continue;
3674
+
3675
+ // heavier objects move less, mass 0 stays put; then bounce apart when moving toward each other
3676
+ const total = a.mass + b.mass;
3677
+ const weightA = !a.mass ? 0 : !b.mass ? 1 : b.mass / total;
3678
+ const weightB = !b.mass ? 0 : !a.mass ? 1 : a.mass / total;
3679
+ a.pos3D = a.pos3D.add(push.scale(weightA));
3680
+ b.pos3D = b.pos3D.subtract(push.scale(weightB));
3681
+ if (weightA)
3682
+ shapeA = render3DSolidShape(a); // it moved, so the next solid must be tested against where it is now
3683
+ // mass 0 keeps its velocity too, so a moving platform keeps moving, and what hits it bounces by its own
3684
+ // restitution as it would off a static wall
3685
+ const normal = push.normalize();
3686
+ if (weightA && a.velocity3D.dot(normal) < 0)
3687
+ a.velocity3D = a.velocity3D.reflect(normal, a.restitution);
3688
+ if (weightB && b.velocity3D.dot(normal) > 0)
3689
+ b.velocity3D = b.velocity3D.reflect(normal, b.restitution);
3690
+ }
3691
+ }
3692
+
3693
+ /**
3694
+ * Collect the EngineObject3D objects whose boxes overlap a sphere or a box, the 3D twin of engineObjectsCollect
3695
+ * - Boxes are axis aligned around the world position, rotation3D is ignored; lights, emitters and trails have no size
3696
+ * and are never collected
3697
+ * @param {Vector3} pos - Center of the area
3698
+ * @param {Vector3|number} size - Diameter of a sphere if a number, 0 for a point, full size of a box if a Vector3
3699
+ * @param {Array<EngineObject>} [objects] - Defaults to every object
3700
+ * @param {boolean} [testCenters] - Test only each object's center, a little faster, and ignores object sizes
3701
+ * @return {Array<EngineObject3D>}
3702
+ * @memberof Render3D
3703
+ */
3704
+ function engineObjectsCollect3D(pos, size, objects=engineObjects, testCenters=false)
3705
+ {
3706
+ // a size of 0 is a point, tested against each box, since a sphere of no size could never hit
3707
+ const radiusSquared = typeof size === 'number' && size > 0 ? (size/2)**2 : undefined;
3708
+ const box = radiusSquared ? undefined : typeof size === 'number' ? vec3() : render3DSize3(size);
3709
+ const collected = [];
3710
+ for (const o of objects)
3711
+ {
3712
+ if (!(o instanceof EngineObject3D) || o.destroyed) continue;
3713
+ // the box in world space, scaled by the object and its parents
3714
+ const m = render3DObjectMatrix(o).m, s = o.size3D;
3715
+ if (!(s.x || s.y || s.z)) continue;
3716
+ const center = vec3(m[12], m[13], m[14]);
3717
+ const worldSize = testCenters ? vec3() : render3DWorldSize(o, m);
3718
+ let hit;
3719
+ if (box)
3720
+ hit = isOverlapping3D(pos, box, center, worldSize);
3721
+ else
3722
+ {
3723
+ // a sphere against the nearest point of the box
3724
+ const dx = max(abs(pos.x - center.x) - worldSize.x/2, 0);
3725
+ const dy = max(abs(pos.y - center.y) - worldSize.y/2, 0);
3726
+ const dz = max(abs(pos.z - center.z) - worldSize.z/2, 0);
3727
+ hit = dx*dx + dy*dy + dz*dz < radiusSquared;
3728
+ }
3729
+ hit && collected.push(o);
3730
+ }
3731
+ return collected;
3732
+ }
3733
+
3734
+ // how far along a ray an object is hit, or undefined for a miss; each one is tested as the box around its mesh in
3735
+ // its own space, or a sprite as the quad it draws, not triangle by triangle
3736
+ function render3DRaycastObject(ray, o)
3737
+ {
3738
+ if (o.destroyed || !(o instanceof EngineObject3D)) return;
3739
+ // a height map or voxel map is hit on its surface, not its box
3740
+ if (o instanceof HeightMap || o instanceof VoxelMap) return o.levelRaycast3D(ray);
3741
+ if (!(o.mesh || o.tileInfo)) return;
3742
+ if (o instanceof InstancedMesh3D) return; // its instances are not objects, and its one sphere is not a thing to hit
3743
+ const matrix = render3DObjectMatrix(o), mesh = o.mesh;
3744
+ if (!mesh) return render3DRaycastSprite(ray, o, matrix);
3745
+ // a mesh that changed since it was measured is measured again, an upload may not have come yet
3746
+ const radius = (mesh.dirty || !mesh.radius ? mesh.computeRadius() : mesh.radius) * render3DMaxStretch(matrix.m);
3747
+ if (!(radius > 0)) return; // nothing to hit
3748
+ const center = matrix.getTranslation();
3749
+ const distance = raycastSphere(ray, center, radius);
3750
+ if (distance === undefined) return;
3751
+
3752
+ // the sphere is a quick reject, a mesh is hit where the ray meets its box in its own space, since a wide floor's
3753
+ // sphere reaches far above it; the direction is not made unit length, so the distance holds in the world;
3754
+ // a mesh flattened to nothing on an axis has no inverse, it is hit as a disc like a sprite
3755
+ if (!matrix.determinant()) return render3DRaycastDisc(ray, center, radius);
3756
+ const inverse = matrix.copy().invert(), bounds = mesh.bounds || mesh.getBounds();
3757
+ const local = new Ray3D(inverse.transformPoint(ray.origin), inverse.transformDirection(ray.direction));
3758
+ const hit = raycastBox(local, bounds.min.add(bounds.max).scale(.5), bounds.max.subtract(bounds.min));
3759
+ if (hit !== 0) return hit;
3760
+
3761
+ // it starts inside the box, like a camera on terrain or in a room, so it is hit where it leaves, and what stands
3762
+ // inside comes first
3763
+ let exit = Infinity;
3764
+ for (const k of ['x', 'y', 'z'])
3765
+ {
3766
+ const d = local.direction[k];
3767
+ if (d)
3768
+ exit = min(exit, ((d > 0 ? bounds.max[k] : bounds.min[k]) - local.origin[k]) / d);
3769
+ }
3770
+ return exit === Infinity ? 0 : exit; // a ray of no length is where it starts
3771
+ }
3772
+
3773
+ // a sprite is the quad drawBillboard draws, its size3D grown by the x and y scale as the draw does, facing the camera
3774
+ // as it was last drawn; where the ray meets its plane, inside its half axes
3775
+ function render3DRaycastSprite(ray, o, matrix)
3776
+ {
3777
+ const center = matrix.getTranslation(), worldSize = render3DWorldSize(o, matrix.m), size = vec2(worldSize.x, worldSize.y);
3778
+ if (!(size.x > 0 && size.y > 0)) return; // nothing to hit
3779
+ if (!ray.direction.lengthSquared()) // a ray of no length is where it starts
3780
+ return render3DRaycastDisc(ray, center, hypot(size.x, size.y) / 2);
3781
+ const a = render3DBillboardAxes(size, o.rotation3D.z, o.upright);
3782
+ const right = vec3(a[0], a[1], a[2]), up = vec3(a[3], a[4], a[5]);
3783
+ const t = raycastPlane(ray, center, right.cross(up));
3784
+ if (t === undefined) return; // behind, or seen edge on
3785
+ const p = ray.getPosition(t).subtract(center);
3786
+ return abs(p.dot(right)) <= right.lengthSquared() && abs(p.dot(up)) <= up.lengthSquared() ? t : undefined;
3787
+ }
3788
+
3789
+ // a mesh flattened to nothing, or a sprite for a ray of no length: a disc facing the ray at its center's depth
3790
+ function render3DRaycastDisc(ray, center, radius)
3791
+ {
3792
+ const d = ray.direction, oc = center.subtract(ray.origin), dd = d.dot(d);
3793
+ const t = dd ? oc.dot(d) / dd : 0; // its depth along the ray, in the ray's own units
3794
+ return t >= 0 && oc.subtract(d.scale(t)).lengthSquared() <= radius*radius ? t : undefined;
3795
+ }
3796
+
3797
+ /**
3798
+ * Collect every EngineObject3D a ray passes through, nearest first, the 3D twin of engineObjectsRaycast
3799
+ * - The ray has no end, so everything along it counts however far away it is
3800
+ * - Use render3D.pick for the nearest one on its own, with the distance to it
3801
+ * @param {Ray3D} ray - From render3D.screenToRay, or any ray
3802
+ * @param {Array<EngineObject>} [objects] - Defaults to every object; only those with a mesh or a sprite count
3803
+ * @return {Array<EngineObject3D>}
3804
+ * @memberof Render3D
3805
+ */
3806
+ function engineObjectsRaycast3D(ray, objects=engineObjects)
3807
+ {
3808
+ const hits = [];
3809
+ for (const o of objects)
3810
+ {
3811
+ const distance = render3DRaycastObject(ray, o);
3812
+ if (distance !== undefined) // only a 3D object has a distance
3813
+ hits.push({o: /** @type {EngineObject3D} */ (o), distance});
3814
+ }
3815
+ return hits.sort((a, b)=> a.distance - b.distance).map(hit=> hit.o);
3816
+ }
3817
+
3818
+ /**
3819
+ * Call a function for each EngineObject3D whose box overlaps a sphere or a box
3820
+ * - An object destroyed by an earlier callback is skipped
3821
+ * @param {Vector3} pos - Center of the area
3822
+ * @param {Vector3|number} size - Diameter of a sphere if a number, 0 for a point, full size of a box if a Vector3
3823
+ * @param {function(EngineObject3D): void} callback
3824
+ * @param {Array<EngineObject>} [objects] - Defaults to every object
3825
+ * @param {boolean} [testCenters] - Test only each object's center, see engineObjectsCollect3D
3826
+ * @memberof Render3D
3827
+ */
3828
+ function engineObjectsCallback3D(pos, size, callback, objects=engineObjects, testCenters=false)
3829
+ {
3830
+ for (const o of engineObjectsCollect3D(pos, size, objects, testCenters))
3831
+ o.destroyed || callback(o);
3832
+ }
3833
+
3834
+ ///////////////////////////////////////////////////////////////////////////////
3835
+ /**
3836
+ * InstancedMesh3D - Many copies of one mesh drawn as one call, with their transforms kept on the GPU
3837
+ * - For big sets that mostly stay put: an instance costs nothing per frame until it changes, so a hundred thousand
3838
+ * trees cost what one tree does; objects and drawMesh batch by themselves too, but rebuild their batch every frame
3839
+ * - setMatrixAt and setColorAt change one instance, and only the changed range uploads before the next draw
3840
+ * - The instances are in world space; the object's own pos3D, rotation3D and scale3D do not move them
3841
+ * - The whole set is culled by one bounding sphere around the origin, and casts and receives shadows like any object
3842
+ * - The object's flags cover the whole set, one emissive, one tileInfo, one shader; only the colors are per instance
3843
+ * - A mirrored instance, one with a negative scale, shows its inside unless the mesh is doubleSided
3844
+ * - A transparent set draws in one go in the transparent stage, its instances are not sorted against each other;
3845
+ * the set sorts against other transparent draws by the object's position, so put pos3D at its middle
3846
+ * - pick, the raycast and the collect helpers do not see the instances, test them yourself from instanceData
3847
+ * @extends EngineObject3D
3848
+ * @memberof Render3D
3849
+ * @example
3850
+ * const forest = new InstancedMesh3D(treeMesh, 1000);
3851
+ * for (let i = 0; i < 1000; ++i)
3852
+ * forest.setMatrixAt(i, buildMatrix(randomGroundPos(), vec3(0, rand(2*PI), 0)));
3853
+ */
3854
+ class InstancedMesh3D extends EngineObject3D
3855
+ {
3856
+ /** Create a set of instances of a mesh, each at the origin in the object's color until it is set
3857
+ * @param {Mesh} mesh
3858
+ * @param {number} count - How many instances there is room for, all of them draw until count is lowered
3859
+ * @param {TileInfo|TextureInfo} [tileInfo] - Texture for all of them
3860
+ * @param {Color} [color] - The color they start with */
3861
+ constructor(mesh, count, tileInfo, color=WHITE)
3862
+ {
3863
+ super(vec3(), mesh, tileInfo, color);
3864
+ ASSERT(mesh instanceof Mesh, 'an InstancedMesh3D needs a Mesh');
3865
+ ASSERT(count >= 1, 'an InstancedMesh3D needs room for at least one instance');
3866
+ this.size3D = vec3(); // not a solid thing to pick or collect
3867
+ /** @property {number} - How many instances draw, the first ones, up to the count it was made with */
3868
+ this.count = count;
3869
+ /** @property {number} - How many instances it was made with */
3870
+ this.maxCount = count;
3871
+ /** @property {Float32Array} - The per instance values the shader reads, 24 floats each: the matrix, the color
3872
+ * and the uv rect; edit it directly and call markDirty for the instances changed */
3873
+ this.instanceData = new Float32Array(count * RENDER3D_INSTANCE_FLOATS);
3874
+ /** @property {number} - Radius of the sphere around the origin that holds every instance set so far, for
3875
+ * culling; from the farthest instance and the largest scale, and the mesh's size when it draws */
3876
+ this.radius = 0;
3877
+ this.reach = 0; // the farthest any instance's position has been from the origin
3878
+ this.maxScale = 0; // and the largest scale any instance has had
3879
+ /** @property {number} - First instance to upload before the next draw */
3880
+ this.dirtyStart = 0;
3881
+ /** @property {number} - One past the last instance to upload, so nothing uploads when it is not past dirtyStart */
3882
+ this.dirtyEnd = count;
3883
+ this.buffer = undefined; // the GPU copy of instanceData
3884
+ this.bufferGeneration = -1; // the context it was made under
3885
+ this.uvTileInfo = tileInfo; // the tile the uv rects were written for
3886
+ const uv = render3DGetTileUVs(tileInfo), data = this.instanceData;
3887
+ for (let i = 0; i < count; ++i)
3888
+ {
3889
+ this.setMatrixAt(i, RENDER3D_IDENTITY);
3890
+ const k = i * RENDER3D_INSTANCE_FLOATS;
3891
+ data[k+16] = color.r; data[k+17] = color.g; data[k+18] = color.b; data[k+19] = color.a;
3892
+ data[k+20] = uv.x; data[k+21] = uv.y; data[k+22] = uv.w; data[k+23] = uv.h;
3893
+ }
3894
+ }
3895
+
3896
+ /** Place an instance, in world space
3897
+ * @param {number} i
3898
+ * @param {Matrix4} matrix */
3899
+ setMatrixAt(i, matrix)
3900
+ {
3901
+ ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
3902
+ const data = this.instanceData, k = i * RENDER3D_INSTANCE_FLOATS, m = matrix.m;
3903
+ data.set(m, k);
3904
+ this.markDirty(i);
3905
+ }
3906
+
3907
+ /** The matrix of an instance
3908
+ * @param {number} i
3909
+ * @return {Matrix4} */
3910
+ getMatrixAt(i)
3911
+ {
3912
+ ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
3913
+ const k = i * RENDER3D_INSTANCE_FLOATS, matrix = new Matrix4;
3914
+ matrix.m.set(this.instanceData.subarray(k, k + 16));
3915
+ return matrix;
3916
+ }
3917
+
3918
+ /** Color an instance
3919
+ * @param {number} i
3920
+ * @param {Color} color */
3921
+ setColorAt(i, color)
3922
+ {
3923
+ ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
3924
+ ASSERT(isColor(color), 'color must be a Color');
3925
+ const data = this.instanceData, k = i * RENDER3D_INSTANCE_FLOATS;
3926
+ data[k+16] = color.r; data[k+17] = color.g; data[k+18] = color.b; data[k+19] = color.a;
3927
+ this.markDirty(i);
3928
+ }
3929
+
3930
+ /** Note that an instance changed, so it uploads before the next draw and the bounds hold it; setMatrixAt and
3931
+ * setColorAt call this, and so must an edit made straight to instanceData
3932
+ * @param {number} i */
3933
+ markDirty(i)
3934
+ {
3935
+ this.dirtyStart = min(this.dirtyStart, i);
3936
+ this.dirtyEnd = max(this.dirtyEnd, i + 1);
3937
+
3938
+ // the bounds grow to hold where it is now and how big, read back from the matrix it has
3939
+ const d = this.instanceData, k = i * RENDER3D_INSTANCE_FLOATS;
3940
+ this.reach = max(this.reach, hypot(d[k+12], d[k+13], d[k+14]));
3941
+ this.maxScale = max(this.maxScale, render3DMaxStretch(d, k));
3942
+ const meshRadius = this.mesh.radius || this.mesh.computeRadius(); // measured once, the draw takes a new size up
3943
+ this.radius = this.reach + meshRadius * this.maxScale;
3944
+ }
3945
+
3946
+ /** Draws every instance as one call, uploading the ones that changed first
3947
+ * @return {void} */
3948
+ render3D()
3949
+ {
3950
+ const r = render3D, gl = glContext, mesh = this.mesh;
3951
+ ASSERT(this.count >= 0 && this.count <= this.maxCount, 'count must be within the count it was made with');
3952
+ if (r.transparentQueue) // sorted as one thing with the other transparent draws
3953
+ return r.queueTransparent(this.getWorldPos3D(), ()=> this.render3D());
3954
+ if (!mesh || !this.count || !render3DCanDraw()) return;
3955
+ if (r.shadowPass && !r.lighting) return; // unlit things cast no shadow
3956
+ render3DMeshUpload(mesh);
3957
+ if (!mesh.bufferCount) return;
3958
+ this.radius = this.reach + mesh.radius * this.maxScale; // the mesh may have grown since
3959
+ if (r.frustumCulling && !render3DSphereVisible(0, 0, 0, this.radius)) return;
3960
+
3961
+ // the uv rect is the object's tile for every instance, rewritten when the tile changes
3962
+ if (this.uvTileInfo !== this.tileInfo)
3963
+ {
3964
+ const uv = render3DGetTileUVs(this.tileInfo), data = this.instanceData;
3965
+ for (let k = 20; k < data.length; k += RENDER3D_INSTANCE_FLOATS)
3966
+ data[k] = uv.x, data[k+1] = uv.y, data[k+2] = uv.w, data[k+3] = uv.h;
3967
+ this.uvTileInfo = this.tileInfo;
3968
+ this.dirtyStart = 0, this.dirtyEnd = this.maxCount;
3969
+ }
3970
+
3971
+ // the GPU copy: all of it under a fresh context, otherwise just the changed range
3972
+ if (!this.buffer || this.bufferGeneration !== r.contextGeneration)
3973
+ {
3974
+ this.buffer = gl.createBuffer();
3975
+ this.bufferGeneration = r.contextGeneration;
3976
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
3977
+ gl.bufferData(gl.ARRAY_BUFFER, this.instanceData, gl.DYNAMIC_DRAW);
3978
+ }
3979
+ else if (this.dirtyEnd > this.dirtyStart)
3980
+ {
3981
+ const start = this.dirtyStart * RENDER3D_INSTANCE_FLOATS, end = this.dirtyEnd * RENDER3D_INSTANCE_FLOATS;
3982
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
3983
+ gl.bufferSubData(gl.ARRAY_BUFFER, start * 4, this.instanceData, start, end - start);
3984
+ }
3985
+ this.dirtyStart = Infinity, this.dirtyEnd = 0;
3986
+
3987
+ // one draw under the object's state, the mesh setting the culling as drawMesh does
3988
+ r.flush();
3989
+ render3DFlushBeforeOverlay(); // as in drawMesh, what was drawn before goes under it
3990
+ const cullBackFaces = r.cullBackFaces, tileInfo = this.tileInfo;
3991
+ r.cullBackFaces = !mesh.doubleSided;
3992
+ render3DDrawInstanced(mesh, this.buffer, this.count, render3DTextureOf(tileInfo), r);
3993
+ r.cullBackFaces = cullBackFaces;
3994
+ }
3995
+
3996
+ /** Destroy the set and free its GPU buffer
3997
+ * @param {boolean} [immediate] */
3998
+ destroy(immediate)
3999
+ {
4000
+ if (this.buffer && this.bufferGeneration === render3D?.contextGeneration)
4001
+ glContext?.deleteBuffer(this.buffer);
4002
+ this.buffer = undefined;
4003
+ super.destroy(immediate);
4004
+ }
4005
+ }
4006
+
4007
+ ///////////////////////////////////////////////////////////////////////////////
4008
+ /**
4009
+ * Light3D - A light that is an EngineObject3D, so it can move, follow a parent or be destroyed like anything else
4010
+ * - A point light: it lights what is near it and fades out by its radius, DirectionalLight3D shines from far away
4011
+ * - Only the sun, render3D.sunDirection, casts shadows; these light and make highlights without one
4012
+ * - Only the 8 lights nearest the camera are used each frame
4013
+ * - radius is where the light fades out, and it fades fast, so a small radius wants a higher intensity
4014
+ * - intensity multiplies the color, above 1 for a light brighter than white
4015
+ * - radius is a world distance, so scale3D does not change it
4016
+ * - An alpha, an intensity or a radius of 0 switches it off, and a light that is off takes none of those slots
4017
+ * - Draws nothing itself, add a glow with drawSoftDisc or a small emissive mesh if it should be seen
4018
+ * @extends EngineObject3D
4019
+ * @memberof Render3D
4020
+ * @example
4021
+ * const torch = new Light3D(vec3(0, 3, 0), 10, hsl(.1, 1, .65));
4022
+ */
4023
+ class Light3D extends EngineObject3D
4024
+ {
4025
+ /** Create a point light
4026
+ * @param {Vector3} [pos3D] - Where it is
4027
+ * @param {number} [radius] - Distance where the light fades to nothing
4028
+ * @param {Color} [color] - Light color, its alpha fades it
4029
+ * @param {number} [intensity] - Brightness, multiplies the color, above 1 is brighter than white */
4030
+ constructor(pos3D=vec3(), radius=5, color=WHITE, intensity=1)
4031
+ {
4032
+ super(pos3D, undefined, undefined, color);
4033
+ ASSERT(radius >= 0, 'light radius cannot be negative, 0 is an off switch like an alpha of 0');
4034
+ ASSERT(intensity >= 0, 'light intensity cannot be negative, 0 is an off switch');
4035
+ this.size3D = vec3(); // not a solid thing to pick or collect
4036
+ /** @property {number} - Distance where the light fades to nothing */
4037
+ this.radius = radius;
4038
+ /** @property {number} - Brightness, multiplies the color, above 1 is brighter than white */
4039
+ this.intensity = intensity;
4040
+ /** @property {boolean} - Shine from far away, from its position toward the origin, instead of out from its
4041
+ * position with a falloff; DirectionalLight3D sets it */
4042
+ this.directional = false;
4043
+ }
4044
+
4045
+ /** Lights draw nothing */
4046
+ render3D() {}
4047
+ }
4048
+
4049
+ ///////////////////////////////////////////////////////////////////////////////
4050
+ /**
4051
+ * DirectionalLight3D - A Light3D that shines from far away with no falloff, like sunlight
4052
+ * - It shines from its position toward the origin, like a three.js DirectionalLight: only the direction to it
4053
+ * counts, so moving it or its parent swings the light around; parent it to a sun in the sky and it follows
4054
+ * - It cannot sit on the origin, since that leaves no direction
4055
+ * - Like every Light3D it casts no shadow, only the sun, render3D.sunDirection, does
4056
+ * @extends Light3D
4057
+ * @memberof Render3D
4058
+ * @example
4059
+ * const fill = new DirectionalLight3D(vec3(-1, 1, 1), hsl(.6, .5, .3)); // from the back left and above
4060
+ */
4061
+ class DirectionalLight3D extends Light3D
4062
+ {
4063
+ /** Create a directional light
4064
+ * @param {Vector3} [pos3D] - Where it shines from, toward the origin
4065
+ * @param {Color} [color] - Light color, its alpha fades it
4066
+ * @param {number} [intensity] - Brightness, multiplies the color, above 1 is brighter than white */
4067
+ constructor(pos3D=vec3(0, 1, 0), color=WHITE, intensity=1)
4068
+ {
4069
+ super(pos3D, 0, color, intensity);
4070
+ this.directional = true;
4071
+ }
4072
+ }