littlejsengine 1.24.3 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/EDITOR.md +2 -2
  3. package/FAQ.md +55 -11
  4. package/README.md +6 -2
  5. package/REFERENCE.md +203 -57
  6. package/dist/littlejs.d.ts +933 -313
  7. package/dist/littlejs.esm.js +5143 -2419
  8. package/dist/littlejs.esm.min.js +38 -24
  9. package/dist/littlejs.js +5132 -2416
  10. package/dist/littlejs.min.js +37 -23
  11. package/dist/littlejs.release.js +4905 -2232
  12. package/package.json +4 -4
  13. package/plugins/box2d.js +141 -56
  14. package/plugins/drawUtilities.js +374 -380
  15. package/plugins/gltf.js +383 -60
  16. package/plugins/lightSystem.js +412 -30
  17. package/plugins/math3d.js +1015 -1014
  18. package/plugins/medalSystem.js +19 -7
  19. package/plugins/newgrounds.js +5 -6
  20. package/plugins/parallax.js +1 -0
  21. package/plugins/particleEffects.js +47 -14
  22. package/plugins/pathFinder.js +60 -38
  23. package/plugins/pluginExport.js +11 -0
  24. package/plugins/postProcess.js +200 -67
  25. package/plugins/render3d.js +493 -2391
  26. package/plugins/render3dDebug.js +5 -8
  27. package/plugins/render3dEditor.js +84 -9
  28. package/plugins/render3dExtras.js +122 -33
  29. package/plugins/render3dLevel.js +67 -8
  30. package/plugins/render3dMesh.js +1158 -0
  31. package/plugins/render3dObject.js +1451 -0
  32. package/plugins/render3dVoxels.js +15 -3
  33. package/plugins/textureSheet.js +9 -8
  34. package/plugins/threejs.js +1 -1
  35. package/plugins/tweenSystem.js +117 -87
  36. package/plugins/uiSystem.js +62 -28
  37. package/plugins/wavedash.js +229 -0
  38. package/src/engine.js +192 -40
  39. package/src/engineAudio.js +47 -27
  40. package/src/engineBuild.mjs +6 -1
  41. package/src/engineDebug.js +23 -162
  42. package/src/engineDraw.js +148 -43
  43. package/src/engineEditor.js +153 -40
  44. package/src/engineExport.js +0 -3
  45. package/src/engineInput.js +31 -25
  46. package/src/engineMath.js +38 -20
  47. package/src/engineObject.js +68 -50
  48. package/src/engineParticles.js +66 -18
  49. package/src/engineRelease.js +18 -15
  50. package/src/engineSettings.js +48 -9
  51. package/src/engineTileLayer.js +44 -18
  52. package/src/engineUtilities.js +56 -11
  53. package/src/engineWebGL.js +68 -22
