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
@@ -1,942 +1,1163 @@
1
- /**
2
- * LittleJS WebGL Interface
3
- * - WebGL2 rendering engine for high-performance graphics
4
- * - Batched sprite rendering for drawing thousands of sprites efficiently
5
- * - Instanced rendering using vertex array objects (VAOs)
6
- * - Polygon rendering with triangle strip support
7
- * - Shader system with custom vertex and fragment shaders
8
- * - Texture management with automatic atlas support
9
- * - Post-processing effects via framebuffer and shader plugins
10
- * - Automatic fallback to Canvas2D if WebGL is unavailable
11
- * - Context loss and restoration handling
12
- * - Can be disabled with glEnable setting
13
- * - Advanced users can create custom shaders and render targets
14
- * @namespace WebGL
15
- */
16
-
17
- 'use strict';
18
-
19
- /** The WebGL canvas which appears below the main canvas
20
- * @type {HTMLCanvasElement}
21
- * @memberof WebGL */
22
- let glCanvas;
23
-
24
- /** WebGL2 context for `glCanvas`
25
- * @type {WebGL2RenderingContext}
26
- * @memberof WebGL */
27
- let glContext;
28
-
29
- /** Should WebGL be setup with anti-aliasing? must be set before calling engineInit
30
- * @type {boolean}
31
- * @memberof WebGL */
32
- let glAntialias = true;
33
-
34
- // WebGL internal variables not exposed to documentation
35
- let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos, glInstancedVAO, glPolyVAO, glFramebuffer, glRenderTarget, glCanBeEnabled = true;
36
-
37
- // WebGL internal constants
38
- const gl_ARRAY_BUFFER_SIZE = 5e5;
39
- const gl_INDICES_PER_INSTANCE = 11;
40
- const gl_INSTANCE_BYTE_STRIDE = gl_INDICES_PER_INSTANCE * 4;
41
- const gl_MAX_INSTANCES = gl_ARRAY_BUFFER_SIZE / gl_INSTANCE_BYTE_STRIDE | 0;
42
- const gl_INDICES_PER_POLY_VERTEX = 3;
43
- const gl_POLY_VERTEX_BYTE_STRIDE = gl_INDICES_PER_POLY_VERTEX * 4;
44
- const gl_MAX_POLY_VERTEXES = gl_ARRAY_BUFFER_SIZE / gl_POLY_VERTEX_BYTE_STRIDE | 0;
45
-
46
- ///////////////////////////////////////////////////////////////////////////////
47
-
48
- // Initialize WebGL, called automatically by the engine
49
- function glInit(rootElement)
50
- {
51
- // keep set of texture infos so they can be restored if context is lost
52
- glTextureInfos = new Set;
53
-
54
- if (!glEnable || headlessMode)
55
- {
56
- glCanBeEnabled = false;
57
- return;
58
- }
59
-
60
- // create the canvas and textures
61
- glCanvas = document.createElement('canvas');
62
- glContext = glCanvas.getContext('webgl2', {antialias:glAntialias});
63
-
64
- if (!glContext)
65
- {
66
- console.warn('WebGL2 not supported, falling back to 2D canvas rendering!');
67
- glCanvas = glContext = undefined;
68
- glEnable = false;
69
- glCanBeEnabled = false;
70
- return;
71
- }
72
-
73
- // attach the WebGL canvas;
74
- rootElement.appendChild(glCanvas);
75
-
76
- // startup webgl
77
- initWebGL();
78
-
79
- // setup context lost and restore handlers
80
- glCanvas.addEventListener('webglcontextlost', (e)=>
81
- {
82
- glEnable = false; // disable WebGL rendering
83
- glCanvas.style.display = 'none'; // hide the gl canvas
84
- e.preventDefault(); // prevent default to allow restoration
85
- LOG('WebGL context lost! Switching to Canvas2d rendering.');
86
-
87
- // remove WebGL textures
88
- for (const info of glTextureInfos)
89
- info.glTexture = undefined;
90
- glActiveTexture = undefined;
91
- // drop any partially-filled batch so the next glFlush doesn't
92
- // upload stale glBatchCount against fresh empty buffers on restore
93
- glBatchCount = 0;
94
- glPolyMode = false;
95
- pluginList.forEach(plugin=>plugin.glContextLost?.());
96
- });
97
- glCanvas.addEventListener('webglcontextrestored', ()=>
98
- {
99
- glEnable = true; // re-enable WebGL rendering
100
- glCanvas.style.display = ''; // show the gl canvas
101
- LOG('WebGL context restored, reinitializing...');
102
-
103
- // reinit WebGL and restore textures
104
- initWebGL();
105
- for (const info of glTextureInfos)
106
- info.glTexture = glCreateTexture(info.image, info.wrap);
107
- pluginList.forEach(plugin=>plugin.glContextRestored?.());
108
- });
109
-
110
- function initWebGL()
111
- {
112
- // setup instanced rendering shader program
113
- glShader = glCreateProgram(
114
- '#version 300 es\n' + // specify GLSL ES version
115
- 'precision highp float;'+ // use highp for accuracy
116
- 'uniform mat4 m;'+ // transform matrix
117
- 'in vec2 g;'+ // in: geometry
118
- 'in vec4 p,u,c,a;'+ // in: position/size, uvs, color, additiveColor
119
- 'in float r;'+ // in: rotation
120
- 'out vec2 v;'+ // out: uv
121
- 'out vec4 d,e;'+ // out: color, additiveColor
122
- 'void main(){'+ // shader entry point
123
- 'vec2 s=(g-.5)*p.zw;'+ // get size offset
124
- 'gl_Position=m*vec4(p.xy+s*cos(r)-vec2(-s.y,s)*sin(r),1,1);'+ // transform position
125
- 'v=mix(u.xw,u.zy,g);'+ // pass uv to fragment shader
126
- 'd=c;e=a;'+ // pass colors to fragment shader
127
- '}' // end of shader
128
- ,
129
- '#version 300 es\n' + // specify GLSL ES version
130
- 'precision highp float;'+ // use highp for accuracy
131
- 'uniform sampler2D s;'+ // texture
132
- 'in vec2 v;'+ // in: uv
133
- 'in vec4 d,e;'+ // in: color, additiveColor
134
- 'out vec4 c;'+ // out: color
135
- 'void main(){'+ // shader entry point
136
- 'c=texture(s,v)*d+e;'+ // modulate texture by color plus additive
137
- '}' // end of shader
138
- );
139
-
140
- // setup poly rendering shaders
141
- glPolyShader = glCreateProgram(
142
- '#version 300 es\n' + // specify GLSL ES version
143
- 'precision highp float;'+ // use highp for better accuracy
144
- 'uniform mat4 m;'+ // transform matrix
145
- 'in vec2 p;'+ // in: position
146
- 'in vec4 c;'+ // in: color
147
- 'out vec4 d;'+ // out: color
148
- 'void main(){'+ // shader entry point
149
- 'gl_Position=m*vec4(p,1,1);'+ // transform position
150
- 'd=c;'+ // pass color to fragment shader
151
- '}' // end of shader
152
- ,
153
- '#version 300 es\n' + // specify GLSL ES version
154
- 'precision highp float;'+ // use highp for better accuracy
155
- 'in vec4 d;'+ // in: color
156
- 'out vec4 c;'+ // out: color
157
- 'void main(){'+ // shader entry point
158
- 'c=d;'+ // set color
159
- '}' // end of shader
160
- );
161
-
162
- // init buffers
163
- const glInstanceData = new ArrayBuffer(gl_ARRAY_BUFFER_SIZE);
164
- glPositionData = new Float32Array(glInstanceData);
165
- glColorData = new Uint32Array(glInstanceData);
166
- glArrayBuffer = glContext.createBuffer();
167
- glGeometryBuffer = glContext.createBuffer();
168
- glFramebuffer = glContext.createFramebuffer();
169
- glBatchCount = 0;
170
-
171
- // create the geometry buffer, triangle strip square
172
- const geometry = new Float32Array([0,0,1,0,0,1,1,1]);
173
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
174
- glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
175
-
176
- let offset, shader, stride;
177
- const initVertexAttrib = (name, type, typeSize, size, divisor=0)=>
178
- {
179
- const location = glContext.getAttribLocation(shader, name);
180
- const normalize = typeSize === 1;
181
- const fixedStride = typeSize && stride;
182
- glContext.enableVertexAttribArray(location);
183
- glContext.vertexAttribPointer(location, size, type, normalize, fixedStride, offset);
184
- glContext.vertexAttribDivisor(location, divisor);
185
- offset += size*typeSize;
186
- }
187
-
188
- // setup VAO for instanced rendering
189
- glInstancedVAO = glContext.createVertexArray();
190
- glContext.bindVertexArray(glInstancedVAO);
191
-
192
- // configure instanced vertex attributes
193
- offset = 0, shader = glShader, stride = gl_INSTANCE_BYTE_STRIDE;
194
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
195
- initVertexAttrib('g', glContext.FLOAT, 0, 2); // geometry
196
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
197
- glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
198
- initVertexAttrib('p', glContext.FLOAT, 4, 4, 1); // position & size
199
- initVertexAttrib('u', glContext.FLOAT, 4, 4, 1); // texture coords
200
- initVertexAttrib('c', glContext.UNSIGNED_BYTE, 1, 4, 1); // color
201
- initVertexAttrib('a', glContext.UNSIGNED_BYTE, 1, 4, 1); // additiveColor
202
- initVertexAttrib('r', glContext.FLOAT, 4, 1, 1); // rotation
203
-
204
- // setup VAO for poly rendering
205
- glPolyVAO = glContext.createVertexArray();
206
- glContext.bindVertexArray(glPolyVAO);
207
-
208
- // configure poly vertex attributes
209
- offset = 0, shader = glPolyShader, stride = gl_POLY_VERTEX_BYTE_STRIDE;
210
- initVertexAttrib('p', glContext.FLOAT, 4, 2); // position
211
- initVertexAttrib('c', glContext.UNSIGNED_BYTE, 1, 4); // color
212
- }
213
- }
214
-
215
- function glSetInstancedMode(force=false)
216
- {
217
- if (!force && !glPolyMode) return;
218
-
219
- // setup instanced mode
220
- glFlush();
221
- glPolyMode = false;
222
- glContext.useProgram(glShader);
223
- glContext.bindVertexArray(glInstancedVAO);
224
- }
225
-
226
- function glSetPolyMode()
227
- {
228
- if (glPolyMode) return;
229
-
230
- // setup poly mode
231
- glFlush();
232
- glPolyMode = true;
233
- glContext.useProgram(glPolyShader);
234
- glContext.bindVertexArray(glPolyVAO);
235
- }
236
-
237
- // Setup WebGL render each frame, called automatically by engine
238
- // Also used by tile layer rendering when redrawing tiles
239
- function glPreRender(clear=true)
240
- {
241
- if (!glEnable || !glContext) return;
242
-
243
- ASSERT(!glBatchCount, 'glPreRender called with unflushed batch.');
244
-
245
- if (!glRenderTarget)
246
- {
247
- // set to same size as main canvas
248
- glCanvas.width = mainCanvasSize.x;
249
- glCanvas.height = mainCanvasSize.y;
250
- }
251
- glContext.viewport(0, 0, mainCanvasSize.x, mainCanvasSize.y);
252
- clear && glClearCanvas();
253
-
254
- // build the transform matrix
255
- const s = vec2(2*cameraScale).divide(mainCanvasSize);
256
- if (glRenderTarget)
257
- s.y = -s.y; // invert y when using render target
258
- const rotatedCam = cameraPos.rotate(-cameraAngle);
259
- const p = vec2(-1).subtract(rotatedCam.multiply(s));
260
- const ca = cos(cameraAngle);
261
- const sa = sin(cameraAngle);
262
- const transform = [
263
- s.x * ca, s.y * sa, 0, 0,
264
- -s.x * sa, s.y * ca, 0, 0,
265
- 1, 1, 1, 0,
266
- p.x, p.y, 0, 1];
267
-
268
- // set the same transform matrix for both shaders
269
- const initUniform = (program, uniform, value)=>
270
- {
271
- glContext.useProgram(program);
272
- const location = glContext.getUniformLocation(program, uniform);
273
- glContext.uniformMatrix4fv(location, false, value);
274
- }
275
- initUniform(glPolyShader, 'm', transform);
276
- initUniform(glShader, 'm', transform);
277
-
278
- // set the active texture
279
- glContext.activeTexture(glContext.TEXTURE0);
280
- if (textureInfos[0])
281
- {
282
- glActiveTexture = textureInfos[0].glTexture;
283
- glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
284
- }
285
-
286
- // rebind the array buffer
287
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
288
-
289
- // start with additive blending off
290
- glAdditive = glBatchAdditive = false;
291
-
292
- // force it to set instanced mode
293
- glSetInstancedMode(true);
294
- }
295
-
296
- /** Clear the canvas and setup the viewport
297
- * @memberof WebGL */
298
- function glClearCanvas()
299
- {
300
- if (!glContext) return;
301
-
302
- // clear using the canvasClearColor
303
- const color = canvasClearColor;
304
- glContext.clearColor(color.r, color.g, color.b, color.a);
305
- glContext.clear(glContext.COLOR_BUFFER_BIT);
306
- }
307
-
308
- /** Set the WebGL texture, called automatically if using multiple textures
309
- * - This may also flush the gl buffer resulting in more draw calls and worse performance
310
- * @param {WebGLTexture} texture
311
- * @memberof WebGL */
312
- function glSetTexture(texture)
313
- {
314
- // must flush cache with the old texture to set a new one
315
- if (!glContext || texture === glActiveTexture) return;
316
-
317
- glFlush();
318
- glActiveTexture = texture;
319
- glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
320
- }
321
-
322
- /** Set the wrap mode (REPEAT or CLAMP_TO_EDGE) on an existing WebGL texture
323
- * Flushes the current batch only if the texture is the active one
324
- * @param {WebGLTexture} texture
325
- * @param {boolean} [wrap] - true for REPEAT, false for CLAMP_TO_EDGE
326
- * @memberof WebGL */
327
- function glSetTextureWrap(texture, wrap=true)
328
- {
329
- if (!glContext || !texture) return;
330
-
331
- // flush only if changing wrap on the currently bound texture
332
- const isCurrent = texture === glActiveTexture;
333
- if (isCurrent)
334
- glFlush();
335
- else
336
- glContext.bindTexture(glContext.TEXTURE_2D, texture);
337
-
338
- const wrapMode = wrap ? glContext.REPEAT : glContext.CLAMP_TO_EDGE;
339
- glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_S, wrapMode);
340
- glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_T, wrapMode);
341
-
342
- if (!isCurrent && glActiveTexture)
343
- glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
344
- }
345
-
346
- /** Compile WebGL shader of the given type, will throw errors if in debug mode
347
- * @param {string} source
348
- * @param {number} type
349
- * @return {WebGLShader}
350
- * @memberof WebGL */
351
- function glCompileShader(source, type)
352
- {
353
- if (!glContext) return;
354
-
355
- // build the shader
356
- const shader = glContext.createShader(type);
357
- glContext.shaderSource(shader, source);
358
- glContext.compileShader(shader);
359
-
360
- // check for errors
361
- if (debug && !glContext.getShaderParameter(shader, glContext.COMPILE_STATUS))
362
- throw glContext.getShaderInfoLog(shader);
363
- return shader;
364
- }
365
-
366
- /** Create WebGL program with given shaders
367
- * @param {string} vsSource
368
- * @param {string} fsSource
369
- * @return {WebGLProgram}
370
- * @memberof WebGL */
371
- function glCreateProgram(vsSource, fsSource)
372
- {
373
- if (!glContext) return;
374
-
375
- // build the program
376
- const program = glContext.createProgram();
377
- glContext.attachShader(program, glCompileShader(vsSource, glContext.VERTEX_SHADER));
378
- glContext.attachShader(program, glCompileShader(fsSource, glContext.FRAGMENT_SHADER));
379
- glContext.linkProgram(program);
380
-
381
- // check for errors
382
- if (debug && !glContext.getProgramParameter(program, glContext.LINK_STATUS))
383
- throw glContext.getProgramInfoLog(program);
384
- return program;
385
- }
386
-
387
- /** Create WebGL texture from an image and init the texture settings
388
- * Restores the active texture when done
389
- * @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas} [image]
390
- * @param {boolean} [wrap] - true for REPEAT, false for CLAMP_TO_EDGE
391
- * @return {WebGLTexture}
392
- * @memberof WebGL */
393
- function glCreateTexture(image, wrap=false)
394
- {
395
- if (!glContext) return;
396
-
397
- // build the texture
398
- const texture = glContext.createTexture();
399
- let mipMap = false;
400
- if (image?.width)
401
- {
402
- glSetTextureData(texture, image);
403
- glContext.bindTexture(glContext.TEXTURE_2D, texture);
404
- mipMap = !tilesPixelated && isPowerOfTwo(image.width) && isPowerOfTwo(image.height);
405
- }
406
- else
407
- {
408
- // create a white texture
409
- const whitePixel = new Uint8Array([255, 255, 255, 255]);
410
- glContext.bindTexture(glContext.TEXTURE_2D, texture);
411
- glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, 1, 1, 0, glContext.RGBA, glContext.UNSIGNED_BYTE, whitePixel);
412
- }
413
-
414
- // set texture filtering
415
- const magFilter = tilesPixelated ? glContext.NEAREST : glContext.LINEAR;
416
- const minFilter = mipMap ? glContext.LINEAR_MIPMAP_LINEAR : magFilter;
417
- glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MAG_FILTER, magFilter);
418
- glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, minFilter);
419
- const wrapMode = wrap ? glContext.REPEAT : glContext.CLAMP_TO_EDGE;
420
- glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_S, wrapMode);
421
- glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_T, wrapMode);
422
- if (mipMap)
423
- glContext.generateMipmap(glContext.TEXTURE_2D);
424
-
425
- // rebind active texture
426
- glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
427
- return texture;
428
- }
429
-
430
- /** Deletes a WebGL texture
431
- * @param {WebGLTexture} [texture]
432
- * @memberof WebGL */
433
- function glDeleteTexture(texture)
434
- {
435
- if (!glContext) return;
436
-
437
- glContext.deleteTexture(texture);
438
- }
439
-
440
- /** Set WebGL texture data from an image, restores the active texture when done
441
- * @param {WebGLTexture} texture
442
- * @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas} image
443
- * @memberof WebGL */
444
- function glSetTextureData(texture, image)
445
- {
446
- if (!glContext) return;
447
-
448
- // build the texture
449
- ASSERT(image?.width > 0, 'Invalid image data.');
450
- glContext.bindTexture(glContext.TEXTURE_2D, texture);
451
- glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
452
-
453
- // keep mipmaps in sync with new level 0 data (same condition as glCreateTexture)
454
- if (!tilesPixelated && isPowerOfTwo(image.width) && isPowerOfTwo(image.height))
455
- glContext.generateMipmap(glContext.TEXTURE_2D);
456
-
457
- // rebind active texture
458
- glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
459
- }
460
-
461
- /** Tells WebGL to create or update the glTexture and start tracking it
462
- * @param {TextureInfo} textureInfo
463
- * @memberof WebGL */
464
- function glRegisterTextureInfo(textureInfo)
465
- {
466
- if (headlessMode) return;
467
-
468
- // add texture info to tracking list even if gl is not enabled
469
- glTextureInfos.add(textureInfo);
470
-
471
- if (!glContext) return;
472
-
473
- // create or set the texture data
474
- if (textureInfo.glTexture)
475
- glSetTextureData(textureInfo.glTexture, textureInfo.image);
476
- else
477
- textureInfo.glTexture = glCreateTexture(textureInfo.image, textureInfo.wrap);
478
- }
479
-
480
- /** Tells WebGL to destroy the glTexture and stop tracking it
481
- * @param {TextureInfo} textureInfo
482
- * @memberof WebGL */
483
- function glUnregisterTextureInfo(textureInfo)
484
- {
485
- if (headlessMode) return;
486
-
487
- // delete texture info from tracking list even if gl is not enabled
488
- glTextureInfos.delete(textureInfo);
489
-
490
- // unset and destroy the texture
491
- const glTexture = textureInfo.glTexture;
492
- textureInfo.glTexture = undefined;
493
- glDeleteTexture(glTexture);
494
- }
495
-
496
- /** Draw all sprites and clear out the buffer, called automatically by the system whenever necessary
497
- * @memberof WebGL */
498
- function glFlush()
499
- {
500
- if (glEnable && glContext && glBatchCount)
501
- {
502
- // set blend mode
503
- const destBlend = glBatchAdditive ? glContext.ONE : glContext.ONE_MINUS_SRC_ALPHA;
504
- glContext.blendFuncSeparate(glContext.SRC_ALPHA, destBlend, glContext.ONE, destBlend);
505
- glContext.enable(glContext.BLEND);
506
-
507
- const byteLength = glBatchCount *
508
- (glPolyMode ? gl_INDICES_PER_POLY_VERTEX : gl_INDICES_PER_INSTANCE);
509
- glContext.bufferSubData(glContext.ARRAY_BUFFER, 0, glPositionData, 0, byteLength);
510
-
511
- // draw the batch
512
- if (glPolyMode)
513
- glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, glBatchCount);
514
- else
515
- glContext.drawArraysInstanced(glContext.TRIANGLE_STRIP, 0, 4, glBatchCount);
516
- ++drawCount;
517
- primitiveCount += glBatchCount;
518
- glBatchCount = 0;
519
- }
520
- glBatchAdditive = glAdditive;
521
- }
522
-
523
- /** Flush any sprites still in the buffer and copy to main canvas
524
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
525
- * @memberof WebGL */
526
- function glCopyToContext(context)
527
- {
528
- if (!glEnable || !glContext) return;
529
-
530
- glFlush();
531
- context.drawImage(glCanvas, 0, 0);
532
- }
533
-
534
- /** Set anti-aliasing for WebGL canvas
535
- * Must be called before engineInit
536
- * @param {boolean} [antialias]
537
- * @memberof WebGL */
538
- function glSetAntialias(antialias=true)
539
- {
540
- ASSERT(!glCanvas, 'must be called before engineInit');
541
- glAntialias = antialias;
542
- }
543
-
544
- /** Add a sprite to the gl draw list, used by all gl draw functions
545
- * @param {number} x
546
- * @param {number} y
547
- * @param {number} sizeX
548
- * @param {number} sizeY
549
- * @param {number} [angle]
550
- * @param {number} [uv0X]
551
- * @param {number} [uv0Y]
552
- * @param {number} [uv1X]
553
- * @param {number} [uv1Y]
554
- * @param {number} [rgba=-1] - white is -1
555
- * @param {number} [rgbaAdditive=0] - black is 0
556
- * @memberof WebGL */
557
- function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgba=-1, rgbaAdditive=0)
558
- {
559
- // flush if there is not enough room or if different blend mode
560
- if (glBatchCount >= gl_MAX_INSTANCES || glBatchAdditive !== glAdditive)
561
- glFlush();
562
- glSetInstancedMode();
563
-
564
- let offset = glBatchCount++ * gl_INDICES_PER_INSTANCE;
565
- glPositionData[offset++] = x;
566
- glPositionData[offset++] = y;
567
- glPositionData[offset++] = sizeX;
568
- glPositionData[offset++] = sizeY;
569
- glPositionData[offset++] = uv0X;
570
- glPositionData[offset++] = uv0Y;
571
- glPositionData[offset++] = uv1X;
572
- glPositionData[offset++] = uv1Y;
573
- glColorData[offset++] = rgba;
574
- glColorData[offset++] = rgbaAdditive;
575
- glPositionData[offset++] = angle;
576
- }
577
-
578
- /** Add an untextured rect to the gl draw list
579
- * Zeroes the uvs and rgba so the texture contribution multiplies to 0,
580
- * then carries the real color in the additive slot. Works regardless of
581
- * which texture is currently bound.
582
- * @param {number} x
583
- * @param {number} y
584
- * @param {number} sizeX
585
- * @param {number} sizeY
586
- * @param {number} angle
587
- * @param {number} rgba - color as 32-bit integer
588
- * @memberof WebGL */
589
- function glDrawUntextured(x, y, sizeX, sizeY, angle, rgba)
590
- {
591
- glDraw(x, y, sizeX, sizeY, angle, 0, 0, 0, 0, 0, rgba);
592
- }
593
-
594
- /** Transform and add a polygon to the gl draw list
595
- * @param {Array<Vector2>} points - Array of Vector2 points
596
- * @param {number} rgba - Color of the polygon as a 32-bit integer
597
- * @param {number} x
598
- * @param {number} y
599
- * @param {number} sx
600
- * @param {number} sy
601
- * @param {number} angle
602
- * @param {boolean} [tristrip] - should tristrip algorithm be used
603
- * @memberof WebGL */
604
- function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
605
- {
606
- const pointsOut = [];
607
- const sa = sin(-angle);
608
- const ca = cos(-angle);
609
- for (const p of points)
610
- {
611
- // transform the point
612
- const px = p.x*sx;
613
- const py = p.y*sy;
614
- pointsOut.push(vec2(x + ca*px - sa*py, y + sa*px + ca*py));
615
- }
616
- const drawPoints = tristrip ? glPolyStrip(pointsOut) : pointsOut;
617
- glDrawPoints(drawPoints, rgba);
618
- }
619
-
620
- /** Transform and add a polygon to the gl draw list
621
- * @param {Array<Vector2>} points - Array of Vector2 points
622
- * @param {number} rgba - Color of the polygon as a 32-bit integer
623
- * @param {number} lineWidth - Width of the outline
624
- * @param {number} x
625
- * @param {number} y
626
- * @param {number} sx
627
- * @param {number} sy
628
- * @param {number} angle
629
- * @param {boolean} [wrap] - Should the outline connect the first and last points
630
- * @memberof WebGL */
631
- function glDrawOutlineTransform(points, rgba, lineWidth, x, y, sx, sy, angle, wrap=true)
632
- {
633
- const outlinePoints = glMakeOutline(points, lineWidth, wrap);
634
- glDrawPointsTransform(outlinePoints, rgba, x, y, sx, sy, angle, false);
635
- }
636
-
637
- /** Add a list of points to the gl draw list
638
- * @param {Array<Vector2>} points - Array of Vector2 points in tri strip order
639
- * @param {number} rgba - Color as a 32-bit integer
640
- * @memberof WebGL */
641
- function glDrawPoints(points, rgba)
642
- {
643
- if (!glEnable || points.length < 3)
644
- return; // needs at least 3 points to have area
645
-
646
- // flush if there is not enough room or if different blend mode
647
- const vertCount = points.length + 2;
648
- if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
649
- glFlush();
650
- ASSERT(vertCount < gl_MAX_POLY_VERTEXES, 'poly exceeds max batch size');
651
- if (vertCount >= gl_MAX_POLY_VERTEXES) return; // release-build safety net
652
- glSetPolyMode();
653
-
654
- // setup triangle strip with degenerate verts at start and end
655
- let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
656
- for (let i = vertCount; i--;)
657
- {
658
- const j = clamp(i-1, 0, vertCount-3);
659
- const point = points[j];
660
- glPositionData[offset++] = point.x;
661
- glPositionData[offset++] = point.y;
662
- glColorData[offset++] = rgba;
663
- }
664
- glBatchCount += vertCount;
665
- }
666
-
667
- /** Add a list of colored points to the gl draw list
668
- * @param {Array<Vector2>} points - Array of Vector2 points in tri strip order
669
- * @param {Array<number>} pointColors - Array of 32-bit integer colors
670
- * @memberof WebGL */
671
- function glDrawColoredPoints(points, pointColors)
672
- {
673
- if (!glEnable || points.length < 3)
674
- return; // needs at least 3 points to have area
675
-
676
- // flush if there is not enough room or if different blend mode
677
- const vertCount = points.length + 2;
678
- if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
679
- glFlush();
680
- ASSERT(vertCount < gl_MAX_POLY_VERTEXES, 'poly exceeds max batch size');
681
- if (vertCount >= gl_MAX_POLY_VERTEXES) return; // release-build safety net
682
- glSetPolyMode();
683
-
684
- // setup triangle strip with degenerate verts at start and end
685
- let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
686
- for (let i = vertCount; i--;)
687
- {
688
- const j = clamp(i-1, 0, vertCount-3);
689
- const point = points[j];
690
- const color = pointColors[j];
691
- glPositionData[offset++] = point.x;
692
- glPositionData[offset++] = point.y;
693
- glColorData[offset++] = color;
694
- }
695
- glBatchCount += vertCount;
696
- }
697
-
698
- /** Set the WebGL render target to the given texture or back to the canvas
699
- * @param {WebGLTexture} [texture] - a texture or undefined to use normal glCanvas
700
- * @param {boolean} [clear] - should the render target be cleared
701
- * @memberof WebGL */
702
- function glSetRenderTarget(texture, clear=false)
703
- {
704
- if (texture)
705
- {
706
- glRenderTarget = texture;
707
- glContext.bindFramebuffer(glContext.FRAMEBUFFER, glFramebuffer);
708
- glContext.framebufferTexture2D(glContext.FRAMEBUFFER,
709
- glContext.COLOR_ATTACHMENT0, glContext.TEXTURE_2D, texture, 0);
710
- glPreRender(clear);
711
- }
712
- else
713
- {
714
- glFlush();
715
- glRenderTarget = undefined;
716
- glContext.bindFramebuffer(glContext.FRAMEBUFFER, null);
717
- glContext.viewport(0, 0, mainCanvasSize.x, mainCanvasSize.y);
718
- }
719
- }
720
-
721
- /** Clear out a rectangle area of the WebGL canvas or render target
722
- * @param {number} x
723
- * @param {number} y
724
- * @param {number} width
725
- * @param {number} height
726
- * @memberof WebGL */
727
- function glClearRect(x, y, width, height)
728
- {
729
- if (!glEnable) return;
730
-
731
- // Enable scissor test to clear only the specified area
732
- glContext.enable(glContext.SCISSOR_TEST);
733
- glContext.scissor(x, y, width, height);
734
- glContext.clearColor(0, 0, 0, 0);
735
- glContext.clear(glContext.COLOR_BUFFER_BIT);
736
- glContext.disable(glContext.SCISSOR_TEST);
737
- }
738
-
739
- ///////////////////////////////////////////////////////////////////////////////
740
-
741
- // WebGL internal function to convert polygon to outline triangle strip
742
- function glMakeOutline(points, width, wrap=true)
743
- {
744
- if (points.length < 2)
745
- return [];
746
-
747
- const halfWidth = width / 2;
748
- const strip = [];
749
- const n = points.length;
750
- const e = 1e-6;
751
- // miter ratio cap (dimensionless, matches SVG/Canvas2D convention)
752
- const miterLimit = 10;
753
- for (let i = 0; i < n; i++)
754
- {
755
- // for each vertex, calculate normal based on adjacent edges
756
- const prev = points[wrap ? (i - 1 + n) % n : max(i - 1, 0)];
757
- const curr = points[i];
758
- const next = points[wrap ? (i + 1) % n : min(i + 1, n - 1)];
759
-
760
- // direction from previous to current
761
- const dx1 = curr.x - prev.x;
762
- const dy1 = curr.y - prev.y;
763
- const len1 = (dx1*dx1 + dy1*dy1)**.5;
764
-
765
- // direction from current to next
766
- const dx2 = next.x - curr.x;
767
- const dy2 = next.y - curr.y;
768
- const len2 = (dx2*dx2 + dy2*dy2)**.5;
769
-
770
- if (len1 < e && len2 < e)
771
- continue; // skip degenerate point
772
-
773
- // calculate perpendicular normals for each edge
774
- const nx1 = len1 > e ? -dy1 / len1 : 0;
775
- const ny1 = len1 > e ? dx1 / len1 : 0;
776
- const nx2 = len2 > e ? -dy2 / len2 : 0;
777
- const ny2 = len2 > e ? dx2 / len2 : 0;
778
-
779
- // average the normals for miter
780
- let nx = nx1 + nx2;
781
- let ny = ny1 + ny2;
782
- const nlen = (nx*nx + ny*ny)**.5;
783
- if (nlen < e)
784
- {
785
- // 180 degree turn - use perpendicular
786
- nx = nx1;
787
- ny = ny1;
788
- }
789
- else
790
- {
791
- // calculate miter length
792
- nx /= nlen;
793
- ny /= nlen;
794
- const dot = nx1 * nx + ny1 * ny;
795
- if (dot > e)
796
- {
797
- // scale normal by miter length, clamped to miterLimit
798
- const miterLength = min(1 / dot, miterLimit);
799
- nx *= miterLength;
800
- ny *= miterLength;
801
- }
802
- }
803
-
804
- // create inner and outer points along the normal
805
- const inner = vec2(curr.x - nx * halfWidth, curr.y - ny * halfWidth);
806
- const outer = vec2(curr.x + nx * halfWidth, curr.y + ny * halfWidth);
807
- strip.push(inner);
808
- strip.push(outer);
809
- }
810
- if (strip.length > 1 && wrap)
811
- {
812
- // close the loop
813
- strip.push(strip[0]);
814
- strip.push(strip[1]);
815
- }
816
- return strip;
817
- }
818
-
819
- // WebGL internal function to convert polys to tri strips
820
- function glPolyStrip(points)
821
- {
822
- // validate input
823
- if (points.length < 3)
824
- return [];
825
-
826
- // cross product helper: (b-a) x (c-a)
827
- const cross = (a,b,c)=> (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
828
-
829
- // calculate signed area of polygon
830
- const signedArea = (poly)=>
831
- {
832
- let area = 0;
833
- for (let i = poly.length; i--;)
834
- {
835
- const j = (i+1) % poly.length;
836
- area += poly[i].cross(poly[j]);
837
- }
838
- return area;
839
- }
840
-
841
- // ensure counter-clockwise winding (slice first so we don't mutate caller's array)
842
- if (signedArea(points) < 0)
843
- points = points.slice().reverse();
844
-
845
- // check if point is inside triangle
846
- const e = 1e-9;
847
- const pointInTriangle = (p, a, b, c)=>
848
- {
849
- const c1 = cross(a, b, p);
850
- const c2 = cross(b, c, p);
851
- const c3 = cross(c, a, p);
852
- const negative = (c1<-e?1:0) + (c2<-e?1:0) + (c3<-e?1:0);
853
- const positive = (c1> e?1:0) + (c2> e?1:0) + (c3> e?1:0);
854
- return !(negative && positive);
855
- };
856
-
857
- // ear clipping triangulation
858
- const indices = [];
859
- for (let i = 0; i < points.length; ++i)
860
- indices[i] = i;
861
- const triangles = [];
862
- let attempts = 0;
863
- const maxAttempts = points.length ** 2 + 100;
864
- while (indices.length > 3 && attempts++ < maxAttempts)
865
- {
866
- let foundEar = false;
867
- for (let i = 0; i < indices.length; i++)
868
- {
869
- const i0 = indices[(i + indices.length - 1) % indices.length];
870
- const i1 = indices[i];
871
- const i2 = indices[(i + 1) % indices.length];
872
- const a = points[i0], b = points[i1], c = points[i2];
873
-
874
- // check if convex
875
- if (cross(a, b, c) < e) continue;
876
-
877
- // check if any other point is inside
878
- let hasInside = false;
879
- for (let j = 0; j < indices.length; j++)
880
- {
881
- const k = indices[j];
882
- if (k === i0 || k === i1 || k === i2) continue;
883
-
884
- const p = points[k];
885
- hasInside = pointInTriangle(p, a, b, c);
886
- if (hasInside) break;
887
- }
888
- if (hasInside) continue;
889
-
890
- // found valid ear
891
- triangles.push([i0, i1, i2]);
892
- indices.splice(i, 1);
893
- foundEar = true;
894
- break;
895
- }
896
-
897
- // fallback for degenerate cases
898
- if (!foundEar)
899
- {
900
- let worstIndex = -1, worstValue = Infinity;
901
- for (let i = 0; i < indices.length; i++)
902
- {
903
- const i0 = indices[(i + indices.length - 1) % indices.length];
904
- const i1 = indices[i];
905
- const i2 = indices[(i + 1) % indices.length];
906
- const value = abs(cross(points[i0], points[i1], points[i2]));
907
- if (value < worstValue)
908
- {
909
- worstValue = value;
910
- worstIndex = i;
911
- }
912
- }
913
- if (worstIndex < 0) break;
914
-
915
- const i0 = indices[(worstIndex + indices.length - 1) % indices.length];
916
- const i1 = indices[worstIndex];
917
- const i2 = indices[(worstIndex + 1) % indices.length];
918
- triangles.push([i0, i1, i2]);
919
- indices.splice(worstIndex, 1);
920
- }
921
- }
922
-
923
- // add final triangle
924
- if (indices.length === 3)
925
- triangles.push([indices[0], indices[1], indices[2]]);
926
- if (!triangles.length)
927
- return [];
928
-
929
- // convert triangles to triangle strip with degenerate connectors
930
- const strip = [];
931
- let [a0, b0, c0] = triangles[0];
932
- strip.push(points[a0], points[b0], points[c0]);
933
- for (let i = 1; i < triangles.length; i++)
934
- {
935
- // add degenerate bridge from last vertex to first of new triangle
936
- const [a, b, c] = triangles[i];
937
- strip.push(points[c0], points[a]);
938
- strip.push(points[a], points[b], points[c]);
939
- c0 = c;
940
- }
941
- return strip;
1
+ /**
2
+ * LittleJS WebGL Interface
3
+ * - WebGL2 rendering engine for high-performance graphics
4
+ * - Batched sprite rendering for drawing thousands of sprites efficiently
5
+ * - Instanced rendering using vertex array objects (VAOs)
6
+ * - Polygon rendering with triangle strip support
7
+ * - Shader system with custom vertex and fragment shaders
8
+ * - Texture management with automatic atlas support
9
+ * - Post-processing effects via framebuffer and shader plugins
10
+ * - Automatic fallback to Canvas2D if WebGL is unavailable
11
+ * - Context loss and restoration handling
12
+ * - Can be disabled with glEnable setting
13
+ * - Advanced users can create custom shaders and render targets
14
+ * @namespace WebGL
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ /** The WebGL canvas which appears below the main canvas
20
+ * @type {HTMLCanvasElement}
21
+ * @memberof WebGL */
22
+ let glCanvas;
23
+
24
+ /** WebGL2 context for `glCanvas`
25
+ * @type {WebGL2RenderingContext}
26
+ * @memberof WebGL */
27
+ let glContext;
28
+
29
+ /** Should WebGL be setup with anti-aliasing? must be set before calling engineInit
30
+ * @type {boolean}
31
+ * @memberof WebGL */
32
+ let glAntialias = true;
33
+
34
+ // WebGL internal variables not exposed to documentation
35
+ let glMipmappedTextures = new WeakSet, glMipmapsUntilTarget = new WeakSet, glMipmapsStale = new Set, glPremultipliedTextures = new WeakSet, glShaderPremultiplied, glEnableBeforeLoss = true, glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos = new Set, glInstancedVAO, glPolyVAO, glFramebuffer, glRenderTarget, glShaderObjects = [], glCustomShader, glBatchShader, glProgramCustom, glTransform, glRenderTargetSaved, glUniformLocations = new WeakMap, glCanBeEnabled = true;
36
+ // ANDed onto every packed color as a draw is queued; the light system's shadow pass sets 0xff000000
37
+ // to draw everything black with its alpha kept (rgbaInt packs alpha in the top byte)
38
+ let glColorMask = -1;
39
+ // ORed onto the additive color of every quad and onto every poly point's color as a draw is queued; the light system's
40
+ // emissive pass sets a grey with the mask at 0xff000000, so a draw comes out that grey in its own shape
41
+ let glColorAdditive = 0;
42
+ // a texture drawn into with the canvas's own transform and its size, the light system's lightmap while its pass
43
+ // runs: a target set and ended inside it, like a tile layer redrawn in a renderLight, goes back to it, not the canvas
44
+ let glRenderTargetBase;
45
+ // set by the light system's shadow pass, which draws the world with its own camera, so a screen space
46
+ // WebGL draw in a render() is skipped instead of landing somewhere in the world
47
+ let glSkipScreenSpace = false;
48
+
49
+ // WebGL internal constants
50
+ const gl_ARRAY_BUFFER_SIZE = 5e5;
51
+ const gl_INDICES_PER_INSTANCE = 11;
52
+ const gl_INSTANCE_BYTE_STRIDE = gl_INDICES_PER_INSTANCE * 4;
53
+ const gl_MAX_INSTANCES = gl_ARRAY_BUFFER_SIZE / gl_INSTANCE_BYTE_STRIDE | 0;
54
+ const gl_INDICES_PER_POLY_VERTEX = 3;
55
+ const gl_POLY_VERTEX_BYTE_STRIDE = gl_INDICES_PER_POLY_VERTEX * 4;
56
+ const gl_MAX_POLY_VERTEXES = gl_ARRAY_BUFFER_SIZE / gl_POLY_VERTEX_BYTE_STRIDE | 0;
57
+
58
+ ///////////////////////////////////////////////////////////////////////////////
59
+
60
+ // the sprite vertex shader, shared by the engine's program and every Shader so one vertex layout fits all
61
+ const gl_VERTEX_SOURCE =
62
+ '#version 300 es\n' + // specify GLSL ES version
63
+ 'precision highp float;'+ // use highp for accuracy
64
+ 'uniform mat4 m;'+ // transform matrix
65
+ 'layout(location=0) in vec2 g;'+ // in: geometry
66
+ 'layout(location=1) in vec4 p;'+ // in: position/size
67
+ 'layout(location=2) in vec4 u;'+ // in: uvs
68
+ 'layout(location=3) in vec4 c;'+ // in: color
69
+ 'layout(location=4) in vec4 a;'+ // in: additiveColor
70
+ 'layout(location=5) in float r;'+// in: rotation
71
+ 'out vec2 v,l;'+ // out: uv, and 0 to 1 across the sprite for a Shader's localUV
72
+ 'out vec4 d,e;'+ // out: color, additiveColor
73
+ 'void main(){'+ // shader entry point
74
+ 'vec2 s=(g-.5)*p.zw;'+ // get size offset
75
+ 'gl_Position=m*vec4(p.xy+s*cos(r)-vec2(-s.y,s)*sin(r),1,1);'+ // transform position
76
+ 'v=mix(u.xw,u.zy,g);'+ // pass uv to fragment shader
77
+ 'l=g;d=c;e=a;'+ // pass local uv and colors to fragment shader
78
+ '}'; // end of shader
79
+
80
+ // the end of every sprite fragment shader, the engine's and each Shader's: t is the surface color
81
+ // a render target and a smooth image hold premultiplied color, so their batches blend with ONE and premultiply here,
82
+ // giving what a straight texel gives, clamped as the blend clamps it: exactly when the additive alpha is 0 or
83
+ // the texel is clear or opaque, and close otherwise, since the straight color is not kept to be multiplied
84
+ const gl_FRAGMENT_TINT_SOURCE =
85
+ 'c=t*d+e;'+ // modulate by color plus additive
86
+ 'if(premultipliedTexture){'+ // a render target or a smooth image
87
+ 'c.a=min(c.a,1.);'+ // the alpha the blend uses
88
+ 'c.rgb=min(t.rgb*d.rgb*min(d.a+e.a,1.)+e.rgb*c.a,c.a);}';
89
+
90
+ // Initialize WebGL, called automatically by the engine
91
+ function glInit(rootElement)
92
+ {
93
+ if (!glEnable || headlessMode)
94
+ {
95
+ glCanBeEnabled = false;
96
+ return;
97
+ }
98
+
99
+ // create the canvas and textures
100
+ glCanvas = document.createElement('canvas');
101
+ glContext = glCanvas.getContext('webgl2', {antialias:glAntialias});
102
+
103
+ if (!glContext)
104
+ {
105
+ console.warn('WebGL2 not supported, falling back to 2D canvas rendering!');
106
+ glCanvas = glContext = undefined;
107
+ glEnable = false;
108
+ glCanBeEnabled = false;
109
+ return;
110
+ }
111
+
112
+ // attach the WebGL canvas;
113
+ rootElement.appendChild(glCanvas);
114
+
115
+ // startup webgl, and make the textures of any texture infos made before it
116
+ initWebGL();
117
+ for (const info of glTextureInfos)
118
+ info.glTexture ||= glCreateTexture(info.image, info.wrap);
119
+
120
+ // setup context lost and restore handlers
121
+ glCanvas.addEventListener('webglcontextlost', (e)=>
122
+ {
123
+ glEnableBeforeLoss = glEnable;
124
+ glEnable = false; // disable WebGL rendering
125
+ glCanvas.style.display = 'none'; // hide the gl canvas
126
+ e.preventDefault(); // prevent default to allow restoration
127
+ LOG('WebGL context lost! Switching to Canvas2d rendering.');
128
+
129
+ // remove WebGL textures
130
+ for (const info of glTextureInfos)
131
+ info.glTexture = undefined;
132
+ glActiveTexture = undefined;
133
+ // every Shader compiles again on its next draw, and the first flush after restore picks its program again
134
+ for (const shader of glShaderObjects)
135
+ shader.program = undefined;
136
+ glBatchShader = undefined;
137
+ glProgramCustom = true;
138
+ glUniformLocations = new WeakMap; // the programs those belonged to are gone
139
+ // drop any partially-filled batch so the next glFlush doesn't
140
+ // upload stale glBatchCount against fresh empty buffers on restore
141
+ glBatchCount = 0;
142
+ glPolyMode = false;
143
+ pluginList.forEach(plugin=>plugin.glContextLost?.());
144
+ });
145
+ glCanvas.addEventListener('webglcontextrestored', ()=>
146
+ {
147
+ glEnable = glEnableBeforeLoss; // WebGL rendering as it was before the loss
148
+ glCanvas.style.display = ''; // show the gl canvas
149
+ LOG('WebGL context restored, reinitializing...');
150
+
151
+ // reinit WebGL and restore textures
152
+ glMipmappedTextures = new WeakSet;
153
+ glMipmapsUntilTarget = new WeakSet;
154
+ glMipmapsStale.clear();
155
+ glPremultipliedTextures = new WeakSet; // the tile layers draw into their new textures again below
156
+ initWebGL();
157
+ for (const info of glTextureInfos)
158
+ info.glTexture = glCreateTexture(info.image, info.wrap);
159
+ pluginList.forEach(plugin=>plugin.glContextRestored?.());
160
+
161
+ // a tile layer drawn on the GPU only had its tiles in the lost texture, it draws them again
162
+ for (const o of engineObjects)
163
+ o instanceof TileLayer && o.isUsingWebGL && !o.destroyed && o.redraw();
164
+ });
165
+
166
+ function initWebGL()
167
+ {
168
+ // setup instanced rendering shader program
169
+ glShader = glCreateProgram(gl_VERTEX_SOURCE,
170
+ '#version 300 es\n' + // specify GLSL ES version
171
+ 'precision highp float;'+ // use highp for accuracy
172
+ 'uniform sampler2D s;'+ // texture
173
+ 'uniform bool premultipliedTexture;'+ // is the texture premultiplied, a render target or a smooth image
174
+ 'in vec2 v;'+ // in: uv
175
+ 'in vec4 d,e;'+ // in: color, additiveColor
176
+ 'out vec4 c;'+ // out: color
177
+ 'void main(){'+ // shader entry point
178
+ 'vec4 t=texture(s,v);'+ // sample the texture
179
+ gl_FRAGMENT_TINT_SOURCE + // apply color and additive
180
+ '}' // end of shader
181
+ );
182
+ glShaderPremultiplied = false; // a new program starts with its uniforms at 0
183
+
184
+ // setup poly rendering shaders
185
+ glPolyShader = glCreateProgram(
186
+ '#version 300 es\n' + // specify GLSL ES version
187
+ 'precision highp float;'+ // use highp for better accuracy
188
+ 'uniform mat4 m;'+ // transform matrix
189
+ 'in vec2 p;'+ // in: position
190
+ 'in vec4 c;'+ // in: color
191
+ 'out vec4 d;'+ // out: color
192
+ 'void main(){'+ // shader entry point
193
+ 'gl_Position=m*vec4(p,1,1);'+ // transform position
194
+ 'd=c;'+ // pass color to fragment shader
195
+ '}' // end of shader
196
+ ,
197
+ '#version 300 es\n' + // specify GLSL ES version
198
+ 'precision highp float;'+ // use highp for better accuracy
199
+ 'in vec4 d;'+ // in: color
200
+ 'out vec4 c;'+ // out: color
201
+ 'void main(){'+ // shader entry point
202
+ 'c=d;'+ // set color
203
+ '}' // end of shader
204
+ );
205
+
206
+ // init buffers
207
+ const glInstanceData = new ArrayBuffer(gl_ARRAY_BUFFER_SIZE);
208
+ glPositionData = new Float32Array(glInstanceData);
209
+ glColorData = new Uint32Array(glInstanceData);
210
+ glArrayBuffer = glContext.createBuffer();
211
+ glGeometryBuffer = glContext.createBuffer();
212
+ glFramebuffer = glContext.createFramebuffer();
213
+ glBatchCount = 0;
214
+
215
+ // create the geometry buffer, triangle strip square
216
+ const geometry = new Float32Array([0,0,1,0,0,1,1,1]);
217
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
218
+ glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
219
+
220
+ let offset, shader, stride;
221
+ const initVertexAttrib = (name, type, typeSize, size, divisor=0)=>
222
+ {
223
+ const location = glContext.getAttribLocation(shader, name);
224
+ const normalize = typeSize === 1;
225
+ const fixedStride = typeSize && stride;
226
+ glContext.enableVertexAttribArray(location);
227
+ glContext.vertexAttribPointer(location, size, type, normalize, fixedStride, offset);
228
+ glContext.vertexAttribDivisor(location, divisor);
229
+ offset += size*typeSize;
230
+ }
231
+
232
+ // setup VAO for instanced rendering
233
+ glInstancedVAO = glContext.createVertexArray();
234
+ glContext.bindVertexArray(glInstancedVAO);
235
+
236
+ // configure instanced vertex attributes
237
+ offset = 0, shader = glShader, stride = gl_INSTANCE_BYTE_STRIDE;
238
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
239
+ // geometry, its own packed buffer: typeSize 0 gives stride 0 and does not advance the offset
240
+ initVertexAttrib('g', glContext.FLOAT, 0, 2);
241
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
242
+ glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
243
+ initVertexAttrib('p', glContext.FLOAT, 4, 4, 1); // position & size
244
+ initVertexAttrib('u', glContext.FLOAT, 4, 4, 1); // texture coords
245
+ initVertexAttrib('c', glContext.UNSIGNED_BYTE, 1, 4, 1); // color
246
+ initVertexAttrib('a', glContext.UNSIGNED_BYTE, 1, 4, 1); // additiveColor
247
+ initVertexAttrib('r', glContext.FLOAT, 4, 1, 1); // rotation
248
+
249
+ // setup VAO for poly rendering
250
+ glPolyVAO = glContext.createVertexArray();
251
+ glContext.bindVertexArray(glPolyVAO);
252
+
253
+ // configure poly vertex attributes
254
+ offset = 0, shader = glPolyShader, stride = gl_POLY_VERTEX_BYTE_STRIDE;
255
+ initVertexAttrib('p', glContext.FLOAT, 4, 2); // position
256
+ initVertexAttrib('c', glContext.UNSIGNED_BYTE, 1, 4); // color
257
+ }
258
+ }
259
+
260
+ function glSetInstancedMode(force=false)
261
+ {
262
+ if (!force && !glPolyMode) return;
263
+
264
+ // setup instanced mode
265
+ glFlush();
266
+ glPolyMode = false;
267
+ glContext.useProgram(glShader);
268
+ glContext.bindVertexArray(glInstancedVAO);
269
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer); // a VAO does not keep this binding
270
+ }
271
+
272
+ function glSetPolyMode()
273
+ {
274
+ if (glPolyMode) return;
275
+
276
+ // setup poly mode
277
+ glFlush();
278
+ glPolyMode = true;
279
+ glContext.useProgram(glPolyShader);
280
+ glContext.bindVertexArray(glPolyVAO);
281
+ }
282
+
283
+ // Setup WebGL render each frame, called automatically by engine
284
+ // Also used by tile layer rendering when redrawing tiles
285
+ function glPreRender(clear=true)
286
+ {
287
+ if (!glEnable || !glContext) return;
288
+
289
+ ASSERT(!glBatchCount, 'glPreRender called with unflushed batch.');
290
+
291
+ // mainCanvasSize is css pixels, the backing store is scaled by the pixel
292
+ // ratio, render targets are offscreen so they are never scaled
293
+ const dpr = glRenderTarget ? 1 : getCanvasPixelRatio();
294
+ const bufferSizeX = mainCanvasSize.x * dpr | 0;
295
+ const bufferSizeY = mainCanvasSize.y * dpr | 0;
296
+ if (!glRenderTarget)
297
+ {
298
+ // set to same size as main canvas, only when it changes because
299
+ // setting it reallocates the drawing buffer and invalidates the frame
300
+ if (glCanvas.width !== bufferSizeX || glCanvas.height !== bufferSizeY)
301
+ {
302
+ glCanvas.width = bufferSizeX;
303
+ glCanvas.height = bufferSizeY;
304
+ }
305
+ }
306
+ glContext.viewport(0, 0, bufferSizeX, bufferSizeY);
307
+ clear && glClearCanvas();
308
+
309
+ // build the transform matrix
310
+ const s = vec2(2*cameraScale).divide(mainCanvasSize);
311
+ if (glRenderTarget)
312
+ s.y = -s.y; // invert y when using render target
313
+ const rotatedCam = cameraPos.rotate(-cameraAngle);
314
+ const p = vec2(-1).subtract(rotatedCam.multiply(s));
315
+ const ca = cos(cameraAngle);
316
+ const sa = sin(cameraAngle);
317
+ // column major; the shaders send (x, y, 1, 1), so the third column adds 1 to x and y and p takes it back off:
318
+ // clip = s * (world turned by -cameraAngle - the camera turned the same way), z = 1
319
+ const transform = [
320
+ s.x * ca, s.y * sa, 0, 0,
321
+ -s.x * sa, s.y * ca, 0, 0,
322
+ 1, 1, 1, 0,
323
+ p.x, p.y, 0, 1];
324
+ glTransform = transform;
325
+
326
+ // set the same transform matrix for both shaders
327
+ const initUniform = (program, uniform, value)=>
328
+ {
329
+ glContext.useProgram(program);
330
+ glContext.uniformMatrix4fv(glUniformLocation(program, uniform), false, value);
331
+ }
332
+ initUniform(glPolyShader, 'm', transform);
333
+ initUniform(glShader, 'm', transform);
334
+
335
+ // set the active texture
336
+ glContext.activeTexture(glContext.TEXTURE0);
337
+ if (textureInfos[0])
338
+ {
339
+ glActiveTexture = textureInfos[0].glTexture;
340
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
341
+ }
342
+
343
+ // rebind the array buffer
344
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
345
+
346
+ // start with additive blending off
347
+ glAdditive = glBatchAdditive = false;
348
+
349
+ // force it to set instanced mode
350
+ glSetInstancedMode(true);
351
+ }
352
+
353
+ /** Clear the canvas or render target to canvasClearColor
354
+ * @memberof WebGL */
355
+ function glClearCanvas()
356
+ {
357
+ if (!glContext) return;
358
+
359
+ // clear using the canvasClearColor, premultiplied like everything the blend writes
360
+ const color = canvasClearColor;
361
+ glContext.clearColor(color.r*color.a, color.g*color.a, color.b*color.a, color.a);
362
+ glContext.clear(glContext.COLOR_BUFFER_BIT);
363
+ }
364
+
365
+ /** Set the WebGL texture, called automatically if using multiple textures
366
+ * - This may also flush the gl buffer resulting in more draw calls and worse performance
367
+ * @param {WebGLTexture} texture
368
+ * @memberof WebGL */
369
+ function glSetTexture(texture)
370
+ {
371
+ if (!glContext) return;
372
+ if (texture !== glActiveTexture)
373
+ {
374
+ // must flush cache with the old texture to set a new one
375
+ glFlush();
376
+ glActiveTexture = texture;
377
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
378
+ }
379
+ glMipmapsStale.size && glUpdateMipmaps(texture); // bound already or not, drawn into since
380
+ }
381
+
382
+ // make the smaller levels of a texture again if it was drawn into since, it must be the bound texture
383
+ function glUpdateMipmaps(texture)
384
+ {
385
+ if (!glMipmapsStale.has(texture)) return;
386
+ glMipmapsStale.delete(texture);
387
+ glContext.generateMipmap(glContext.TEXTURE_2D);
388
+ }
389
+
390
+ // a texture's wrap as its two WebGL modes, across and down: true repeats both and false clamps both, or a pair of
391
+ // modes as a glTF sampler gives them; the pairs are WebGL's fixed values, shared so a draw makes no array
392
+ const glWrapRepeat = Object.freeze([10497, 10497]), glWrapClamp = Object.freeze([33071, 33071]);
393
+ function glWrapModes(wrap)
394
+ { return isArray(wrap) ? wrap : wrap ? glWrapRepeat : glWrapClamp; }
395
+
396
+ /** Set the wrap mode on an existing WebGL texture
397
+ * Flushes the current batch only if the texture is the active one
398
+ * @param {WebGLTexture} texture
399
+ * @param {boolean|Array<number>} [wrap] - true for REPEAT, false for CLAMP_TO_EDGE, or the WebGL modes across and
400
+ * down, like [gl.CLAMP_TO_EDGE, gl.MIRRORED_REPEAT]
401
+ * @memberof WebGL */
402
+ function glSetTextureWrap(texture, wrap=true)
403
+ {
404
+ if (!glContext || !texture) return;
405
+
406
+ // flush only if changing wrap on the currently bound texture
407
+ const isCurrent = texture === glActiveTexture;
408
+ if (isCurrent)
409
+ glFlush();
410
+ else
411
+ glContext.bindTexture(glContext.TEXTURE_2D, texture);
412
+
413
+ const [wrapS, wrapT] = glWrapModes(wrap);
414
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_S, wrapS);
415
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_T, wrapT);
416
+
417
+ if (!isCurrent && glActiveTexture)
418
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
419
+ }
420
+
421
+ /** Compile WebGL shader of the given type, will throw errors if in debug mode
422
+ * @param {string} source
423
+ * @param {number} type
424
+ * @return {WebGLShader}
425
+ * @memberof WebGL */
426
+ function glCompileShader(source, type)
427
+ {
428
+ if (!glContext) return;
429
+
430
+ // build the shader
431
+ const shader = glContext.createShader(type);
432
+ glContext.shaderSource(shader, source);
433
+ glContext.compileShader(shader);
434
+
435
+ // check for errors
436
+ if (debug && !glContext.getShaderParameter(shader, glContext.COMPILE_STATUS))
437
+ throw glContext.getShaderInfoLog(shader);
438
+ return shader;
439
+ }
440
+
441
+ /** Create WebGL program with given shaders
442
+ * @param {string} vsSource
443
+ * @param {string} fsSource
444
+ * @return {WebGLProgram}
445
+ * @memberof WebGL */
446
+ function glCreateProgram(vsSource, fsSource)
447
+ {
448
+ if (!glContext) return;
449
+
450
+ // build the program
451
+ const program = glContext.createProgram();
452
+ glContext.attachShader(program, glCompileShader(vsSource, glContext.VERTEX_SHADER));
453
+ glContext.attachShader(program, glCompileShader(fsSource, glContext.FRAGMENT_SHADER));
454
+ glContext.linkProgram(program);
455
+
456
+ // check for errors
457
+ if (debug && !glContext.getProgramParameter(program, glContext.LINK_STATUS))
458
+ throw glContext.getProgramInfoLog(program);
459
+ return program;
460
+ }
461
+
462
+ // a uniform location, looked up once per program
463
+ function glUniformLocation(program, name)
464
+ {
465
+ let cache = glUniformLocations.get(program);
466
+ cache || glUniformLocations.set(program, cache = {});
467
+ return cache[name] ??= glContext.getUniformLocation(program, name);
468
+ }
469
+
470
+ // a Shader's 2D program, compiled the first time a batch needs it: the snippet's mainImage gives the surface
471
+ // color, then the sprite's color and additive color apply as the engine's own fragment shader does
472
+ function glShaderProgram(shader)
473
+ {
474
+ return shader.program ||= glCreateProgram(gl_VERTEX_SOURCE,
475
+ '#version 300 es\n' +
476
+ 'precision highp float;' +
477
+ 'uniform sampler2D iChannel0;' + // the texture
478
+ 'uniform vec3 iResolution;' + // canvas size in pixels
479
+ 'uniform float iTime;' + // engine time
480
+ 'uniform bool premultipliedTexture;' + // is the texture premultiplied, a render target or a smooth image
481
+ 'in vec2 v,l;in vec4 d,e;out vec4 c;\n' + // a define needs its own line
482
+ '#define localUV l\n' +
483
+ shader.fragmentCode + '\n' +
484
+ 'void main(){vec4 t;mainImage(t,v);' + gl_FRAGMENT_TINT_SOURCE + '}');
485
+ }
486
+
487
+ /** Create WebGL texture from an image and init the texture settings
488
+ * Restores the active texture when done
489
+ * @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap} [image]
490
+ * @param {boolean|Array<number>} [wrap] - true for REPEAT, false for CLAMP_TO_EDGE, or the WebGL modes across and down
491
+ * @return {WebGLTexture}
492
+ * @memberof WebGL */
493
+ function glCreateTexture(image, wrap=false)
494
+ {
495
+ if (!glContext) return;
496
+
497
+ // build the texture
498
+ const texture = glContext.createTexture();
499
+ let mipMap = false;
500
+ if (image?.width)
501
+ {
502
+ glSetTextureData(texture, image);
503
+ glContext.bindTexture(glContext.TEXTURE_2D, texture);
504
+ // WebGL2 makes mipmaps at any size, a texture that becomes a render target keeps them only at powers of two
505
+ mipMap = !tilesPixelated;
506
+ if (mipMap && !(isPowerOfTwo(image.width) && isPowerOfTwo(image.height)))
507
+ glMipmapsUntilTarget.add(texture);
508
+ }
509
+ else
510
+ {
511
+ // create a white texture
512
+ const whitePixel = new Uint8Array([255, 255, 255, 255]);
513
+ glContext.bindTexture(glContext.TEXTURE_2D, texture);
514
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, 1, 1, 0, glContext.RGBA, glContext.UNSIGNED_BYTE, whitePixel);
515
+ }
516
+
517
+ // set texture filtering
518
+ const magFilter = tilesPixelated ? glContext.NEAREST : glContext.LINEAR;
519
+ const minFilter = mipMap ? glContext.LINEAR_MIPMAP_LINEAR : magFilter;
520
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MAG_FILTER, magFilter);
521
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, minFilter);
522
+ const [wrapS, wrapT] = glWrapModes(wrap);
523
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_S, wrapS);
524
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_T, wrapT);
525
+ if (mipMap)
526
+ {
527
+ glContext.generateMipmap(glContext.TEXTURE_2D);
528
+ glMipmappedTextures.add(texture);
529
+ }
530
+
531
+ // rebind active texture
532
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
533
+ return texture;
534
+ }
535
+
536
+ /** Deletes a WebGL texture
537
+ * @param {WebGLTexture} [texture]
538
+ * @memberof WebGL */
539
+ function glDeleteTexture(texture)
540
+ {
541
+ if (!glContext) return;
542
+
543
+ glContext.deleteTexture(texture);
544
+ }
545
+
546
+ /** Set WebGL texture data from an image, restores the active texture when done
547
+ * @param {WebGLTexture} texture
548
+ * @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap} image
549
+ * @memberof WebGL */
550
+ function glSetTextureData(texture, image)
551
+ {
552
+ if (!glContext) return;
553
+
554
+ // build the texture, after drawing what was queued with the old image
555
+ ASSERT(image?.width > 0, 'Invalid image data.');
556
+ texture === glActiveTexture && glFlush();
557
+ glContext.bindTexture(glContext.TEXTURE_2D, texture);
558
+ if (typeof ImageBitmap != 'undefined' && image instanceof ImageBitmap)
559
+ {
560
+ // WebGL ignores the unpack flag below for a bitmap and uploads it as it was made, so it is drawn onto a
561
+ // canvas first, which reads either kind of bitmap right and uploads as the flag says
562
+ const canvas = new OffscreenCanvas(image.width, image.height);
563
+ canvas.getContext('2d').drawImage(image, 0, 0);
564
+ image = canvas;
565
+ }
566
+ // smooth filtering mixes a texel with its see through neighbors, right only for premultiplied color, or the
567
+ // edges go dark; pixel art is sampled a texel at a time, and uploads straight color as it always has
568
+ const premultiply = !tilesPixelated;
569
+ glContext.pixelStorei(glContext.UNPACK_PREMULTIPLY_ALPHA_WEBGL, premultiply);
570
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
571
+ glContext.pixelStorei(glContext.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
572
+ if (premultiply)
573
+ glPremultipliedTextures.add(texture); // drawn with the premultiplied blend, as a render target is
574
+ else
575
+ glPremultipliedTextures.delete(texture); // an image uploads straight color, even into a used render target
576
+
577
+ // keep mipmaps in sync with new level 0 data, for any texture that has them
578
+ if (glMipmappedTextures.has(texture))
579
+ glContext.generateMipmap(glContext.TEXTURE_2D);
580
+
581
+ // rebind active texture
582
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
583
+ }
584
+
585
+ /** Internal: tells WebGL to create or update the glTexture and start tracking it, TextureInfo calls it
586
+ * @param {TextureInfo} textureInfo
587
+ * @ignore */
588
+ function glRegisterTextureInfo(textureInfo)
589
+ {
590
+ if (headlessMode) return;
591
+
592
+ // add texture info to tracking list even if gl is not enabled
593
+ glTextureInfos.add(textureInfo);
594
+
595
+ if (!glContext) return;
596
+
597
+ // create or set the texture data
598
+ if (textureInfo.glTexture)
599
+ glSetTextureData(textureInfo.glTexture, textureInfo.image);
600
+ else
601
+ textureInfo.glTexture = glCreateTexture(textureInfo.image, textureInfo.wrap);
602
+ }
603
+
604
+ /** Internal: tells WebGL to destroy the glTexture and stop tracking it, TextureInfo calls it
605
+ * @param {TextureInfo} textureInfo
606
+ * @ignore */
607
+ function glUnregisterTextureInfo(textureInfo)
608
+ {
609
+ if (headlessMode) return;
610
+
611
+ // delete texture info from tracking list even if gl is not enabled
612
+ glTextureInfos.delete(textureInfo);
613
+
614
+ // unset and destroy the texture, drawing what is batched with it first, since deleting unbinds it
615
+ const glTexture = textureInfo.glTexture;
616
+ textureInfo.glTexture = undefined;
617
+ if (glTexture && glTexture === glActiveTexture)
618
+ {
619
+ glFlush();
620
+ glActiveTexture = undefined; // so nothing binds the deleted texture again
621
+ }
622
+ glMipmapsStale.delete(glTexture);
623
+ glPremultipliedTextures.delete(glTexture);
624
+ glDeleteTexture(glTexture);
625
+ }
626
+
627
+ /** Draw all sprites and clear out the buffer, called automatically by the system whenever necessary
628
+ * @memberof WebGL */
629
+ function glFlush()
630
+ {
631
+ if (glEnable && glContext && glBatchCount)
632
+ {
633
+ // a render target holds premultiplied color, a batch drawing one takes its color as it is
634
+ // glSetTexture flushes on every change, so the whole batch drew with the bound texture
635
+ const premultiplied = !glPolyMode && glPremultipliedTextures.has(glActiveTexture);
636
+
637
+ // set blend mode
638
+ const sourceBlend = premultiplied ? glContext.ONE : glContext.SRC_ALPHA;
639
+ const destBlend = glBatchAdditive ? glContext.ONE : glContext.ONE_MINUS_SRC_ALPHA;
640
+ glContext.blendFuncSeparate(sourceBlend, destBlend, glContext.ONE, destBlend);
641
+ glContext.enable(glContext.BLEND);
642
+
643
+ // a Shader's program for this batch, or the engine's own again after one
644
+ if (!glPolyMode && (glBatchShader || glProgramCustom))
645
+ {
646
+ const program = glBatchShader ? glShaderProgram(glBatchShader) : glShader;
647
+ glContext.useProgram(program);
648
+ glProgramCustom = !!glBatchShader;
649
+ if (glBatchShader)
650
+ {
651
+ const uniform = (name)=> glUniformLocation(program, name);
652
+ glContext.uniformMatrix4fv(uniform('m'), false, glTransform);
653
+ glContext.uniform1f(uniform('iTime'), time);
654
+ // a render target is the size glPreRender gave its viewport
655
+ const width = glRenderTarget ? mainCanvasSize.x : glCanvas.width;
656
+ const height = glRenderTarget ? mainCanvasSize.y : glCanvas.height;
657
+ glContext.uniform3f(uniform('iResolution'), width, height, 1);
658
+ glContext.uniform1i(uniform('premultipliedTexture'), +premultiplied);
659
+ }
660
+ }
661
+ if (!glPolyMode && !glBatchShader && glShaderPremultiplied !== premultiplied)
662
+ {
663
+ // the engine's program is the one in use, and it keeps the flag until it changes
664
+ glContext.uniform1i(glUniformLocation(glShader, 'premultipliedTexture'), +premultiplied);
665
+ glShaderPremultiplied = premultiplied;
666
+ }
667
+
668
+ const byteLength = glBatchCount *
669
+ (glPolyMode ? gl_INDICES_PER_POLY_VERTEX : gl_INDICES_PER_INSTANCE);
670
+ glContext.bufferSubData(glContext.ARRAY_BUFFER, 0, glPositionData, 0, byteLength);
671
+
672
+ // draw the batch
673
+ if (glPolyMode)
674
+ glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, glBatchCount);
675
+ else
676
+ glContext.drawArraysInstanced(glContext.TRIANGLE_STRIP, 0, 4, glBatchCount);
677
+ ++drawCount;
678
+ primitiveCount += glBatchCount;
679
+ glBatchCount = 0;
680
+ }
681
+ glBatchAdditive = glAdditive;
682
+ glBatchShader = glCustomShader;
683
+ }
684
+
685
+ /** Flush any sprites still in the buffer and copy the WebGL canvas to a 2D context
686
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
687
+ * @memberof WebGL */
688
+ function glCopyToContext(context)
689
+ {
690
+ if (!glEnable || !glContext) return;
691
+
692
+ glFlush();
693
+ context.drawImage(glCanvas, 0, 0);
694
+ }
695
+
696
+ /** Set anti-aliasing for WebGL canvas
697
+ * Must be called before engineInit
698
+ * @param {boolean} [antialias]
699
+ * @memberof WebGL */
700
+ function glSetAntialias(antialias=true)
701
+ {
702
+ ASSERT(!glCanvas, 'must be called before engineInit');
703
+ glAntialias = antialias;
704
+ }
705
+
706
+ /** Add a sprite to the gl draw list, used by all gl draw functions
707
+ * @param {number} x
708
+ * @param {number} y
709
+ * @param {number} sizeX
710
+ * @param {number} sizeY
711
+ * @param {number} [angle]
712
+ * @param {number} [uv0X]
713
+ * @param {number} [uv0Y]
714
+ * @param {number} [uv1X]
715
+ * @param {number} [uv1Y]
716
+ * @param {number} [rgba=-1] - white is -1
717
+ * @param {number} [rgbaAdditive=0] - black is 0
718
+ * @memberof WebGL */
719
+ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgba=-1, rgbaAdditive=0)
720
+ {
721
+ // flush if there is not enough room or if different blend mode
722
+ if (glBatchCount >= gl_MAX_INSTANCES || glBatchAdditive !== glAdditive || glBatchShader !== glCustomShader)
723
+ glFlush();
724
+ glSetInstancedMode();
725
+
726
+ let offset = glBatchCount++ * gl_INDICES_PER_INSTANCE;
727
+ glPositionData[offset++] = x;
728
+ glPositionData[offset++] = y;
729
+ glPositionData[offset++] = sizeX;
730
+ glPositionData[offset++] = sizeY;
731
+ glPositionData[offset++] = uv0X;
732
+ glPositionData[offset++] = uv0Y;
733
+ glPositionData[offset++] = uv1X;
734
+ glPositionData[offset++] = uv1Y;
735
+ glColorData[offset++] = rgba & glColorMask;
736
+ glColorData[offset++] = rgbaAdditive & glColorMask | glColorAdditive;
737
+ glPositionData[offset++] = angle;
738
+ }
739
+
740
+ /** Add an untextured rect to the gl draw list
741
+ * Zeroes the uvs and rgba so the texture contribution multiplies to 0,
742
+ * then carries the real color in the additive slot. Works regardless of
743
+ * which texture is currently bound.
744
+ * @param {number} x
745
+ * @param {number} y
746
+ * @param {number} sizeX
747
+ * @param {number} sizeY
748
+ * @param {number} angle
749
+ * @param {number} rgba - color as 32-bit integer
750
+ * @memberof WebGL */
751
+ function glDrawUntextured(x, y, sizeX, sizeY, angle, rgba)
752
+ {
753
+ glDraw(x, y, sizeX, sizeY, angle, 0, 0, 0, 0, 0, rgba);
754
+ }
755
+
756
+ /** Transform and add a polygon to the gl draw list
757
+ * @param {Array<Vector2>} points - Array of Vector2 points
758
+ * @param {number} rgba - Color of the polygon as a 32-bit integer
759
+ * @param {number} x
760
+ * @param {number} y
761
+ * @param {number} sx
762
+ * @param {number} sy
763
+ * @param {number} angle
764
+ * @param {boolean} [tristrip] - should tristrip algorithm be used
765
+ * @memberof WebGL */
766
+ function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
767
+ {
768
+ const pointsOut = [];
769
+ const sa = sin(-angle);
770
+ const ca = cos(-angle);
771
+ for (const p of points)
772
+ {
773
+ // transform the point
774
+ const px = p.x*sx;
775
+ const py = p.y*sy;
776
+ pointsOut.push(vec2(x + ca*px - sa*py, y + sa*px + ca*py));
777
+ }
778
+ const drawPoints = tristrip ? glPolyStrip(pointsOut) : pointsOut;
779
+ glDrawPoints(drawPoints, rgba);
780
+ }
781
+
782
+ /** Transform and add a polygon's outline to the gl draw list
783
+ * @param {Array<Vector2>} points - Array of Vector2 points
784
+ * @param {number} rgba - Color of the polygon as a 32-bit integer
785
+ * @param {number} lineWidth - Width of the outline
786
+ * @param {number} x
787
+ * @param {number} y
788
+ * @param {number} sx
789
+ * @param {number} sy
790
+ * @param {number} angle
791
+ * @param {boolean} [wrap] - Should the outline connect the first and last points
792
+ * @memberof WebGL */
793
+ function glDrawOutlineTransform(points, rgba, lineWidth, x, y, sx, sy, angle, wrap=true)
794
+ {
795
+ const outlinePoints = glMakeOutline(points, lineWidth, wrap);
796
+ glDrawPointsTransform(outlinePoints, rgba, x, y, sx, sy, angle, false);
797
+ }
798
+
799
+ /** Add a list of points to the gl draw list
800
+ * @param {Array<Vector2>} points - Array of Vector2 points in tri strip order
801
+ * @param {number} rgba - Color as a 32-bit integer
802
+ * @memberof WebGL */
803
+ function glDrawPoints(points, rgba)
804
+ { glDrawPolyStrip(points, rgba); }
805
+
806
+ /** Add a list of colored points to the gl draw list
807
+ * @param {Array<Vector2>} points - Array of Vector2 points in tri strip order
808
+ * @param {Array<number>} pointColors - Array of 32-bit integer colors
809
+ * @memberof WebGL */
810
+ function glDrawColoredPoints(points, pointColors)
811
+ { glDrawPolyStrip(points, 0, pointColors); }
812
+
813
+ // queue a triangle strip in the poly batch with degenerate verts at start and end,
814
+ // one color for all its points or one each
815
+ function glDrawPolyStrip(points, rgba, pointColors)
816
+ {
817
+ if (!glEnable || points.length < 3)
818
+ return; // needs at least 3 points to have area
819
+
820
+ // flush if there is not enough room or if different blend mode
821
+ const vertCount = points.length + 2;
822
+ if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
823
+ glFlush();
824
+ ASSERT(vertCount < gl_MAX_POLY_VERTEXES, 'poly exceeds max batch size');
825
+ if (vertCount >= gl_MAX_POLY_VERTEXES) return; // release-build safety net
826
+ glSetPolyMode();
827
+
828
+ // setup triangle strip with degenerate verts at start and end
829
+ let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
830
+ for (let i = vertCount; i--;)
831
+ {
832
+ const j = clamp(i-1, 0, vertCount-3);
833
+ const point = points[j];
834
+ const color = pointColors ? pointColors[j] : rgba;
835
+ glPositionData[offset++] = point.x;
836
+ glPositionData[offset++] = point.y;
837
+ glColorData[offset++] = color & glColorMask | glColorAdditive;
838
+ }
839
+ glBatchCount += vertCount;
840
+ }
841
+
842
+ /** Set the WebGL render target to the given texture or back to the canvas
843
+ * - What is drawn into a texture is stored premultiplied, and draws of that texture blend it as such
844
+ * @param {WebGLTexture} [texture] - a texture or undefined to use normal glCanvas
845
+ * @param {boolean} [clear] - should the render target be cleared, to canvasClearColor, CLEAR_BLACK for a transparent one
846
+ * @memberof WebGL */
847
+ function glSetRenderTarget(texture, clear=false)
848
+ {
849
+ // what was batched so far draws where it was meant to, before the target changes
850
+ glFlush();
851
+ const previousTarget = glRenderTarget;
852
+ if (texture)
853
+ {
854
+ if (glMipmapsUntilTarget.has(texture))
855
+ {
856
+ // a layer at a size other than a power of two draws without mipmaps as it always has,
857
+ // so they are not made again after every redraw, the 3D renderer still makes its own
858
+ glMipmapsUntilTarget.delete(texture);
859
+ glMipmappedTextures.delete(texture);
860
+ glContext.bindTexture(glContext.TEXTURE_2D, texture);
861
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, glContext.LINEAR);
862
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
863
+ }
864
+ glPremultipliedTextures.add(texture); // the blend writes premultiplied color into it
865
+ // coming from the canvas, keep its transform and blend mode to put back after
866
+ glRenderTarget || (glRenderTargetSaved = [glTransform, glAdditive]);
867
+ glRenderTarget = texture;
868
+ glContext.bindFramebuffer(glContext.FRAMEBUFFER, glFramebuffer);
869
+ glContext.framebufferTexture2D(glContext.FRAMEBUFFER,
870
+ glContext.COLOR_ATTACHMENT0, glContext.TEXTURE_2D, texture, 0);
871
+ glPreRender(clear);
872
+ }
873
+ else
874
+ {
875
+ glRenderTarget = undefined;
876
+ if (glRenderTargetBase)
877
+ {
878
+ // back to the texture the canvas transform was drawing into, at its own size
879
+ const [baseTexture, baseSize] = glRenderTargetBase;
880
+ glContext.bindFramebuffer(glContext.FRAMEBUFFER, glFramebuffer);
881
+ glContext.framebufferTexture2D(glContext.FRAMEBUFFER,
882
+ glContext.COLOR_ATTACHMENT0, glContext.TEXTURE_2D, baseTexture, 0);
883
+ glContext.viewport(0, 0, baseSize.x, baseSize.y);
884
+ }
885
+ else
886
+ {
887
+ glContext.bindFramebuffer(glContext.FRAMEBUFFER, null);
888
+
889
+ // use the backing store size, mainCanvasSize is css pixels and may
890
+ // still be the render target's size when unwinding a layer redraw
891
+ glContext.viewport(0, 0, glCanvas.width, glCanvas.height);
892
+ }
893
+
894
+ // the canvas's own transform and blend mode again, the target set its own
895
+ if (glRenderTargetSaved)
896
+ {
897
+ [glTransform, glAdditive] = glRenderTargetSaved;
898
+ glRenderTargetSaved = undefined;
899
+ for (const program of [glPolyShader, glShader])
900
+ {
901
+ glContext.useProgram(program);
902
+ glContext.uniformMatrix4fv(glUniformLocation(program, 'm'), false, glTransform);
903
+ }
904
+ glBatchAdditive = glAdditive;
905
+ glSetInstancedMode(true);
906
+ }
907
+ }
908
+
909
+ // a target with mipmaps drawn into has only its top level new, the smaller levels are made again from it
910
+ // the next time it is drawn from
911
+ if (previousTarget && previousTarget !== texture && glMipmappedTextures.has(previousTarget))
912
+ glMipmapsStale.add(previousTarget);
913
+ }
914
+
915
+ /** Clear out a rectangle area of the WebGL canvas or render target
916
+ * - In framebuffer pixels from the bottom left: backing store pixels on the canvas, texture pixels in a
917
+ * render target
918
+ * @param {number} x
919
+ * @param {number} y
920
+ * @param {number} width
921
+ * @param {number} height
922
+ * @memberof WebGL */
923
+ function glClearRect(x, y, width, height)
924
+ {
925
+ if (!glEnable) return;
926
+ glFlush(); // tiles batched before the clear go down before it, not over what replaces them
927
+
928
+ // Enable scissor test to clear only the specified area
929
+ glContext.enable(glContext.SCISSOR_TEST);
930
+ glContext.scissor(x, y, width, height);
931
+ glContext.clearColor(0, 0, 0, 0);
932
+ glContext.clear(glContext.COLOR_BUFFER_BIT);
933
+ glContext.disable(glContext.SCISSOR_TEST);
934
+ }
935
+
936
+ ///////////////////////////////////////////////////////////////////////////////
937
+
938
+ // WebGL internal function to convert polygon to outline triangle strip
939
+ function glMakeOutline(points, width, wrap=true)
940
+ {
941
+ if (points.length < 2)
942
+ return [];
943
+
944
+ const halfWidth = width / 2;
945
+ const strip = [];
946
+ const n = points.length;
947
+ const e = 1e-6;
948
+ // miter ratio cap (dimensionless, matches SVG/Canvas2D convention)
949
+ const miterLimit = 10;
950
+ for (let i = 0; i < n; i++)
951
+ {
952
+ // for each vertex, calculate normal based on adjacent edges
953
+ const prev = points[wrap ? (i - 1 + n) % n : max(i - 1, 0)];
954
+ const curr = points[i];
955
+ const next = points[wrap ? (i + 1) % n : min(i + 1, n - 1)];
956
+
957
+ // direction from previous to current
958
+ const dx1 = curr.x - prev.x;
959
+ const dy1 = curr.y - prev.y;
960
+ const len1 = (dx1*dx1 + dy1*dy1)**.5;
961
+
962
+ // direction from current to next
963
+ const dx2 = next.x - curr.x;
964
+ const dy2 = next.y - curr.y;
965
+ const len2 = (dx2*dx2 + dy2*dy2)**.5;
966
+
967
+ if (len1 < e && len2 < e)
968
+ continue; // skip degenerate point
969
+
970
+ // calculate perpendicular normals for each edge
971
+ const nx1 = len1 > e ? -dy1 / len1 : 0;
972
+ const ny1 = len1 > e ? dx1 / len1 : 0;
973
+ const nx2 = len2 > e ? -dy2 / len2 : 0;
974
+ const ny2 = len2 > e ? dx2 / len2 : 0;
975
+
976
+ // average the normals for miter
977
+ let nx = nx1 + nx2;
978
+ let ny = ny1 + ny2;
979
+ const nlen = (nx*nx + ny*ny)**.5;
980
+ if (nlen < e)
981
+ {
982
+ // 180 degree turn - use perpendicular
983
+ nx = nx1;
984
+ ny = ny1;
985
+ }
986
+ else
987
+ {
988
+ // calculate miter length
989
+ nx /= nlen;
990
+ ny /= nlen;
991
+ const dot = nx1 * nx + ny1 * ny;
992
+ if (dot > e)
993
+ {
994
+ // scale normal by miter length, clamped to miterLimit
995
+ const miterLength = min(1 / dot, miterLimit);
996
+ nx *= miterLength;
997
+ ny *= miterLength;
998
+ }
999
+ }
1000
+
1001
+ // create inner and outer points along the normal
1002
+ const inner = vec2(curr.x - nx * halfWidth, curr.y - ny * halfWidth);
1003
+ const outer = vec2(curr.x + nx * halfWidth, curr.y + ny * halfWidth);
1004
+ strip.push(inner);
1005
+ strip.push(outer);
1006
+ }
1007
+ if (strip.length > 1 && wrap)
1008
+ {
1009
+ // close the loop
1010
+ strip.push(strip[0]);
1011
+ strip.push(strip[1]);
1012
+ }
1013
+ return strip;
1014
+ }
1015
+
1016
+ // WebGL internal function to convert polys to tri strips
1017
+ function glPolyStrip(points)
1018
+ {
1019
+ // validate input
1020
+ if (points.length < 3)
1021
+ return [];
1022
+
1023
+ // cross product helper: (b-a) x (c-a)
1024
+ const cross = (a,b,c)=> (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
1025
+
1026
+ // calculate signed area of polygon
1027
+ const signedArea = (poly)=>
1028
+ {
1029
+ let area = 0;
1030
+ for (let i = poly.length; i--;)
1031
+ {
1032
+ const j = (i+1) % poly.length;
1033
+ area += poly[i].cross(poly[j]);
1034
+ }
1035
+ return area;
1036
+ }
1037
+
1038
+ // ensure counter-clockwise winding (slice first so we don't mutate caller's array)
1039
+ if (signedArea(points) < 0)
1040
+ points = points.slice().reverse();
1041
+
1042
+ // a convex polygon, which every circle, ellipse and regular polygon is, zigzags between its two sides
1043
+ // into one strip with no bridges, so it skips the ear clipping below and draws with a fifth of the vertices
1044
+ // convex is every corner turning left, and the edges going right then left only once each way round, since
1045
+ // a star listed point by point also turns left at every corner but winds around twice
1046
+ const e = 1e-9, n = points.length;
1047
+ let convex = true, flips = 0, lastSide = 0;
1048
+ for (let i = 0; convex && i < n; ++i)
1049
+ {
1050
+ const a = points[(i + n - 1) % n], b = points[i], dx = points[(i + 1) % n].x - b.x;
1051
+ convex = cross(a, b, points[(i + 1) % n]) > -e;
1052
+ const side = dx > e ? 1 : dx < -e ? -1 : 0;
1053
+ if (side && side !== lastSide)
1054
+ lastSide && ++flips, lastSide = side;
1055
+ }
1056
+ if (convex && flips <= 2)
1057
+ {
1058
+ const strip = [points[0]];
1059
+ for (let i = 1, j = n - 1; i <= j; ++i, --j)
1060
+ {
1061
+ strip.push(points[i]);
1062
+ i === j || strip.push(points[j]);
1063
+ }
1064
+ return strip;
1065
+ }
1066
+
1067
+ // check if point is inside triangle
1068
+ const pointInTriangle = (p, a, b, c)=>
1069
+ {
1070
+ const c1 = cross(a, b, p);
1071
+ const c2 = cross(b, c, p);
1072
+ const c3 = cross(c, a, p);
1073
+ const negative = (c1<-e?1:0) + (c2<-e?1:0) + (c3<-e?1:0);
1074
+ const positive = (c1> e?1:0) + (c2> e?1:0) + (c3> e?1:0);
1075
+ return !(negative && positive);
1076
+ };
1077
+
1078
+ // ear clipping triangulation
1079
+ const indices = [];
1080
+ for (let i = 0; i < points.length; ++i)
1081
+ indices[i] = i;
1082
+ const triangles = [];
1083
+ let attempts = 0;
1084
+ const maxAttempts = points.length ** 2 + 100;
1085
+ while (indices.length > 3 && attempts++ < maxAttempts)
1086
+ {
1087
+ let foundEar = false;
1088
+ for (let i = 0; i < indices.length; i++)
1089
+ {
1090
+ const i0 = indices[(i + indices.length - 1) % indices.length];
1091
+ const i1 = indices[i];
1092
+ const i2 = indices[(i + 1) % indices.length];
1093
+ const a = points[i0], b = points[i1], c = points[i2];
1094
+
1095
+ // check if convex
1096
+ if (cross(a, b, c) < e) continue;
1097
+
1098
+ // check if any other point is inside
1099
+ let hasInside = false;
1100
+ for (let j = 0; j < indices.length; j++)
1101
+ {
1102
+ const k = indices[j];
1103
+ if (k === i0 || k === i1 || k === i2) continue;
1104
+
1105
+ const p = points[k];
1106
+ hasInside = pointInTriangle(p, a, b, c);
1107
+ if (hasInside) break;
1108
+ }
1109
+ if (hasInside) continue;
1110
+
1111
+ // found valid ear
1112
+ triangles.push([i0, i1, i2]);
1113
+ indices.splice(i, 1);
1114
+ foundEar = true;
1115
+ break;
1116
+ }
1117
+
1118
+ // fallback for degenerate cases
1119
+ if (!foundEar)
1120
+ {
1121
+ let worstIndex = -1, worstValue = Infinity;
1122
+ for (let i = 0; i < indices.length; i++)
1123
+ {
1124
+ const i0 = indices[(i + indices.length - 1) % indices.length];
1125
+ const i1 = indices[i];
1126
+ const i2 = indices[(i + 1) % indices.length];
1127
+ const value = abs(cross(points[i0], points[i1], points[i2]));
1128
+ if (value < worstValue)
1129
+ {
1130
+ worstValue = value;
1131
+ worstIndex = i;
1132
+ }
1133
+ }
1134
+ if (worstIndex < 0) break;
1135
+
1136
+ const i0 = indices[(worstIndex + indices.length - 1) % indices.length];
1137
+ const i1 = indices[worstIndex];
1138
+ const i2 = indices[(worstIndex + 1) % indices.length];
1139
+ triangles.push([i0, i1, i2]);
1140
+ indices.splice(worstIndex, 1);
1141
+ }
1142
+ }
1143
+
1144
+ // add final triangle
1145
+ if (indices.length === 3)
1146
+ triangles.push([indices[0], indices[1], indices[2]]);
1147
+ if (!triangles.length)
1148
+ return [];
1149
+
1150
+ // convert triangles to triangle strip with degenerate connectors
1151
+ const strip = [];
1152
+ let [a0, b0, c0] = triangles[0];
1153
+ strip.push(points[a0], points[b0], points[c0]);
1154
+ for (let i = 1; i < triangles.length; i++)
1155
+ {
1156
+ // add degenerate bridge from last vertex to first of new triangle
1157
+ const [a, b, c] = triangles[i];
1158
+ strip.push(points[c0], points[a]);
1159
+ strip.push(points[a], points[b], points[c]);
1160
+ c0 = c;
1161
+ }
1162
+ return strip;
942
1163
  }