@@ -0,0 +1,1158 @@
1
+ /*
2
+ * LittleJS 3D Mesh Plugin
3
+ * - Mesh, the geometry the renderer draws: triangle strips or indexed lists, uploaded to the GPU on their first draw
4
+ * - The basic builders, buildBox, buildSphere, buildGrid, buildLathe and buildSky, and the helpers that pack a mesh
5
+ * - Goes after the Render3D plugin, everything here is part of its Render3D namespace
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ ///////////////////////////////////////////////////////////////////////////////
11
+
12
+ // how many vertices a strip of n points takes with its repeats, and which point vertex k of it is
13
+ function render3DStripCount(n) { return n + 2 + (n & 1); }
14
+ function render3DStripIndex(k, n) { return k < 1 ? 0 : k <= n ? k - 1 : n - 1; }
15
+
16
+ // walk a strip's vertices with the repeats applied, calling back with (point, normal, uv, color)
17
+ // normals, uvs and colors may be one value for all points, an array per point, or undefined
18
+ function render3DForEachStripVertex(points, normals, uvs, colors, callback)
19
+ {
20
+ ASSERT(isArray(points) && points.length > 2, 'strip needs at least 3 points');
21
+ const n = points.length, count = render3DStripCount(n);
22
+ const normalArray = isArray(normals), uvArray = isArray(uvs), colorArray = isArray(colors);
23
+ for (let k = 0; k < count; ++k)
24
+ {
25
+ const i = render3DStripIndex(k, n);
26
+ callback(points[i],
27
+ normalArray ? normals[i] : normals || RENDER3D_DEFAULT_NORMAL,
28
+ uvArray ? uvs[i] : uvs || RENDER3D_DEFAULT_UV,
29
+ colorArray ? colors[i] : colors || WHITE);
30
+ }
31
+ }
32
+
33
+ // write one vertex into a packed buffer at float index j
34
+ function render3DWriteVertex(floats, ints, j, x, y, z, n, u, v, rgba)
35
+ {
36
+ floats[j] = x; floats[j+1] = y; floats[j+2] = z;
37
+ floats[j+3] = n.x; floats[j+4] = n.y; floats[j+5] = n.z;
38
+ floats[j+6] = u; floats[j+7] = v;
39
+ ints[j+8] = rgba;
40
+ }
41
+
42
+ // turn every triangle of an index list the other way round, in place: the authoring form reads counter clockwise
43
+ // from the front and the pass draws clockwise, as a strip's triangles come out after its leading repeat
44
+ function render3DFlipTriangles(indices)
45
+ {
46
+ for (let t = 0; t < indices.length; t += 3)
47
+ {
48
+ const b = indices[t+1];
49
+ indices[t+1] = indices[t+2], indices[t+2] = b;
50
+ }
51
+ return indices;
52
+ }
53
+
54
+ // the triangles of a strip of count entries as an index list, given which vertex each entry maps to and which place
55
+ // 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
56
+ // 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
57
+ function render3DStripTriangles(count, remap, place)
58
+ {
59
+ const indices = [];
60
+ for (let i = 2; i < count; ++i)
61
+ {
62
+ const a = i & 1 ? i - 1 : i - 2, b = i & 1 ? i - 2 : i - 1;
63
+ if (place[a] != place[b] && place[b] != place[i] && place[a] != place[i])
64
+ indices.push(remap[a], remap[b], remap[i]);
65
+ }
66
+ return indices;
67
+ }
68
+
69
+ // a mesh's packed vertex data for the GPU, one vertex per entry of a layout, which is the strip index of each,
70
+ // written into data when given, the buffer an earlier call returned for the same layout; it measures the mesh's
71
+ // radius and box as it goes, as computeRadius does, over the vertices once instead of every strip entry
72
+ function render3DMeshVertexData(mesh, vertices, data=new ArrayBuffer(vertices.length * RENDER3D_VERTEX_BYTES))
73
+ {
74
+ const count = vertices.length;
75
+ const floats = new Float32Array(data), ints = new Uint32Array(data);
76
+ let r = 0, x0 = Infinity, y0 = Infinity, z0 = Infinity, x1 = -Infinity, y1 = -Infinity, z1 = -Infinity;
77
+ for (let j = 0; j < count; ++j)
78
+ {
79
+ // a hand built mesh may leave normals, uvs and colors empty
80
+ const i = vertices[j], p = mesh.points[i], uv = mesh.uvs[i] || RENDER3D_DEFAULT_UV;
81
+ const x = p.x, y = p.y, z = p.z;
82
+ render3DWriteVertex(floats, ints, j * RENDER3D_VERTEX_FLOATS, x, y, z,
83
+ mesh.normals[i] || RENDER3D_DEFAULT_NORMAL, uv.x, uv.y, (mesh.colors[i] || WHITE).rgbaInt());
84
+ r = max(r, x*x + y*y + z*z);
85
+ x0 = min(x0, x), y0 = min(y0, y), z0 = min(z0, z);
86
+ x1 = max(x1, x), y1 = max(y1, y), z1 = max(z1, z);
87
+ }
88
+ mesh.radius = r ** .5;
89
+ mesh.bounds = count ? {min: vec3(x0, y0, z0), max: vec3(x1, y1, z1)} : {min: vec3(), max: vec3()};
90
+ return data;
91
+ }
92
+
93
+ // reorder a convex polygon's points, counter clockwise from outside, into one triangle strip
94
+ function render3DPolygonStrip(points)
95
+ {
96
+ const strip = [points[0]];
97
+ for (let i = 1, j = points.length - 1; i <= j; ++i, --j)
98
+ {
99
+ strip.push(points[i]);
100
+ if (i !== j)
101
+ strip.push(points[j]);
102
+ }
103
+ return strip;
104
+ }
105
+
106
+ // turn every triangle of a mesh the other way round, the normals left alone: an index list reads each one in the
107
+ // other order, and a strip gets one extra point at each end, which flips every triangle and keeps the count even
108
+ function render3DFlipWinding(mesh)
109
+ {
110
+ if (mesh.indices)
111
+ return void render3DFlipTriangles(mesh.indices);
112
+ for (const key of ['points', 'normals', 'uvs', 'colors'])
113
+ {
114
+ const a = mesh[key];
115
+ if (a.length)
116
+ a.unshift(a[0]), a.push(a[a.length - 1]);
117
+ }
118
+ mesh.vertexKeys = undefined;
119
+ }
120
+
121
+ ///////////////////////////////////////////////////////////////////////////////
122
+
123
+ // frees the GPU buffer of a mesh that is garbage collected without dispose, some time after it goes; it holds the
124
+ // buffer and its context, never the mesh, or the mesh could not be collected, and dispose unregisters the mesh
125
+ const render3DMeshBuffers = typeof FinalizationRegistry == 'undefined' ? undefined :
126
+ new FinalizationRegistry(({buffer, indexBuffer, generation})=>
127
+ {
128
+ if (generation !== render3D?.contextGeneration || !glContext) return;
129
+ glContext.deleteBuffer(buffer);
130
+ glContext.deleteBuffer(indexBuffer);
131
+ });
132
+
133
+ // the points at one place, to a hundred thousandth, which smooth normals are summed by: each point's group, numbered
134
+ // from 0, found by a number made from its place and checked against the place, so no text is made for a point
135
+ function render3DPlaceGroups(points)
136
+ {
137
+ const groups = new Int32Array(points.length), first = new Map, next = [], places = [];
138
+ for (let i = 0; i < points.length; ++i)
139
+ {
140
+ const p = points[i], x = round(p.x * 1e5), y = round(p.y * 1e5), z = round(p.z * 1e5);
141
+ const hash = Math.imul(x, 73856093) ^ Math.imul(y, 19349663) ^ Math.imul(z, 83492791);
142
+ let group = first.get(hash);
143
+ while (group !== undefined && !(places[group*3] === x && places[group*3+1] === y && places[group*3+2] === z))
144
+ group = next[group];
145
+ if (group === undefined)
146
+ {
147
+ group = next.length;
148
+ next.push(first.get(hash));
149
+ first.set(hash, group);
150
+ places.push(x, y, z);
151
+ }
152
+ groups[i] = group;
153
+ }
154
+ return {groups, count: next.length};
155
+ }
156
+
157
+ // the face normals meeting at each place added up, each weighted by its corner angle so a cube corner averages its
158
+ // three faces evenly however they are cut into triangles; of a strip, every other triangle turned back, with no
159
+ // indices, or of the index list; in numbers, with no vector made for a triangle or a corner
160
+ function render3DSmoothNormalSums(points, indices)
161
+ {
162
+ const strip = !indices, triangles = strip ? max(points.length - 2, 0) : indices.length / 3 | 0;
163
+ const {groups, count} = render3DPlaceGroups(points);
164
+ const sums = new Float64Array(count * 3), touched = new Uint8Array(count);
165
+ let nx = 0, ny = 0, nz = 0;
166
+ const addCorner = (a, b, c, group)=>
167
+ {
168
+ // the angle at corner a, then the face normal scaled by it added to the sum at a's place
169
+ const ux = b.x - a.x, uy = b.y - a.y, uz = b.z - a.z, vx = c.x - a.x, vy = c.y - a.y, vz = c.z - a.z;
170
+ const dot = ux*vx + uy*vy + uz*vz, lengths = (ux**2 + uy**2 + uz**2)**.5 * (vx**2 + vy**2 + vz**2)**.5;
171
+ const angle = Math.acos(clamp(dot / (lengths || 1), -1, 1)), s = group * 3;
172
+ sums[s] += nx * angle, sums[s+1] += ny * angle, sums[s+2] += nz * angle;
173
+ touched[group] = 1;
174
+ };
175
+ for (let t = 0; t < triangles; ++t)
176
+ {
177
+ const i0 = strip ? t : indices[t*3], i1 = strip ? t + 1 : indices[t*3+1], i2 = strip ? t + 2 : indices[t*3+2];
178
+ const a = points[i0], b = points[i1], c = points[i2];
179
+ const ux = b.x - a.x, uy = b.y - a.y, uz = b.z - a.z, vx = c.x - a.x, vy = c.y - a.y, vz = c.z - a.z;
180
+ const cx = uy*vz - uz*vy, cy = uz*vx - ux*vz, cz = ux*vy - uy*vx;
181
+ const lengthSquared = cx**2 + cy**2 + cz**2;
182
+ if (!lengthSquared) continue; // no area, like a triangle joining two strips
183
+ const k = (strip && !(t & 1) ? -1 : 1) / lengthSquared**.5;
184
+ nx = cx * k, ny = cy * k, nz = cz * k;
185
+ addCorner(a, b, c, groups[i0]);
186
+ addCorner(b, c, a, groups[i1]);
187
+ addCorner(c, a, b, groups[i2]);
188
+ }
189
+ return {groups, sums, touched};
190
+ }
191
+
192
+ /**
193
+ * Mesh - Triangles with positions, normals, uvs and colors, uploaded once and drawn by matrix
194
+ * - Build with addStrip, addQuad, combine or the shape builders, then render each frame
195
+ * - Its back faces are skipped unless doubleSided is set, which the open builders like buildGrid do for you
196
+ * - Two forms: a triangle strip, what the builders make, or an indexed list of triangles over their own vertices,
197
+ * what addTriangles and the model loaders make; upload sends the GPU an indexed list either way, see getTriangles,
198
+ * so a strip's joins between its pieces cost nothing to draw, and toIndexed turns a strip mesh into the list form
199
+ * - The GPU buffer is created lazily on first render and dropped by dispose, or freed once the mesh is garbage
200
+ * collected, so dispose is only needed to free it right away, like for a mesh rebuilt often
201
+ * @memberof Render3D
202
+ * @example
203
+ * const mesh = buildLathe([[0, -1], [1, 0], [0, 1]], 4); // octahedron
204
+ * mesh.render(buildMatrix(vec3(0, 1, 0)), undefined, RED);
205
+ */
206
+ class Mesh
207
+ {
208
+ /** Create an empty mesh */
209
+ constructor()
210
+ {
211
+ /** @property {Array<Vector3>} - Vertex positions, in strip order or one per vertex of an indexed mesh
212
+ * @type {Array<Vector3>} */
213
+ this.points = [];
214
+ /** @property {Array<Vector3>} - Vertex normals
215
+ * @type {Array<Vector3>} */
216
+ this.normals = [];
217
+ /** @property {Array<Vector2>} - Vertex texture coords, 0-1 across the tile
218
+ * @type {Array<Vector2>} */
219
+ this.uvs = [];
220
+ /** @property {Array<Color>} - Vertex colors
221
+ * @type {Array<Color>} */
222
+ this.colors = [];
223
+ /** @property {WebGLBuffer|undefined} - GPU vertex buffer, created by upload
224
+ * @type {WebGLBuffer|undefined} */
225
+ this.buffer = undefined;
226
+ /** @property {WebGLBuffer|undefined} - GPU index buffer, the triangles, created by upload
227
+ * @type {WebGLBuffer|undefined} */
228
+ this.indexBuffer = undefined;
229
+ /** @property {number} - Indices in the GPU index buffer, three per triangle */
230
+ this.bufferCount = 0;
231
+ this.indexType = 0; // gl.UNSIGNED_SHORT, or UNSIGNED_INT past 65535 vertices
232
+ /** @property {boolean} - The mesh changed and needs uploading again, set it yourself if you edit the arrays */
233
+ this.dirty = false;
234
+ /** @property {boolean|undefined} - Draw every use of this mesh in the opaque stage as one instanced call,
235
+ * undefined follows render3D.instancing
236
+ * @type {boolean|undefined} */
237
+ this.instanced = undefined;
238
+ /** @property {boolean} - Draw both sides, each lit as the side that is seen; off skips the faces pointing away,
239
+ * which is faster and right for closed shapes, the open builders like buildGrid and buildRibbon turn it on */
240
+ this.doubleSided = false;
241
+ /** @property {boolean} - The values change often but the shape never does, for a water surface or a cloth: set
242
+ * once, the mesh keeps its GPU layout and a dirty upload only rewrites the vertices into the buffer it has;
243
+ * the strip must keep the same points in the same order, a new point count asserts; the layout is decided by
244
+ * the first upload, so strip entries equal then stay one vertex and triangles with no area then stay dropped,
245
+ * set vertexKeys or give it distinct values at the start, not a flat grid of one color or points all in one place */
246
+ this.dynamicDraw = false;
247
+ /** @property {Array<number>|undefined} - The mesh as an indexed triangle list instead of a strip: the arrays
248
+ * hold each vertex once and this says how they join, three vertex numbers per triangle, counter clockwise seen
249
+ * from the front like a strip's first triangle; addTriangles and the loaders fill it, toIndexed turns a strip
250
+ * mesh into this form
251
+ * @type {Array<number>|undefined} */
252
+ this.indices = undefined;
253
+ /** @property {Int32Array|undefined} - Which strip entries are one vertex, set by a builder that knows, one
254
+ * whole number per entry with equal numbers meaning the same vertex; upload skips its search for them, then
255
+ * drops the keys, since an edit after that may tell the entries apart; adding geometry or recomputing normals
256
+ * drops them too
257
+ * @type {Int32Array|undefined} */
258
+ this.vertexKeys = undefined;
259
+ /** @type {{vertices: Array<number>, pointCount: number, data: ArrayBuffer}|undefined} */
260
+ // the strip index of each GPU vertex, the point count and packed data of the last upload, for a dynamicDraw mesh
261
+ this.vertexLayout = undefined;
262
+ // the layout's data already holds the mesh as it is, with its radius and box, written by a glTF skin as it
263
+ // bent the mesh, so the next upload sends it without packing it again
264
+ this.vertexDataPacked = false;
265
+ this.instanceCount = 0; // draws waiting in this mesh's batch, with their values, texture and draw state
266
+ /** @type {Float32Array|undefined} */
267
+ this.instanceData = undefined;
268
+ /** @property {number} - Bounding sphere radius around the origin, for culling and picking, computed by upload */
269
+ this.radius = 0;
270
+ /** @property {{min: Vector3, max: Vector3}|undefined} - Bounding box, for picking, measured with the radius
271
+ * @type {{min: Vector3, max: Vector3}|undefined} */
272
+ this.bounds = undefined;
273
+ this.contextGeneration = 0; // the context the buffer belongs to, see render3D.contextGeneration
274
+ }
275
+
276
+ /** Number of vertices in the mesh
277
+ * @return {number} */
278
+ get vertexCount() { return this.points.length; }
279
+
280
+ /** Add a triangle strip, joined to the previous one by invisible flat triangles so one mesh holds many strips
281
+ * - Strip order: the first three points make a triangle, then each point makes another with the two before it
282
+ * - List the first three points counter clockwise as seen from the front, or the face points away
283
+ * and may vanish when back faces are culled
284
+ * @param {Array<Vector3>} points - Strip order
285
+ * @param {Vector3|Array<Vector3>} [normals] - One for all or one per point, default up
286
+ * @param {Vector2|Array<Vector2>} [uvs] - One for all or one per point, default zero
287
+ * @param {Color|Array<Color>} [colors] - One for all or one per point, default white
288
+ * @return {Mesh} */
289
+ addStrip(points, normals, uvs, colors)
290
+ {
291
+ if (this.indices)
292
+ {
293
+ // an indexed mesh takes the strip as the triangles it makes
294
+ const part = new Mesh().addStrip(points, normals, uvs, colors).toIndexed();
295
+ return this.addTriangles(part.points, part.indices, part.normals, part.uvs, part.colors);
296
+ }
297
+ render3DForEachStripVertex(points, normals, uvs, colors, (p, n, uv, c)=>
298
+ {
299
+ this.points.push(p);
300
+ this.normals.push(n);
301
+ this.uvs.push(uv);
302
+ this.colors.push(c);
303
+ });
304
+ this.vertexKeys = undefined; // the new entries have no keys
305
+ this.dirty = true;
306
+ return this;
307
+ }
308
+
309
+ /** Add triangles over their own vertices, the indexed form a model file comes in
310
+ * - The mesh becomes indexed: a strip mesh is turned into triangles first, and strips added later join as triangles
311
+ * - List each triangle counter clockwise as seen from the front, like a strip's first triangle
312
+ * @param {Array<Vector3>} points - Each vertex once
313
+ * @param {Array<number>} indices - Three vertex numbers per triangle, into points
314
+ * @param {Vector3|Array<Vector3>} [normals] - One for all or one per point, default up
315
+ * @param {Vector2|Array<Vector2>} [uvs] - One for all or one per point, default zero
316
+ * @param {Color|Array<Color>} [colors] - One for all or one per point, default white
317
+ * @return {Mesh} */
318
+ addTriangles(points, indices, normals, uvs, colors)
319
+ {
320
+ ASSERT(isArray(points) && isArray(indices) && indices.length % 3 === 0,
321
+ 'addTriangles takes points and three indices per triangle');
322
+ ASSERT(indices.every(i=> i >= 0 && i < points.length && i % 1 === 0), 'an index points past the vertices given');
323
+ this.toIndexed();
324
+ const offset = this.points.length, normalArray = isArray(normals), uvArray = isArray(uvs), colorArray = isArray(colors);
325
+ for (let i = 0; i < points.length; ++i)
326
+ {
327
+ this.points.push(points[i]);
328
+ this.normals.push(normalArray ? normals[i] : normals || RENDER3D_DEFAULT_NORMAL);
329
+ this.uvs.push(uvArray ? uvs[i] : uvs || RENDER3D_DEFAULT_UV);
330
+ this.colors.push(colorArray ? colors[i] : colors || WHITE);
331
+ }
332
+ for (const i of indices)
333
+ this.indices.push(i + offset);
334
+ this.dirty = true;
335
+ return this;
336
+ }
337
+
338
+ /** Turn a strip mesh into the indexed form, each distinct vertex once and the real triangles over them, in place
339
+ * - An indexed mesh is left as it is; the builders make strips and a loader makes this, and either draws the same
340
+ * @return {Mesh} */
341
+ toIndexed()
342
+ {
343
+ if (this.indices) return this;
344
+ const {vertices, indices} = this.getTriangles();
345
+ this.points = vertices.map(i=> this.points[i]);
346
+ this.normals = vertices.map(i=> this.normals[i] || RENDER3D_DEFAULT_NORMAL);
347
+ this.uvs = vertices.map(i=> this.uvs[i] || RENDER3D_DEFAULT_UV);
348
+ this.colors = vertices.map(i=> this.colors[i] || WHITE);
349
+ // the list upload sends reads clockwise, the pass draws it that way; the authoring form is counter clockwise
350
+ this.indices = render3DFlipTriangles(indices);
351
+ this.vertexKeys = undefined;
352
+ this.dirty = true;
353
+ return this;
354
+ }
355
+
356
+ /** Add a flat quad from four corners in loop order, counter clockwise seen from the front, a is the top left of the texture
357
+ * @param {Vector3} a
358
+ * @param {Vector3} b
359
+ * @param {Vector3} c
360
+ * @param {Vector3} d
361
+ * @param {Color|Array<Color>} [color] - One for all or one per corner
362
+ * @param {Array<Vector2>} [uvs] - One per corner, default across the tile
363
+ * @return {Mesh} */
364
+ addQuad(a, b, c, d, color, uvs)
365
+ {
366
+ return this.addStrip(render3DQuadStrip(a, b, c, d), render3DFaceNormal(a, b, c, d),
367
+ uvs ? render3DQuadValues(uvs) : RENDER3D_QUAD_UVS, render3DQuadValues(color));
368
+ }
369
+
370
+ /** Append another mesh transformed by a matrix, for building one shape out of several
371
+ * @param {Mesh} mesh
372
+ * @param {Matrix4|Vector3} [matrix] - Transform, or just a position to move it to
373
+ * @param {Color} [color] - Multiplies the appended vertex colors
374
+ * @return {Mesh} */
375
+ combine(mesh, matrix=RENDER3D_IDENTITY, color=WHITE)
376
+ {
377
+ matrix = render3DMatrix(matrix); // most parts only need moving into place
378
+ const normalMatrix = render3DNormalMatrix(matrix), mirrors = render3DMirrors(matrix);
379
+ let part = mesh, order;
380
+ if (this.indices || mesh.indices)
381
+ {
382
+ // one of them is indexed, so both are: the part as a copy if it is a strip
383
+ this.toIndexed();
384
+ part = mesh.indices ? mesh : new Mesh().combine(mesh).toIndexed();
385
+ const offset = this.points.length, indices = part.indices;
386
+ // the count read once, a mesh combined with itself grows as it is read; a mirror turns every triangle the
387
+ // other way round
388
+ for (let t = 0, n = indices.length; t < n; t += 3)
389
+ this.indices.push(indices[t] + offset, indices[t + (mirrors ? 2 : 1)] + offset,
390
+ indices[t + (mirrors ? 1 : 2)] + offset);
391
+ }
392
+ else if (mirrors && part.points.length)
393
+ {
394
+ // a strip reads the other way round with one more point at each end, as render3DFlipWinding does
395
+ const last = part.points.length - 1;
396
+ order = [0, ...part.points.keys(), last];
397
+ }
398
+ for (let j = 0, count = order ? order.length : part.points.length; j < count; ++j)
399
+ {
400
+ const i = order ? order[j] : j;
401
+ this.points.push(matrix.transformPoint(part.points[i]));
402
+ this.normals.push(normalMatrix.transformDirection(part.normals[i] || RENDER3D_DEFAULT_NORMAL).normalize());
403
+ this.uvs.push((part.uvs[i] || RENDER3D_DEFAULT_UV).copy());
404
+ this.colors.push((part.colors[i] || WHITE).multiply(color));
405
+ }
406
+ this.doubleSided ||= mesh.doubleSided; // an open part leaves the whole mesh open
407
+ this.vertexKeys = undefined; // the new entries have no keys
408
+ this.dirty = true;
409
+ return this;
410
+ }
411
+
412
+ /** Returns a new mesh: this one and its mirror image across the plane through the origin facing axis
413
+ * - For modeling half a shape against that plane, a part that crosses it overlaps its image
414
+ * @param {Vector3} [axis] - Faces the mirror plane, vec3(1,0,0) mirrors across x
415
+ * @return {Mesh} */
416
+ mirror(axis=vec3(1, 0, 0))
417
+ {
418
+ ASSERT(isVector3(axis) && axis.lengthSquared() > 0, 'mirror needs an axis');
419
+ const n = axis.normalize(), a = -2 * n.x, b = -2 * n.y, c = -2 * n.z;
420
+ 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,
421
+ a * n.z, b * n.z, 1 + c * n.z, 0, 0, 0, 0, 1]);
422
+ return new Mesh().combine(this).combine(this, reflect); // combine turns the image's faces the right way out
423
+ }
424
+
425
+ /** Returns a new mesh of count copies of this one, each turned further around an axis through the origin
426
+ * @param {number} count - Copies, spaced evenly around the whole turn
427
+ * @param {Vector3} [axis] - Up by default
428
+ * @return {Mesh} */
429
+ spin(count, axis=vec3(0, 1, 0))
430
+ {
431
+ ASSERT(count >= 1 && count % 1 === 0, 'spin count must be a whole number, 1 or more');
432
+ ASSERT(isVector3(axis) && axis.lengthSquared() > 0, 'spin needs an axis');
433
+ const mesh = new Mesh, unit = axis.normalize();
434
+ for (let i = 0; i < count; ++i)
435
+ mesh.combine(this, render3DAxisRotation(unit, i / count * 2 * PI));
436
+ return mesh;
437
+ }
438
+
439
+ /** Returns a new mesh of everything in this mesh or the other, see subtract
440
+ * @param {Mesh} mesh - Closed, as the builders make them apart from the open ones like buildGrid
441
+ * @param {Matrix4|Vector3} [matrix] - Places the other mesh, or just a position to move it to
442
+ * @return {Mesh} */
443
+ union(mesh, matrix) { return render3DMeshCSG(this, mesh, matrix, 0); }
444
+
445
+ /** Returns a new mesh of this one with the other cut out of it, CSG with BSP trees
446
+ * - Both must be closed, every edge shared by two triangles, as the builders make them apart from the open
447
+ * ones like buildGrid and buildRibbon; the result is closed and indexed, and neither mesh changes
448
+ * - The faces a cut makes come from the other mesh's surface, turned to face out, with its normals, uvs and
449
+ * colors, so a smooth cylinder drills a round hole
450
+ * - Parts that overlap must be joined with union to be one solid, not with combine, mirror or spin, which
451
+ * leave them overlapping, and CSG then gives a wrong shape
452
+ * - Cuts split the triangles near them, so the result has more: a few thousand triangles take tens to a few
453
+ * hundred milliseconds, so build shapes this way at load time, not every frame; joining several cutters
454
+ * with union and cutting once is quicker than cutting with each in turn
455
+ * - Details closer than about 1e-4 are made one, so build a very small part larger and scale it after
456
+ * @param {Mesh} mesh - Closed, as the builders make them apart from the open ones like buildGrid
457
+ * @param {Matrix4|Vector3} [matrix] - Places the other mesh, or just a position to move it to
458
+ * @return {Mesh}
459
+ * @example
460
+ * const wall = buildBox(vec3(4, 3, .5)).subtract(buildBox(vec3(1, 2, 1)), vec3(0, -.5, 0)); // a doorway */
461
+ subtract(mesh, matrix) { return render3DMeshCSG(this, mesh, matrix, 1); }
462
+
463
+ /** Returns a new mesh of only what is in both this mesh and the other, see subtract
464
+ * @param {Mesh} mesh - Closed, as the builders make them apart from the open ones like buildGrid
465
+ * @param {Matrix4|Vector3} [matrix] - Places the other mesh, or just a position to move it to
466
+ * @return {Mesh} */
467
+ intersect(mesh, matrix) { return render3DMeshCSG(this, mesh, matrix, 2); }
468
+
469
+ /** Scale every uv, so a whole texture repeats across the mesh when its TextureInfo wraps
470
+ * @param {Vector2|number} scale - Repeats across and up, a number for both
471
+ * @return {Mesh} */
472
+ scaleUVs(scale)
473
+ {
474
+ const s = render3DSize2(scale);
475
+ // new vectors, builders share uv objects between faces
476
+ this.uvs = this.uvs.map(uv=> vec2(uv.x * s.x, uv.y * s.y));
477
+ this.dirty = true;
478
+ return this;
479
+ }
480
+
481
+ /** Move, turn or scale every vertex in place, normals follow along
482
+ * @param {Matrix4|Vector3} matrix - Transform, or just an offset to move by
483
+ * @return {Mesh} */
484
+ transform(matrix)
485
+ {
486
+ matrix = render3DMatrix(matrix);
487
+ const normalMatrix = render3DNormalMatrix(matrix);
488
+ for (let i = 0; i < this.points.length; ++i)
489
+ {
490
+ this.points[i] = matrix.transformPoint(this.points[i]);
491
+ // a mesh built by hand may have no normals yet, and then there is nothing to turn
492
+ this.normals[i] &&= normalMatrix.transformDirection(this.normals[i]).normalize();
493
+ }
494
+ render3DMirrors(matrix) && render3DFlipWinding(this); // a mirror would leave the faces pointing in
495
+ this.dirty = true;
496
+ return this;
497
+ }
498
+
499
+ /** Turn the mesh inside out so it is lit and drawn from within, for rooms and domes
500
+ * @return {Mesh} */
501
+ flipNormals()
502
+ {
503
+ render3DFlipWinding(this);
504
+ this.normals = this.normals.map(n=> n.scale(-1));
505
+ this.dirty = true;
506
+ return this;
507
+ }
508
+
509
+ /** Set every vertex color
510
+ * @param {Color} color
511
+ * @return {Mesh} */
512
+ setColor(color)
513
+ {
514
+ // one per point, not one per color already there, so a mesh built by hand with no
515
+ // colors gets them instead of quietly staying white
516
+ this.colors = this.points.map(()=> color);
517
+ this.dirty = true;
518
+ return this;
519
+ }
520
+
521
+ /** Measure the axis aligned box around the vertices
522
+ * @return {{min: Vector3, max: Vector3}} */
523
+ getBounds()
524
+ {
525
+ if (!this.points.length)
526
+ return {min: vec3(), max: vec3()};
527
+ const lo = vec3(Infinity), hi = vec3(-Infinity);
528
+ for (const p of this.points)
529
+ {
530
+ lo.x = min(lo.x, p.x); lo.y = min(lo.y, p.y); lo.z = min(lo.z, p.z);
531
+ hi.x = max(hi.x, p.x); hi.y = max(hi.y, p.y); hi.z = max(hi.z, p.z);
532
+ }
533
+ return {min: lo, max: hi};
534
+ }
535
+
536
+ /** Move the mesh so the center of its bounds is on the origin
537
+ * @return {Mesh} */
538
+ center()
539
+ {
540
+ const bounds = this.getBounds();
541
+ return this.transform(bounds.min.add(bounds.max).scale(-.5));
542
+ }
543
+
544
+ /** Scale the mesh evenly so its largest extent is a size, for loaded models of unknown units
545
+ * @param {number} [size]
546
+ * @return {Mesh} */
547
+ fit(size=1)
548
+ {
549
+ const bounds = this.getBounds();
550
+ const extent = bounds.max.subtract(bounds.min);
551
+ const scale = size / (max(extent.x, extent.y, extent.z) || 1);
552
+ return this.transform(Matrix4.scaling(vec3(scale)));
553
+ }
554
+
555
+ /** Measure the bounding sphere around the origin into radius, called by upload
556
+ * @return {number} */
557
+ computeRadius()
558
+ {
559
+ let r = 0;
560
+ for (const p of this.points)
561
+ r = max(r, p.lengthSquared());
562
+ this.bounds = this.getBounds(); // measured with it, a pick tests a mesh's box after its sphere
563
+ return this.radius = r ** .5;
564
+ }
565
+
566
+ /** Derive normals from the triangles, of the strip or of the index list
567
+ * @param {boolean} [smooth] - Round the lighting across faces instead of giving each face a hard edge; flat
568
+ * normals on an indexed mesh give every corner its own vertex
569
+ * @return {Mesh} */
570
+ computeNormals(smooth=false)
571
+ {
572
+ if (this.indices)
573
+ {
574
+ // the triangles are listed: smooth normals add up around each position, weighted by the corner angle
575
+ // like the strip's, so vertices split apart at one place smooth back together and a mesh can go flat and
576
+ // smooth again; flat ones need a vertex per corner, so the vertices are split up first
577
+ if (smooth)
578
+ {
579
+ const {groups, sums, touched} = render3DSmoothNormalSums(this.points, this.indices);
580
+ this.normals = this.points.map((p, i)=>
581
+ {
582
+ const s = groups[i] * 3, x = sums[s], y = sums[s+1], z = sums[s+2];
583
+ const lengthSquared = x**2 + y**2 + z**2, k = 1 / lengthSquared**.5;
584
+ return touched[groups[i]] && lengthSquared ? vec3(x * k, y * k, z * k) : RENDER3D_DEFAULT_NORMAL;
585
+ });
586
+ this.dirty = true;
587
+ return this;
588
+ }
589
+ const split = (a)=> this.indices.map(i=> a[i]);
590
+ this.points = split(this.points), this.normals = split(this.normals);
591
+ this.uvs = split(this.uvs), this.colors = split(this.colors);
592
+ this.indices = this.indices.map((_, i)=> i);
593
+ const points = this.points, indices = this.indices;
594
+ const normals = points.map(()=> RENDER3D_DEFAULT_NORMAL);
595
+ for (let t = 0; t < indices.length; t += 3)
596
+ {
597
+ const a = points[indices[t]], b = points[indices[t+1]], c = points[indices[t+2]];
598
+ const cross = b.subtract(a).cross(c.subtract(a));
599
+ if (cross.lengthSquared()) // its own three corners
600
+ normals[indices[t]] = normals[indices[t+1]] = normals[indices[t+2]] = cross.normalize();
601
+ }
602
+ this.normals = normals;
603
+ this.dirty = true;
604
+ return this;
605
+ }
606
+
607
+ // smooth normals are the sums at each place made unit length, or straight up for a place with none
608
+ const points = this.points, n = points.length;
609
+ if (smooth)
610
+ {
611
+ const {groups, sums, touched} = render3DSmoothNormalSums(points);
612
+ this.normals = points.map((p, i)=>
613
+ {
614
+ const s = groups[i] * 3, x = sums[s], y = sums[s+1], z = sums[s+2], l = (x**2 + y**2 + z**2)**.5;
615
+ return !touched[groups[i]] ? vec3(0, 1, 0) : l ? vec3(x * (1/l), y * (1/l), z * (1/l)) : vec3();
616
+ });
617
+ this.vertexKeys = undefined;
618
+ this.dirty = true;
619
+ return this;
620
+ }
621
+
622
+ // the outward normal of each triangle in the strip
623
+ const faceNormals = [];
624
+ for (let i = 0; i + 2 < n; ++i)
625
+ {
626
+ const a = points[i], b = points[i+1], c = points[i+2];
627
+ const normal = b.subtract(a).cross(c.subtract(a));
628
+ // triangles in a strip alternate which way they wind, so every other one is flipped back
629
+ // a zero normal means a flat triangle joining two strips, so skip it
630
+ faceNormals.push(normal.lengthSquared() ? normal.normalize(i & 1 ? 1 : -1) : undefined);
631
+ }
632
+
633
+ // then hand those to the vertices: every triangle writes its own three corners, so the only vertices left
634
+ // with the default are the repeats at the ends of a strip, which no triangle with any area uses
635
+ const normals = points.map(()=> RENDER3D_DEFAULT_NORMAL);
636
+ faceNormals.forEach((f, i)=> f && (normals[i] = normals[i+1] = normals[i+2] = f));
637
+
638
+ this.normals = normals;
639
+ this.vertexKeys = undefined; // flat normals tell entries at one place apart
640
+ this.dirty = true;
641
+ return this;
642
+ }
643
+
644
+ /** Pack the vertices and create the GPU buffer, called automatically by render
645
+ * @return {Mesh} */
646
+ upload()
647
+ {
648
+ // the packing below measures the radius and box, with nothing to pack to they are measured here
649
+ if (!render3D?.program || !glContext) return this.computeRadius(), this;
650
+ const gl = glContext, layout = this.vertexLayout;
651
+ // no layout when dynamicDraw was turned on after an upload, the next upload makes one
652
+ if (this.dynamicDraw && layout && this.buffer && this.contextGeneration === render3D.contextGeneration)
653
+ {
654
+ // the layout of the last upload stands, only the values are written again into the buffer it has,
655
+ // packed into the same memory each time so a mesh uploaded every frame makes no garbage
656
+ ASSERT(layout.pointCount === this.points.length,
657
+ 'a dynamicDraw mesh keeps its shape, the same points in the same order; ' +
658
+ 'for a new shape make a new mesh or turn dynamicDraw off', this.points.length);
659
+ if (layout.pointCount === this.points.length)
660
+ {
661
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
662
+ gl.bufferSubData(gl.ARRAY_BUFFER, 0,
663
+ this.vertexDataPacked ? layout.data : render3DMeshVertexData(this, layout.vertices, layout.data));
664
+ gl.bindBuffer(gl.ARRAY_BUFFER, glArrayBuffer);
665
+ this.dirty = this.vertexDataPacked = false;
666
+ return this;
667
+ }
668
+ }
669
+ this.dispose();
670
+ const {vertices, indices} = this.getTriangles(), count = vertices.length, wide = count > 65535;
671
+ const data = render3DMeshVertexData(this, vertices);
672
+ this.vertexLayout = this.dynamicDraw ? {vertices, pointCount: this.points.length, data} : undefined;
673
+ this.vertexKeys = undefined; // used once: an edit after this may tell entries apart
674
+ this.buffer = gl.createBuffer();
675
+ this.indexBuffer = gl.createBuffer();
676
+ this.bufferCount = indices.length;
677
+ this.indexType = wide ? gl.UNSIGNED_INT : gl.UNSIGNED_SHORT;
678
+ this.dirty = this.vertexDataPacked = false;
679
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
680
+ gl.bufferData(gl.ARRAY_BUFFER, data, this.dynamicDraw ? gl.DYNAMIC_DRAW : gl.STATIC_DRAW);
681
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
682
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, wide ? new Uint32Array(indices) : new Uint16Array(indices), gl.STATIC_DRAW);
683
+ this.contextGeneration = render3D.contextGeneration;
684
+ const buffers = {buffer: this.buffer, indexBuffer: this.indexBuffer, generation: this.contextGeneration};
685
+ render3DMeshBuffers?.register(this, buffers, this);
686
+ gl.bindBuffer(gl.ARRAY_BUFFER, glArrayBuffer); // the engine's 2D batch writes through this binding
687
+ return this;
688
+ }
689
+
690
+ /** The mesh as an indexed triangle list, what upload sends to the GPU: the strip's real triangles over its
691
+ * distinct vertices, the joins between its pieces dropped and every triangle facing the way it did in the strip
692
+ * - Vertices are compared to a millionth, so two at one place with the same normal, uv and color are one
693
+ * @return {{vertices: Array<number>, indices: Array<number>}} - vertices are strip indices, one per distinct
694
+ * vertex; indices are the triangles, three per triangle, into vertices */
695
+ getTriangles()
696
+ {
697
+ const count = this.points.length, vertices = [];
698
+ if (this.indices)
699
+ {
700
+ // already a list: every vertex as it is, and the triangles read the way the pass draws, clockwise
701
+ for (let i = 0; i < count; ++i)
702
+ vertices.push(i);
703
+ return {vertices, indices: render3DFlipTriangles(this.indices.slice())};
704
+ }
705
+ const remap = new Int32Array(count), place = new Int32Array(count), keys = this.vertexKeys;
706
+ if (keys && keys.length === count)
707
+ {
708
+ // a builder said which entries are one vertex: the first entry with a key stands for every entry with it,
709
+ // and is their place, so nothing is searched
710
+ const first = new Int32Array(count).fill(-1);
711
+ for (let i = 0; i < count; ++i)
712
+ {
713
+ const key = keys[i];
714
+ ASSERT(key >= 0 && key < count, 'vertexKeys must be whole numbers below the entry count', key);
715
+ const j = first[key];
716
+ if (j < 0)
717
+ first[key] = i, remap[i] = vertices.length, vertices.push(i);
718
+ else
719
+ remap[i] = remap[j];
720
+ place[i] = first[key];
721
+ }
722
+ return {vertices, indices: render3DStripTriangles(count, remap, place)};
723
+ }
724
+
725
+ // each vertex as nine whole numbers, its values in millionths and its color, so vertices hash and compare as
726
+ // numbers; a hash table over all nine finds the distinct vertices and one over the first three the places,
727
+ // each slot holding a vertex index plus one and a taken slot moving on to the next
728
+ const values = new Float64Array(count * 9);
729
+ let size = 1;
730
+ while (size < count * 2) size *= 2;
731
+ const mask = size - 1, seen = new Int32Array(size), places = new Int32Array(size);
732
+ const mix = (h, v)=> Math.imul(h ^ v, 0x9e3779b1) >>> 0;
733
+ for (let i = 0; i < count; ++i)
734
+ {
735
+ const p = this.points[i], n = this.normals[i] || RENDER3D_DEFAULT_NORMAL;
736
+ const uv = this.uvs[i] || RENDER3D_DEFAULT_UV, k = i * 9;
737
+ const x = values[k] = round(p.x * 1e6), y = values[k+1] = round(p.y * 1e6), z = values[k+2] = round(p.z * 1e6);
738
+ values[k+3] = round(n.x * 1e6), values[k+4] = round(n.y * 1e6), values[k+5] = round(n.z * 1e6);
739
+ values[k+6] = round(uv.x * 1e6), values[k+7] = round(uv.y * 1e6);
740
+ values[k+8] = (this.colors[i] || WHITE).rgbaInt();
741
+ let h = mix(mix(mix(0x811c9dc5, x), y), z);
742
+
743
+ // the place, shared with a vertex there that has another normal or uv, as at a lathe's poles
744
+ for (let slot = h & mask;; slot = (slot + 1) & mask)
745
+ {
746
+ const o = places[slot] - 1;
747
+ if (o < 0) { places[slot] = i + 1, place[i] = i; break; }
748
+ if (values[o*9] === x && values[o*9+1] === y && values[o*9+2] === z) { place[i] = o; break; }
749
+ }
750
+
751
+ // the whole vertex
752
+ for (let m = 3; m < 9; ++m)
753
+ h = mix(h, values[k+m]);
754
+ for (let slot = h & mask;; slot = (slot + 1) & mask)
755
+ {
756
+ const o = seen[slot] - 1;
757
+ if (o < 0) { seen[slot] = i + 1, remap[i] = vertices.length, vertices.push(i); break; }
758
+ let same = true;
759
+ for (let m = 0; same && m < 9; ++m)
760
+ same = values[o*9+m] === values[k+m];
761
+ if (same) { remap[i] = remap[o]; break; }
762
+ }
763
+ }
764
+ return {vertices, indices: render3DStripTriangles(count, remap, place)};
765
+ }
766
+
767
+ /** Draw the mesh with the current draw state, batched with its other uses in the opaque stage
768
+ * @param {Matrix4|Vector3} [matrix] - Object transform, or just a position to draw it at
769
+ * @param {TileInfo|TextureInfo} [tileInfo] - Texture, mesh uvs map across the tile or the whole texture
770
+ * @param {Color} [color] - Tint */
771
+ render(matrix, tileInfo, color) { render3D?.drawMesh(this, matrix, tileInfo, color); }
772
+
773
+ /** Delete the GPU buffer now, the CPU arrays stay so the mesh can be rendered again
774
+ * - Optional, the buffer is freed anyway once the mesh is garbage collected, this frees it right away */
775
+ dispose()
776
+ {
777
+ if (!this.buffer) return;
778
+ render3DMeshBuffers?.unregister(this); // freed here, so not again when the mesh is collected
779
+ // a buffer from a context that was lost is gone with it, and the new context refuses to delete it
780
+ if (this.contextGeneration === render3D?.contextGeneration)
781
+ glContext?.deleteBuffer(this.buffer), glContext?.deleteBuffer(this.indexBuffer);
782
+ this.buffer = this.indexBuffer = undefined;
783
+ this.bufferCount = 0;
784
+ }
785
+ }
786
+
787
+ ///////////////////////////////////////////////////////////////////////////////
788
+ // Shape builders, all centered on the origin so buildMatrix does placement
789
+ // Sizes are full sizes like buildBox and the 2D drawCircle, sides go around an axis and rings along it
790
+
791
+ /**
792
+ * Spin a flat outline around the Y axis to make a round shape, like a vase or a wheel
793
+ * - profile is [[radius, y], ...] from bottom to top
794
+ * - A profile that ends where it starts makes a closed ring like a donut
795
+ * - An end left open, with a radius and no cap, makes the mesh doubleSided so its inside shows
796
+ * - An end on the axis smooth shades as a round pole like a sphere's when its segment is within 45 degrees of
797
+ * level, and as a point like a cone's tip when it is steeper
798
+ * @param {Array<Array<number>>} profile
799
+ * @param {number} [sides] - Around the axis
800
+ * @param {boolean} [smooth] - Defaults to render3D.smoothShading
801
+ * @param {boolean} [capped] - Close the ends that have a radius with flat discs
802
+ * @return {Mesh}
803
+ * @memberof Render3D
804
+ * @example
805
+ * const vase = buildLathe([[0, -1], [.8, -.3], [.9, .2], [.4, .6], [0, 1]], 12);
806
+ */
807
+ function buildLathe(profile, sides=16, smooth=render3D?.smoothShading, capped=true)
808
+ {
809
+ ASSERT(isArray(profile) && profile.length > 1, 'lathe profile needs at least 2 points');
810
+ sides |= 0;
811
+ ASSERT(sides > 2, 'lathe needs at least 3 sides');
812
+ const mesh = new Mesh;
813
+ const rings = profile.length;
814
+ const point = (i, a)=> vec3(sin(a) * profile[i][0], profile[i][1], cos(a) * profile[i][0]);
815
+
816
+ // 2D outward normal of each profile segment, in (radius, y) space
817
+ const segmentNormal = (i)=>
818
+ {
819
+ const [r0, y0] = profile[i], [r1, y1] = profile[i+1];
820
+ const n = vec2(y1 - y0, r0 - r1);
821
+ return n.length() ? n.normalize() : vec2(1, 0);
822
+ };
823
+ // vertex normal: average of the adjacent segment normals, across the seam when the profile is closed,
824
+ // and a closed profile needs no caps
825
+ const closed = rings > 2 && abs(profile[0][0] - profile[rings-1][0]) < 1e-9 &&
826
+ abs(profile[0][1] - profile[rings-1][1]) < 1e-9;
827
+ const segmentLength = (i)=> hypot(profile[i+1][0] - profile[i][0], profile[i+1][1] - profile[i][1]);
828
+ const vertexNormal = (i)=>
829
+ {
830
+ // an open end on the axis is a pole and points along it, like a sphere's, when the surface there is
831
+ // within 45 degrees of level; a steeper one is a point like a cone's tip and takes its side's normal
832
+ if (!closed && (!i || i == rings - 1) && abs(profile[i][0]) < 1e-9)
833
+ {
834
+ const up = i ? 1 : -1;
835
+ if (segmentNormal(i ? i - 1 : 0).y * up > Math.SQRT1_2 - 1e-9)
836
+ return vec2(0, up);
837
+ }
838
+ // otherwise the neighbors weighted by their length, so a short band does not tilt a long wall
839
+ let n = vec2();
840
+ const add = (s)=> n = n.add(segmentNormal(s).scale(segmentLength(s)));
841
+ if (i > 0) add(i - 1);
842
+ else if (closed) add(rings - 2);
843
+ if (i < rings - 1) add(i);
844
+ else if (closed) add(0);
845
+ return n.length() ? n.normalize() : vec2(1, 0);
846
+ };
847
+ const normal3D = (n, a)=> vec3(sin(a) * n.x, n.y, cos(a) * n.x);
848
+
849
+ // v runs along the profile by arc length
850
+ const lengths = [0];
851
+ for (let i = 1; i < rings; ++i)
852
+ lengths[i] = lengths[i-1] + hypot(profile[i][0] - profile[i-1][0], profile[i][1] - profile[i-1][1]);
853
+ const total = lengths[rings - 1] || 1;
854
+ const v = (i)=> 1 - lengths[i] / total;
855
+
856
+ for (let i = 0; i + 1 < rings; ++i)
857
+ {
858
+ if (smooth)
859
+ {
860
+ // one ribbon around the ring pair, top point then bottom point per column
861
+ const points = [], normals = [], uvs = [];
862
+ const n0 = vertexNormal(i), n1 = vertexNormal(i + 1);
863
+ // a point on the axis is one per column, each drawn by the face beside it, so its normal turns half
864
+ // a side toward the middle of that face; a pole's points along the axis and does not turn
865
+ const half = PI / sides;
866
+ const turn1 = abs(profile[i + 1][0]) < 1e-9 ? -half : 0, turn0 = abs(profile[i][0]) < 1e-9 ? half : 0;
867
+ for (let j = 0; j <= sides; ++j)
868
+ {
869
+ const a = j / sides * 2 * PI, u = j / sides;
870
+ points.push(point(i + 1, a), point(i, a));
871
+ normals.push(normal3D(n1, a + turn1), normal3D(n0, a + turn0));
872
+ uvs.push(vec2(u, v(i + 1)), vec2(u, v(i)));
873
+ }
874
+ mesh.addStrip(points, normals, uvs);
875
+ }
876
+ else
877
+ {
878
+ // one quad per side with its face normal
879
+ const n = segmentNormal(i);
880
+ for (let j = 0; j < sides; ++j)
881
+ {
882
+ const a0 = j / sides * 2 * PI, a1 = (j + 1) / sides * 2 * PI;
883
+ const u0 = j / sides, u1 = (j + 1) / sides;
884
+ mesh.addStrip(
885
+ [point(i + 1, a0), point(i, a0), point(i + 1, a1), point(i, a1)],
886
+ normal3D(n, (a0 + a1) / 2),
887
+ [vec2(u0, v(i + 1)), vec2(u0, v(i)), vec2(u1, v(i + 1)), vec2(u1, v(i))]);
888
+ }
889
+ }
890
+ }
891
+
892
+ // flat discs close the ends that have a radius, a hard edge even when the sides are smooth
893
+ if (capped && !closed)
894
+ for (const [i, up] of /** @type {Array<[number, boolean]>} */ ([[0, false], [rings - 1, true]]))
895
+ {
896
+ if (abs(profile[i][0]) < 1e-9) continue; // a pole has no cap
897
+ const points = [], uvs = [];
898
+ for (let j = 0; j < sides; ++j)
899
+ {
900
+ const a = (up ? j : -j) / sides * 2 * PI; // counter clockwise seen from outside
901
+ points.push(point(i, a));
902
+ uvs.push(vec2(sin(a) * .5 + .5, cos(a) * .5 + .5));
903
+ }
904
+ mesh.addStrip(render3DPolygonStrip(points), vec3(0, up ? 1 : -1, 0), render3DPolygonStrip(uvs));
905
+ }
906
+
907
+ // an end left open shows the inside, so it is seen from both sides
908
+ mesh.doubleSided = !closed && !capped && (abs(profile[0][0]) > 1e-9 || abs(profile[rings-1][0]) > 1e-9);
909
+ return mesh;
910
+ }
911
+
912
+ /**
913
+ * Build a sphere centered on the origin
914
+ * @param {number} [size] - Diameter
915
+ * @param {number} [sides] - Around
916
+ * @param {number} [rings] - Top to bottom
917
+ * @param {boolean} [smooth] - Defaults to render3D.smoothShading
918
+ * @return {Mesh}
919
+ * @memberof Render3D
920
+ */
921
+ function buildSphere(size=1, sides=16, rings=8, smooth=render3D?.smoothShading)
922
+ {
923
+ ASSERT(rings > 1, 'sphere needs at least 2 rings');
924
+ const profile = [];
925
+ for (let i = 0; i <= rings; ++i)
926
+ {
927
+ const a = i / rings * PI - PI/2;
928
+ profile.push([cos(a) * size / 2, sin(a) * size / 2]);
929
+ }
930
+ return buildLathe(profile, sides, smooth);
931
+ }
932
+
933
+ // 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
934
+ // circle, and an eighth of a sphere at each corner; one segment makes them flat, a chamfer
935
+ function render3DBevelBox(half, t, segments, faces)
936
+ {
937
+ const mesh = new Mesh, inner = half.subtract(vec3(t)), n = max(1, segments | 0);
938
+
939
+ // a triangle facing out, since the box is convex around the origin, flat shaded or with the normals of the
940
+ // round, and uvs from the face it faces most, as a plain box maps that face
941
+ const triangle = (a, b, c, na, nb, nc)=>
942
+ {
943
+ let normal = b.subtract(a).cross(c.subtract(a));
944
+ if (normal.lengthSquared() < 1e-20) return; // an edge or face with no size
945
+ if (normal.dot(a.add(b).add(c)) < 0)
946
+ [b, c, nb, nc, normal] = [c, b, nc, nb, normal.scale(-1)];
947
+ normal = normal.normalize();
948
+ let face = faces[0];
949
+ for (const f of faces)
950
+ if (f[0].dot(normal) > face[0].dot(normal))
951
+ face = f;
952
+ const [, r, u] = face, hr = abs(half.dot(r)), hu = abs(half.dot(u));
953
+ const uv = (p)=> vec2(.5 + p.dot(r) / (2 * hr), .5 - p.dot(u) / (2 * hu));
954
+ mesh.addStrip([a, b, c], n > 1 && na ? [na, nb, nc] : normal, [uv(a), uv(b), uv(c)]);
955
+ };
956
+
957
+ // the faces, shrunk by the bevel
958
+ for (const [normal, r, u] of faces)
959
+ {
960
+ const [a, b, c, d] = render3DQuadAxes(normal.multiply(half), r.multiply(inner), u.multiply(inner));
961
+ triangle(a, b, c);
962
+ triangle(c, b, d);
963
+ }
964
+
965
+ // the edges, each a strip around a quarter circle from one face to the next
966
+ const axes = [vec3(1, 0, 0), vec3(0, 1, 0), vec3(0, 0, 1)];
967
+ for (let k = 0; k < 3; ++k)
968
+ for (const si of [-1, 1])
969
+ for (const sj of [-1, 1])
970
+ {
971
+ const n1 = axes[(k + 1) % 3].scale(si), n2 = axes[(k + 2) % 3].scale(sj);
972
+ const middle = n1.multiply(inner).add(n2.multiply(inner)), along = axes[k].multiply(inner);
973
+ const start = middle.subtract(along), end = middle.add(along);
974
+ const dir = (m)=> n1.scale(cos(m / n * PI / 2)).add(n2.scale(sin(m / n * PI / 2)));
975
+ for (let m = 0; m < n; ++m)
976
+ {
977
+ const d0 = dir(m), d1 = dir(m + 1);
978
+ const a = start.add(d0.scale(t)), b = end.add(d0.scale(t));
979
+ const c = start.add(d1.scale(t)), d = end.add(d1.scale(t));
980
+ triangle(a, b, c, d0, d0, d1);
981
+ triangle(c, b, d, d1, d0, d1);
982
+ }
983
+ }
984
+
985
+ // the corners, an eighth of a sphere in rows from one face's axis down to the edge between the other two
986
+ for (const sx of [-1, 1])
987
+ for (const sy of [-1, 1])
988
+ for (const sz of [-1, 1])
989
+ {
990
+ const nx = vec3(sx, 0, 0), ny = vec3(0, sy, 0), nz = vec3(0, 0, sz), corner = inner.multiply(vec3(sx, sy, sz));
991
+ const dir = (i, j)=>
992
+ {
993
+ const polar = i / n * PI / 2, around = i ? j / i * PI / 2 : 0;
994
+ return nz.scale(cos(polar)).add(nx.scale(cos(around) * sin(polar))).add(ny.scale(sin(around) * sin(polar)));
995
+ };
996
+ const point = (i, j)=> corner.add(dir(i, j).scale(t));
997
+ for (let i = 0; i < n; ++i)
998
+ {
999
+ for (let j = 0; j <= i; ++j)
1000
+ 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));
1001
+ for (let j = 0; j < i; ++j)
1002
+ 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));
1003
+ }
1004
+ }
1005
+ return mesh;
1006
+ }
1007
+
1008
+ /**
1009
+ * Build a box centered on the origin, six flat faces with uvs covering each face
1010
+ * - bevel cuts its edges and corners: 1 segment is a flat chamfer, more round them, and the biggest bevel, half
1011
+ * the smallest side, rounds a cube into a ball
1012
+ * @param {Vector3|number} [size] - Full size, a number for a cube
1013
+ * @param {number} [bevel] - Size of the cut on each edge, clamped to half the smallest side
1014
+ * @param {number} [bevelSegments] - Steps around each edge, 1 for a flat chamfer
1015
+ * @return {Mesh}
1016
+ * @memberof Render3D
1017
+ */
1018
+ function buildBox(size=1, bevel=0, bevelSegments=1)
1019
+ {
1020
+ ASSERT(isNumber(bevel) && bevel >= 0, 'bevel must be a number, 0 or more');
1021
+ const half = render3DSize3(size).scale(.5);
1022
+ // each face: normal, right axis, up axis (right cross up = normal)
1023
+ const faces = [
1024
+ [vec3(0, 0, 1), vec3(1, 0, 0), vec3(0, 1, 0)],
1025
+ [vec3(0, 0, -1), vec3(-1, 0, 0), vec3(0, 1, 0)],
1026
+ [vec3(1, 0, 0), vec3(0, 0, -1), vec3(0, 1, 0)],
1027
+ [vec3(-1, 0, 0), vec3(0, 0, 1), vec3(0, 1, 0)],
1028
+ [vec3(0, 1, 0), vec3(1, 0, 0), vec3(0, 0, -1)],
1029
+ [vec3(0, -1, 0), vec3(1, 0, 0), vec3(0, 0, 1)],
1030
+ ];
1031
+ const t = min(bevel, half.x, half.y, half.z);
1032
+ if (t > 0)
1033
+ return render3DBevelBox(half, t, bevelSegments, faces);
1034
+ const mesh = new Mesh;
1035
+ for (const [n, r, u] of faces)
1036
+ {
1037
+ const center = n.multiply(half);
1038
+ const right = r.multiply(half), up = u.multiply(half);
1039
+ mesh.addStrip(render3DQuadAxes(center, right, up), n, RENDER3D_QUAD_UVS);
1040
+ }
1041
+ return mesh;
1042
+ }
1043
+
1044
+ /**
1045
+ * Build a heightfield grid in the XZ plane centered on the origin
1046
+ * - smooth rounds the lighting across cells and colors each corner
1047
+ * - flat lights and colors each cell on its own, so a checkerboard stays crisp
1048
+ * - doubleSided, a sheet seen from both sides; turn it off for ground only ever seen from above
1049
+ * - One cell is a plain square, render3D.planeMesh and planeMeshDoubleSided are shared ones
1050
+ * @param {Vector2|number} [size] - World size along X and Z, a number for a square
1051
+ * @param {Vector2|number} [segments] - Cells along X and Z, a number for both
1052
+ * @param {Color|function(number, number): Color} [color] - One Color for the whole grid, or (x, z) => Color
1053
+ * @param {function(number, number): number} [heightFunction] - (x, z) => y, default flat
1054
+ * @param {boolean} [smooth] - Defaults to render3D.smoothShading with a heightFunction; a flat grid is flat shaded,
1055
+ * since its light is even anyway and smoothing would only blend its cell colors; pass smooth to blend a gradient
1056
+ * @return {Mesh}
1057
+ * @memberof Render3D
1058
+ * @example
1059
+ * const ground = buildGrid(vec2(20), 10, (x, z)=> (floor(x / 2) + floor(z / 2)) & 1 ? GRAY : WHITE); // 2 unit checks
1060
+ */
1061
+ function buildGrid(size=vec2(1), segments=1, color, heightFunction, smooth=heightFunction && render3D?.smoothShading)
1062
+ {
1063
+ heightFunction ||= ()=> 0;
1064
+ size = render3DSize2(size);
1065
+ segments = render3DSize2(segments);
1066
+ ASSERT(segments.x > 0 && segments.y > 0 && segments.x % 1 === 0 && segments.y % 1 === 0,
1067
+ 'grid segments must be whole numbers above zero');
1068
+ const mesh = new Mesh;
1069
+ const segmentsX = segments.x, segmentsZ = segments.y;
1070
+ const cellX = size.x / segmentsX, cellZ = size.y / segmentsZ;
1071
+ const halfX = size.x / 2, halfZ = size.y / 2, ex = cellX / 2, ez = cellZ / 2;
1072
+ const px = (i)=> i * cellX - halfX, pz = (j)=> j * cellZ - halfZ;
1073
+ const colorAt = /** @type {function(number, number): Color} */ (color);
1074
+ const cellColor = (i, j)=> !color ? WHITE : isColor(color) ? /** @type {Color} */ (color) : colorAt(px(i), pz(j));
1075
+ // a big terrain has millions of vertices, so each one is made once, shared by the rows above and below it
1076
+ const row = (j)=>
1077
+ {
1078
+ const points = [], normals = [], uvs = [], colors = [], z = pz(j);
1079
+ for (let i = 0; i <= segmentsX; ++i)
1080
+ {
1081
+ const x = px(i);
1082
+ points.push(vec3(x, heightFunction(x, z), z));
1083
+ uvs.push(vec2(i / segmentsX, j / segmentsZ));
1084
+ if (!smooth) continue;
1085
+ normals.push(render3DSlopeNormal(heightFunction, x, z, ex, ez, halfX, halfZ));
1086
+ colors.push(cellColor(i, j));
1087
+ }
1088
+ return {points, normals, uvs, colors};
1089
+ };
1090
+ let above = row(0);
1091
+ for (let j = 0; j < segmentsZ; ++j)
1092
+ {
1093
+ const below = row(j + 1);
1094
+ if (smooth)
1095
+ {
1096
+ // one ribbon per row with vertex normals from the slope
1097
+ const points = [], normals = [], uvs = [], colors = [];
1098
+ for (let i = 0; i <= segmentsX; ++i)
1099
+ {
1100
+ points.push(above.points[i], below.points[i]);
1101
+ normals.push(above.normals[i], below.normals[i]);
1102
+ uvs.push(above.uvs[i], below.uvs[i]);
1103
+ colors.push(above.colors[i], below.colors[i]);
1104
+ }
1105
+ mesh.addStrip(points, normals, uvs, colors);
1106
+ }
1107
+ else
1108
+ {
1109
+ // one quad per cell with its face normal and one color sampled at its center
1110
+ for (let i = 0; i < segmentsX; ++i)
1111
+ mesh.addQuad(above.points[i], below.points[i], below.points[i+1], above.points[i+1], cellColor(i + .5, j + .5),
1112
+ [above.uvs[i], below.uvs[i], below.uvs[i+1], above.uvs[i+1]]);
1113
+ }
1114
+ above = below;
1115
+ }
1116
+ if (smooth)
1117
+ {
1118
+ // the grid knows which strip entries are one vertex, each shared by the rows above and below it and by the
1119
+ // repeats at the ends of its ribbons, so the upload of a big terrain skips searching millions of entries
1120
+ const n = 2 * (segmentsX + 1) + 2, keys = mesh.vertexKeys = new Int32Array(mesh.points.length);
1121
+ const id = (i, j)=> j * (segmentsX + 1) + i;
1122
+ for (let j = 0; j < segmentsZ; ++j)
1123
+ {
1124
+ const start = j * n;
1125
+ keys[start] = id(0, j); // the leading repeat
1126
+ for (let i = 0; i <= segmentsX; ++i)
1127
+ keys[start + 1 + 2*i] = id(i, j), keys[start + 2 + 2*i] = id(i, j + 1);
1128
+ keys[start + n - 1] = id(segmentsX, j + 1); // the trailing repeat
1129
+ }
1130
+ }
1131
+ mesh.doubleSided = true; // a sheet, seen from both sides; terrain seen only from above can turn it off
1132
+ return mesh;
1133
+ }
1134
+
1135
+ /**
1136
+ * Build a sky dome: a sphere colored by direction, wound to be seen from inside
1137
+ * - set it as render3D.sky and the pass draws it around the camera behind everything
1138
+ * @param {Color} [topColor] - Straight up
1139
+ * @param {Color} [horizonColor] - Level with the camera
1140
+ * @param {Color} [bottomColor] - Straight down, what a camera looking at the ground sees past its edge; defaults to the
1141
+ * horizon color
1142
+ * @param {number} [sides] - Around
1143
+ * @param {number} [rings] - Top to bottom
1144
+ * @return {Mesh}
1145
+ * @memberof Render3D
1146
+ */
1147
+ function buildSky(topColor=hsl(.6, .8, .55), horizonColor=hsl(.6, 1, .9), bottomColor=horizonColor, sides=16, rings=8)
1148
+ {
1149
+ // a sphere turned inside out so it is seen from within, each point colored by how high it is
1150
+ const mesh = buildSphere(2, sides, rings, true).flipNormals();
1151
+ mesh.colors = mesh.points.map(p=> p.y < 0 ? horizonColor.lerp(bottomColor, -p.y) : horizonColor.lerp(topColor, p.y));
1152
+ render3DSkyColors.set(mesh, [topColor.copy(), horizonColor.copy(), bottomColor.copy()]);
1153
+ return mesh;
1154
+ }
1155
+
1156
+ // the top, horizon and bottom colors of each sky dome buildSky made, which a reflection shows while that dome is
1157
+ // render3D.sky, whether setSky set it or the game did
1158
+ const render3DSkyColors = new WeakMap;