littlejsengine 1.18.28 → 1.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,942 +1,1006 @@
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 glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos, glInstancedVAO, glPolyVAO, glFramebuffer, glRenderTarget, glShaderObjects = [], glCustomShader, glBatchShader, glProgramCustom, glTransform, glUniformLocations = new Map, 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
+ // the sprite vertex shader, shared by the engine's program and every Shader so one vertex layout fits all
50
+ const gl_VERTEX_SOURCE =
51
+ '#version 300 es\n' + // specify GLSL ES version
52
+ 'precision highp float;'+ // use highp for accuracy
53
+ 'uniform mat4 m;'+ // transform matrix
54
+ 'layout(location=0) in vec2 g;'+ // in: geometry
55
+ 'layout(location=1) in vec4 p;'+ // in: position/size
56
+ 'layout(location=2) in vec4 u;'+ // in: uvs
57
+ 'layout(location=3) in vec4 c;'+ // in: color
58
+ 'layout(location=4) in vec4 a;'+ // in: additiveColor
59
+ 'layout(location=5) in float r;'+// in: rotation
60
+ 'out vec2 v,l;'+ // out: uv, and 0 to 1 across the sprite for a Shader's localUV
61
+ 'out vec4 d,e;'+ // out: color, additiveColor
62
+ 'void main(){'+ // shader entry point
63
+ 'vec2 s=(g-.5)*p.zw;'+ // get size offset
64
+ 'gl_Position=m*vec4(p.xy+s*cos(r)-vec2(-s.y,s)*sin(r),1,1);'+ // transform position
65
+ 'v=mix(u.xw,u.zy,g);'+ // pass uv to fragment shader
66
+ 'l=g;d=c;e=a;'+ // pass local uv and colors to fragment shader
67
+ '}'; // end of shader
68
+
69
+ function glInit(rootElement)
70
+ {
71
+ // keep set of texture infos so they can be restored if context is lost
72
+ glTextureInfos = new Set;
73
+
74
+ if (!glEnable || headlessMode)
75
+ {
76
+ glCanBeEnabled = false;
77
+ return;
78
+ }
79
+
80
+ // create the canvas and textures
81
+ glCanvas = document.createElement('canvas');
82
+ glContext = glCanvas.getContext('webgl2', {antialias:glAntialias});
83
+
84
+ if (!glContext)
85
+ {
86
+ console.warn('WebGL2 not supported, falling back to 2D canvas rendering!');
87
+ glCanvas = glContext = undefined;
88
+ glEnable = false;
89
+ glCanBeEnabled = false;
90
+ return;
91
+ }
92
+
93
+ // attach the WebGL canvas;
94
+ rootElement.appendChild(glCanvas);
95
+
96
+ // startup webgl
97
+ initWebGL();
98
+
99
+ // setup context lost and restore handlers
100
+ glCanvas.addEventListener('webglcontextlost', (e)=>
101
+ {
102
+ glEnable = false; // disable WebGL rendering
103
+ glCanvas.style.display = 'none'; // hide the gl canvas
104
+ e.preventDefault(); // prevent default to allow restoration
105
+ LOG('WebGL context lost! Switching to Canvas2d rendering.');
106
+
107
+ // remove WebGL textures
108
+ for (const info of glTextureInfos)
109
+ info.glTexture = undefined;
110
+ glActiveTexture = undefined;
111
+ // every Shader compiles again on its next draw, and the first flush after restore picks its program again
112
+ for (const shader of glShaderObjects)
113
+ shader.program = undefined;
114
+ glBatchShader = undefined;
115
+ glProgramCustom = true;
116
+ glUniformLocations = new Map; // the programs those belonged to are gone
117
+ // drop any partially-filled batch so the next glFlush doesn't
118
+ // upload stale glBatchCount against fresh empty buffers on restore
119
+ glBatchCount = 0;
120
+ glPolyMode = false;
121
+ pluginList.forEach(plugin=>plugin.glContextLost?.());
122
+ });
123
+ glCanvas.addEventListener('webglcontextrestored', ()=>
124
+ {
125
+ glEnable = true; // re-enable WebGL rendering
126
+ glCanvas.style.display = ''; // show the gl canvas
127
+ LOG('WebGL context restored, reinitializing...');
128
+
129
+ // reinit WebGL and restore textures
130
+ initWebGL();
131
+ for (const info of glTextureInfos)
132
+ info.glTexture = glCreateTexture(info.image, info.wrap);
133
+ pluginList.forEach(plugin=>plugin.glContextRestored?.());
134
+ });
135
+
136
+ function initWebGL()
137
+ {
138
+ // setup instanced rendering shader program
139
+ glShader = glCreateProgram(gl_VERTEX_SOURCE,
140
+ '#version 300 es\n' + // specify GLSL ES version
141
+ 'precision highp float;'+ // use highp for accuracy
142
+ 'uniform sampler2D s;'+ // texture
143
+ 'in vec2 v;'+ // in: uv
144
+ 'in vec4 d,e;'+ // in: color, additiveColor
145
+ 'out vec4 c;'+ // out: color
146
+ 'void main(){'+ // shader entry point
147
+ 'c=texture(s,v)*d+e;'+ // modulate texture by color plus additive
148
+ '}' // end of shader
149
+ );
150
+
151
+ // setup poly rendering shaders
152
+ glPolyShader = glCreateProgram(
153
+ '#version 300 es\n' + // specify GLSL ES version
154
+ 'precision highp float;'+ // use highp for better accuracy
155
+ 'uniform mat4 m;'+ // transform matrix
156
+ 'in vec2 p;'+ // in: position
157
+ 'in vec4 c;'+ // in: color
158
+ 'out vec4 d;'+ // out: color
159
+ 'void main(){'+ // shader entry point
160
+ 'gl_Position=m*vec4(p,1,1);'+ // transform position
161
+ 'd=c;'+ // pass color to fragment shader
162
+ '}' // end of shader
163
+ ,
164
+ '#version 300 es\n' + // specify GLSL ES version
165
+ 'precision highp float;'+ // use highp for better accuracy
166
+ 'in vec4 d;'+ // in: color
167
+ 'out vec4 c;'+ // out: color
168
+ 'void main(){'+ // shader entry point
169
+ 'c=d;'+ // set color
170
+ '}' // end of shader
171
+ );
172
+
173
+ // init buffers
174
+ const glInstanceData = new ArrayBuffer(gl_ARRAY_BUFFER_SIZE);
175
+ glPositionData = new Float32Array(glInstanceData);
176
+ glColorData = new Uint32Array(glInstanceData);
177
+ glArrayBuffer = glContext.createBuffer();
178
+ glGeometryBuffer = glContext.createBuffer();
179
+ glFramebuffer = glContext.createFramebuffer();
180
+ glBatchCount = 0;
181
+
182
+ // create the geometry buffer, triangle strip square
183
+ const geometry = new Float32Array([0,0,1,0,0,1,1,1]);
184
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
185
+ glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
186
+
187
+ let offset, shader, stride;
188
+ const initVertexAttrib = (name, type, typeSize, size, divisor=0)=>
189
+ {
190
+ const location = glContext.getAttribLocation(shader, name);
191
+ const normalize = typeSize === 1;
192
+ const fixedStride = typeSize && stride;
193
+ glContext.enableVertexAttribArray(location);
194
+ glContext.vertexAttribPointer(location, size, type, normalize, fixedStride, offset);
195
+ glContext.vertexAttribDivisor(location, divisor);
196
+ offset += size*typeSize;
197
+ }
198
+
199
+ // setup VAO for instanced rendering
200
+ glInstancedVAO = glContext.createVertexArray();
201
+ glContext.bindVertexArray(glInstancedVAO);
202
+
203
+ // configure instanced vertex attributes
204
+ offset = 0, shader = glShader, stride = gl_INSTANCE_BYTE_STRIDE;
205
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
206
+ initVertexAttrib('g', glContext.FLOAT, 0, 2); // geometry
207
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
208
+ glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
209
+ initVertexAttrib('p', glContext.FLOAT, 4, 4, 1); // position & size
210
+ initVertexAttrib('u', glContext.FLOAT, 4, 4, 1); // texture coords
211
+ initVertexAttrib('c', glContext.UNSIGNED_BYTE, 1, 4, 1); // color
212
+ initVertexAttrib('a', glContext.UNSIGNED_BYTE, 1, 4, 1); // additiveColor
213
+ initVertexAttrib('r', glContext.FLOAT, 4, 1, 1); // rotation
214
+
215
+ // setup VAO for poly rendering
216
+ glPolyVAO = glContext.createVertexArray();
217
+ glContext.bindVertexArray(glPolyVAO);
218
+
219
+ // configure poly vertex attributes
220
+ offset = 0, shader = glPolyShader, stride = gl_POLY_VERTEX_BYTE_STRIDE;
221
+ initVertexAttrib('p', glContext.FLOAT, 4, 2); // position
222
+ initVertexAttrib('c', glContext.UNSIGNED_BYTE, 1, 4); // color
223
+ }
224
+ }
225
+
226
+ function glSetInstancedMode(force=false)
227
+ {
228
+ if (!force && !glPolyMode) return;
229
+
230
+ // setup instanced mode
231
+ glFlush();
232
+ glPolyMode = false;
233
+ glContext.useProgram(glShader);
234
+ glContext.bindVertexArray(glInstancedVAO);
235
+ }
236
+
237
+ function glSetPolyMode()
238
+ {
239
+ if (glPolyMode) return;
240
+
241
+ // setup poly mode
242
+ glFlush();
243
+ glPolyMode = true;
244
+ glContext.useProgram(glPolyShader);
245
+ glContext.bindVertexArray(glPolyVAO);
246
+ }
247
+
248
+ // Setup WebGL render each frame, called automatically by engine
249
+ // Also used by tile layer rendering when redrawing tiles
250
+ function glPreRender(clear=true)
251
+ {
252
+ if (!glEnable || !glContext) return;
253
+
254
+ ASSERT(!glBatchCount, 'glPreRender called with unflushed batch.');
255
+
256
+ // mainCanvasSize is css pixels, the backing store is scaled by the pixel
257
+ // ratio, render targets are offscreen so they are never scaled
258
+ const dpr = glRenderTarget ? 1 : getCanvasPixelRatio();
259
+ const bufferSizeX = mainCanvasSize.x * dpr | 0;
260
+ const bufferSizeY = mainCanvasSize.y * dpr | 0;
261
+ if (!glRenderTarget)
262
+ {
263
+ // set to same size as main canvas, only when it changes because
264
+ // setting it reallocates the drawing buffer and invalidates the frame
265
+ if (glCanvas.width !== bufferSizeX || glCanvas.height !== bufferSizeY)
266
+ {
267
+ glCanvas.width = bufferSizeX;
268
+ glCanvas.height = bufferSizeY;
269
+ }
270
+ }
271
+ glContext.viewport(0, 0, bufferSizeX, bufferSizeY);
272
+ clear && glClearCanvas();
273
+
274
+ // build the transform matrix
275
+ const s = vec2(2*cameraScale).divide(mainCanvasSize);
276
+ if (glRenderTarget)
277
+ s.y = -s.y; // invert y when using render target
278
+ const rotatedCam = cameraPos.rotate(-cameraAngle);
279
+ const p = vec2(-1).subtract(rotatedCam.multiply(s));
280
+ const ca = cos(cameraAngle);
281
+ const sa = sin(cameraAngle);
282
+ const transform = [
283
+ s.x * ca, s.y * sa, 0, 0,
284
+ -s.x * sa, s.y * ca, 0, 0,
285
+ 1, 1, 1, 0,
286
+ p.x, p.y, 0, 1];
287
+ glTransform = transform;
288
+
289
+ // set the same transform matrix for both shaders
290
+ const initUniform = (program, uniform, value)=>
291
+ {
292
+ glContext.useProgram(program);
293
+ const location = glContext.getUniformLocation(program, uniform);
294
+ glContext.uniformMatrix4fv(location, false, value);
295
+ }
296
+ initUniform(glPolyShader, 'm', transform);
297
+ initUniform(glShader, 'm', transform);
298
+
299
+ // set the active texture
300
+ glContext.activeTexture(glContext.TEXTURE0);
301
+ if (textureInfos[0])
302
+ {
303
+ glActiveTexture = textureInfos[0].glTexture;
304
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
305
+ }
306
+
307
+ // rebind the array buffer
308
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
309
+
310
+ // start with additive blending off
311
+ glAdditive = glBatchAdditive = false;
312
+
313
+ // force it to set instanced mode
314
+ glSetInstancedMode(true);
315
+ }
316
+
317
+ /** Clear the canvas and setup the viewport
318
+ * @memberof WebGL */
319
+ function glClearCanvas()
320
+ {
321
+ if (!glContext) return;
322
+
323
+ // clear using the canvasClearColor
324
+ const color = canvasClearColor;
325
+ glContext.clearColor(color.r, color.g, color.b, color.a);
326
+ glContext.clear(glContext.COLOR_BUFFER_BIT);
327
+ }
328
+
329
+ /** Set the WebGL texture, called automatically if using multiple textures
330
+ * - This may also flush the gl buffer resulting in more draw calls and worse performance
331
+ * @param {WebGLTexture} texture
332
+ * @memberof WebGL */
333
+ function glSetTexture(texture)
334
+ {
335
+ // must flush cache with the old texture to set a new one
336
+ if (!glContext || texture === glActiveTexture) return;
337
+
338
+ glFlush();
339
+ glActiveTexture = texture;
340
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
341
+ }
342
+
343
+ /** Set the wrap mode (REPEAT or CLAMP_TO_EDGE) on an existing WebGL texture
344
+ * Flushes the current batch only if the texture is the active one
345
+ * @param {WebGLTexture} texture
346
+ * @param {boolean} [wrap] - true for REPEAT, false for CLAMP_TO_EDGE
347
+ * @memberof WebGL */
348
+ function glSetTextureWrap(texture, wrap=true)
349
+ {
350
+ if (!glContext || !texture) return;
351
+
352
+ // flush only if changing wrap on the currently bound texture
353
+ const isCurrent = texture === glActiveTexture;
354
+ if (isCurrent)
355
+ glFlush();
356
+ else
357
+ glContext.bindTexture(glContext.TEXTURE_2D, texture);
358
+
359
+ const wrapMode = wrap ? glContext.REPEAT : glContext.CLAMP_TO_EDGE;
360
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_S, wrapMode);
361
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_T, wrapMode);
362
+
363
+ if (!isCurrent && glActiveTexture)
364
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
365
+ }
366
+
367
+ /** Compile WebGL shader of the given type, will throw errors if in debug mode
368
+ * @param {string} source
369
+ * @param {number} type
370
+ * @return {WebGLShader}
371
+ * @memberof WebGL */
372
+ function glCompileShader(source, type)
373
+ {
374
+ if (!glContext) return;
375
+
376
+ // build the shader
377
+ const shader = glContext.createShader(type);
378
+ glContext.shaderSource(shader, source);
379
+ glContext.compileShader(shader);
380
+
381
+ // check for errors
382
+ if (debug && !glContext.getShaderParameter(shader, glContext.COMPILE_STATUS))
383
+ throw glContext.getShaderInfoLog(shader);
384
+ return shader;
385
+ }
386
+
387
+ /** Create WebGL program with given shaders
388
+ * @param {string} vsSource
389
+ * @param {string} fsSource
390
+ * @return {WebGLProgram}
391
+ * @memberof WebGL */
392
+ function glCreateProgram(vsSource, fsSource)
393
+ {
394
+ if (!glContext) return;
395
+
396
+ // build the program
397
+ const program = glContext.createProgram();
398
+ glContext.attachShader(program, glCompileShader(vsSource, glContext.VERTEX_SHADER));
399
+ glContext.attachShader(program, glCompileShader(fsSource, glContext.FRAGMENT_SHADER));
400
+ glContext.linkProgram(program);
401
+
402
+ // check for errors
403
+ if (debug && !glContext.getProgramParameter(program, glContext.LINK_STATUS))
404
+ throw glContext.getProgramInfoLog(program);
405
+ return program;
406
+ }
407
+
408
+ // a uniform location, looked up once per program
409
+ function glUniformLocation(program, name)
410
+ {
411
+ let cache = glUniformLocations.get(program);
412
+ cache || glUniformLocations.set(program, cache = {});
413
+ return cache[name] ??= glContext.getUniformLocation(program, name);
414
+ }
415
+
416
+ // a Shader's 2D program, compiled the first time a batch needs it: the snippet's mainImage gives the surface
417
+ // color, then the sprite's color and additive color apply as the engine's own fragment shader does
418
+ function glShaderProgram(shader)
419
+ {
420
+ return shader.program ||= glCreateProgram(gl_VERTEX_SOURCE,
421
+ '#version 300 es\n' +
422
+ 'precision highp float;' +
423
+ 'uniform sampler2D iChannel0;' + // the texture
424
+ 'uniform vec3 iResolution;' + // canvas size in pixels
425
+ 'uniform float iTime;' + // engine time
426
+ 'in vec2 v,l;in vec4 d,e;out vec4 c;\n' + // a define needs its own line
427
+ '#define localUV l\n' +
428
+ shader.fragmentCode + '\n' +
429
+ 'void main(){vec4 t;mainImage(t,v);c=t*d+e;}');
430
+ }
431
+
432
+ /** Create WebGL texture from an image and init the texture settings
433
+ * Restores the active texture when done
434
+ * @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas} [image]
435
+ * @param {boolean} [wrap] - true for REPEAT, false for CLAMP_TO_EDGE
436
+ * @return {WebGLTexture}
437
+ * @memberof WebGL */
438
+ function glCreateTexture(image, wrap=false)
439
+ {
440
+ if (!glContext) return;
441
+
442
+ // build the texture
443
+ const texture = glContext.createTexture();
444
+ let mipMap = false;
445
+ if (image?.width)
446
+ {
447
+ glSetTextureData(texture, image);
448
+ glContext.bindTexture(glContext.TEXTURE_2D, texture);
449
+ mipMap = !tilesPixelated && isPowerOfTwo(image.width) && isPowerOfTwo(image.height);
450
+ }
451
+ else
452
+ {
453
+ // create a white texture
454
+ const whitePixel = new Uint8Array([255, 255, 255, 255]);
455
+ glContext.bindTexture(glContext.TEXTURE_2D, texture);
456
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, 1, 1, 0, glContext.RGBA, glContext.UNSIGNED_BYTE, whitePixel);
457
+ }
458
+
459
+ // set texture filtering
460
+ const magFilter = tilesPixelated ? glContext.NEAREST : glContext.LINEAR;
461
+ const minFilter = mipMap ? glContext.LINEAR_MIPMAP_LINEAR : magFilter;
462
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MAG_FILTER, magFilter);
463
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, minFilter);
464
+ const wrapMode = wrap ? glContext.REPEAT : glContext.CLAMP_TO_EDGE;
465
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_S, wrapMode);
466
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_T, wrapMode);
467
+ if (mipMap)
468
+ glContext.generateMipmap(glContext.TEXTURE_2D);
469
+
470
+ // rebind active texture
471
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
472
+ return texture;
473
+ }
474
+
475
+ /** Deletes a WebGL texture
476
+ * @param {WebGLTexture} [texture]
477
+ * @memberof WebGL */
478
+ function glDeleteTexture(texture)
479
+ {
480
+ if (!glContext) return;
481
+
482
+ glContext.deleteTexture(texture);
483
+ }
484
+
485
+ /** Set WebGL texture data from an image, restores the active texture when done
486
+ * @param {WebGLTexture} texture
487
+ * @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas} image
488
+ * @memberof WebGL */
489
+ function glSetTextureData(texture, image)
490
+ {
491
+ if (!glContext) return;
492
+
493
+ // build the texture
494
+ ASSERT(image?.width > 0, 'Invalid image data.');
495
+ glContext.bindTexture(glContext.TEXTURE_2D, texture);
496
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
497
+
498
+ // keep mipmaps in sync with new level 0 data (same condition as glCreateTexture)
499
+ if (!tilesPixelated && isPowerOfTwo(image.width) && isPowerOfTwo(image.height))
500
+ glContext.generateMipmap(glContext.TEXTURE_2D);
501
+
502
+ // rebind active texture
503
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
504
+ }
505
+
506
+ /** Tells WebGL to create or update the glTexture and start tracking it
507
+ * @param {TextureInfo} textureInfo
508
+ * @memberof WebGL */
509
+ function glRegisterTextureInfo(textureInfo)
510
+ {
511
+ if (headlessMode) return;
512
+
513
+ // add texture info to tracking list even if gl is not enabled
514
+ glTextureInfos.add(textureInfo);
515
+
516
+ if (!glContext) return;
517
+
518
+ // create or set the texture data
519
+ if (textureInfo.glTexture)
520
+ glSetTextureData(textureInfo.glTexture, textureInfo.image);
521
+ else
522
+ textureInfo.glTexture = glCreateTexture(textureInfo.image, textureInfo.wrap);
523
+ }
524
+
525
+ /** Tells WebGL to destroy the glTexture and stop tracking it
526
+ * @param {TextureInfo} textureInfo
527
+ * @memberof WebGL */
528
+ function glUnregisterTextureInfo(textureInfo)
529
+ {
530
+ if (headlessMode) return;
531
+
532
+ // delete texture info from tracking list even if gl is not enabled
533
+ glTextureInfos.delete(textureInfo);
534
+
535
+ // unset and destroy the texture
536
+ const glTexture = textureInfo.glTexture;
537
+ textureInfo.glTexture = undefined;
538
+ glDeleteTexture(glTexture);
539
+ }
540
+
541
+ /** Draw all sprites and clear out the buffer, called automatically by the system whenever necessary
542
+ * @memberof WebGL */
543
+ function glFlush()
544
+ {
545
+ if (glEnable && glContext && glBatchCount)
546
+ {
547
+ // set blend mode
548
+ const destBlend = glBatchAdditive ? glContext.ONE : glContext.ONE_MINUS_SRC_ALPHA;
549
+ glContext.blendFuncSeparate(glContext.SRC_ALPHA, destBlend, glContext.ONE, destBlend);
550
+ glContext.enable(glContext.BLEND);
551
+
552
+ // a Shader's program for this batch, or the engine's own again after one
553
+ if (!glPolyMode && (glBatchShader || glProgramCustom))
554
+ {
555
+ const program = glBatchShader ? glShaderProgram(glBatchShader) : glShader;
556
+ glContext.useProgram(program);
557
+ glProgramCustom = !!glBatchShader;
558
+ if (glBatchShader)
559
+ {
560
+ const uniform = (name)=> glUniformLocation(program, name);
561
+ glContext.uniformMatrix4fv(uniform('m'), false, glTransform);
562
+ glContext.uniform1f(uniform('iTime'), time);
563
+ glContext.uniform3f(uniform('iResolution'), glCanvas.width, glCanvas.height, 1);
564
+ }
565
+ }
566
+
567
+ const byteLength = glBatchCount *
568
+ (glPolyMode ? gl_INDICES_PER_POLY_VERTEX : gl_INDICES_PER_INSTANCE);
569
+ glContext.bufferSubData(glContext.ARRAY_BUFFER, 0, glPositionData, 0, byteLength);
570
+
571
+ // draw the batch
572
+ if (glPolyMode)
573
+ glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, glBatchCount);
574
+ else
575
+ glContext.drawArraysInstanced(glContext.TRIANGLE_STRIP, 0, 4, glBatchCount);
576
+ ++drawCount;
577
+ primitiveCount += glBatchCount;
578
+ glBatchCount = 0;
579
+ }
580
+ glBatchAdditive = glAdditive;
581
+ glBatchShader = glCustomShader;
582
+ }
583
+
584
+ /** Flush any sprites still in the buffer and copy to main canvas
585
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
586
+ * @memberof WebGL */
587
+ function glCopyToContext(context)
588
+ {
589
+ if (!glEnable || !glContext) return;
590
+
591
+ glFlush();
592
+ context.drawImage(glCanvas, 0, 0);
593
+ }
594
+
595
+ /** Set anti-aliasing for WebGL canvas
596
+ * Must be called before engineInit
597
+ * @param {boolean} [antialias]
598
+ * @memberof WebGL */
599
+ function glSetAntialias(antialias=true)
600
+ {
601
+ ASSERT(!glCanvas, 'must be called before engineInit');
602
+ glAntialias = antialias;
603
+ }
604
+
605
+ /** Add a sprite to the gl draw list, used by all gl draw functions
606
+ * @param {number} x
607
+ * @param {number} y
608
+ * @param {number} sizeX
609
+ * @param {number} sizeY
610
+ * @param {number} [angle]
611
+ * @param {number} [uv0X]
612
+ * @param {number} [uv0Y]
613
+ * @param {number} [uv1X]
614
+ * @param {number} [uv1Y]
615
+ * @param {number} [rgba=-1] - white is -1
616
+ * @param {number} [rgbaAdditive=0] - black is 0
617
+ * @memberof WebGL */
618
+ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgba=-1, rgbaAdditive=0)
619
+ {
620
+ // flush if there is not enough room or if different blend mode
621
+ if (glBatchCount >= gl_MAX_INSTANCES || glBatchAdditive !== glAdditive || glBatchShader !== glCustomShader)
622
+ glFlush();
623
+ glSetInstancedMode();
624
+
625
+ let offset = glBatchCount++ * gl_INDICES_PER_INSTANCE;
626
+ glPositionData[offset++] = x;
627
+ glPositionData[offset++] = y;
628
+ glPositionData[offset++] = sizeX;
629
+ glPositionData[offset++] = sizeY;
630
+ glPositionData[offset++] = uv0X;
631
+ glPositionData[offset++] = uv0Y;
632
+ glPositionData[offset++] = uv1X;
633
+ glPositionData[offset++] = uv1Y;
634
+ glColorData[offset++] = rgba;
635
+ glColorData[offset++] = rgbaAdditive;
636
+ glPositionData[offset++] = angle;
637
+ }
638
+
639
+ /** Add an untextured rect to the gl draw list
640
+ * Zeroes the uvs and rgba so the texture contribution multiplies to 0,
641
+ * then carries the real color in the additive slot. Works regardless of
642
+ * which texture is currently bound.
643
+ * @param {number} x
644
+ * @param {number} y
645
+ * @param {number} sizeX
646
+ * @param {number} sizeY
647
+ * @param {number} angle
648
+ * @param {number} rgba - color as 32-bit integer
649
+ * @memberof WebGL */
650
+ function glDrawUntextured(x, y, sizeX, sizeY, angle, rgba)
651
+ {
652
+ glDraw(x, y, sizeX, sizeY, angle, 0, 0, 0, 0, 0, rgba);
653
+ }
654
+
655
+ /** Transform and add a polygon to the gl draw list
656
+ * @param {Array<Vector2>} points - Array of Vector2 points
657
+ * @param {number} rgba - Color of the polygon as a 32-bit integer
658
+ * @param {number} x
659
+ * @param {number} y
660
+ * @param {number} sx
661
+ * @param {number} sy
662
+ * @param {number} angle
663
+ * @param {boolean} [tristrip] - should tristrip algorithm be used
664
+ * @memberof WebGL */
665
+ function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
666
+ {
667
+ const pointsOut = [];
668
+ const sa = sin(-angle);
669
+ const ca = cos(-angle);
670
+ for (const p of points)
671
+ {
672
+ // transform the point
673
+ const px = p.x*sx;
674
+ const py = p.y*sy;
675
+ pointsOut.push(vec2(x + ca*px - sa*py, y + sa*px + ca*py));
676
+ }
677
+ const drawPoints = tristrip ? glPolyStrip(pointsOut) : pointsOut;
678
+ glDrawPoints(drawPoints, rgba);
679
+ }
680
+
681
+ /** Transform and add a polygon to the gl draw list
682
+ * @param {Array<Vector2>} points - Array of Vector2 points
683
+ * @param {number} rgba - Color of the polygon as a 32-bit integer
684
+ * @param {number} lineWidth - Width of the outline
685
+ * @param {number} x
686
+ * @param {number} y
687
+ * @param {number} sx
688
+ * @param {number} sy
689
+ * @param {number} angle
690
+ * @param {boolean} [wrap] - Should the outline connect the first and last points
691
+ * @memberof WebGL */
692
+ function glDrawOutlineTransform(points, rgba, lineWidth, x, y, sx, sy, angle, wrap=true)
693
+ {
694
+ const outlinePoints = glMakeOutline(points, lineWidth, wrap);
695
+ glDrawPointsTransform(outlinePoints, rgba, x, y, sx, sy, angle, false);
696
+ }
697
+
698
+ /** Add a list of points to the gl draw list
699
+ * @param {Array<Vector2>} points - Array of Vector2 points in tri strip order
700
+ * @param {number} rgba - Color as a 32-bit integer
701
+ * @memberof WebGL */
702
+ function glDrawPoints(points, rgba)
703
+ {
704
+ if (!glEnable || points.length < 3)
705
+ return; // needs at least 3 points to have area
706
+
707
+ // flush if there is not enough room or if different blend mode
708
+ const vertCount = points.length + 2;
709
+ if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
710
+ glFlush();
711
+ ASSERT(vertCount < gl_MAX_POLY_VERTEXES, 'poly exceeds max batch size');
712
+ if (vertCount >= gl_MAX_POLY_VERTEXES) return; // release-build safety net
713
+ glSetPolyMode();
714
+
715
+ // setup triangle strip with degenerate verts at start and end
716
+ let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
717
+ for (let i = vertCount; i--;)
718
+ {
719
+ const j = clamp(i-1, 0, vertCount-3);
720
+ const point = points[j];
721
+ glPositionData[offset++] = point.x;
722
+ glPositionData[offset++] = point.y;
723
+ glColorData[offset++] = rgba;
724
+ }
725
+ glBatchCount += vertCount;
726
+ }
727
+
728
+ /** Add a list of colored points to the gl draw list
729
+ * @param {Array<Vector2>} points - Array of Vector2 points in tri strip order
730
+ * @param {Array<number>} pointColors - Array of 32-bit integer colors
731
+ * @memberof WebGL */
732
+ function glDrawColoredPoints(points, pointColors)
733
+ {
734
+ if (!glEnable || points.length < 3)
735
+ return; // needs at least 3 points to have area
736
+
737
+ // flush if there is not enough room or if different blend mode
738
+ const vertCount = points.length + 2;
739
+ if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
740
+ glFlush();
741
+ ASSERT(vertCount < gl_MAX_POLY_VERTEXES, 'poly exceeds max batch size');
742
+ if (vertCount >= gl_MAX_POLY_VERTEXES) return; // release-build safety net
743
+ glSetPolyMode();
744
+
745
+ // setup triangle strip with degenerate verts at start and end
746
+ let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
747
+ for (let i = vertCount; i--;)
748
+ {
749
+ const j = clamp(i-1, 0, vertCount-3);
750
+ const point = points[j];
751
+ const color = pointColors[j];
752
+ glPositionData[offset++] = point.x;
753
+ glPositionData[offset++] = point.y;
754
+ glColorData[offset++] = color;
755
+ }
756
+ glBatchCount += vertCount;
757
+ }
758
+
759
+ /** Set the WebGL render target to the given texture or back to the canvas
760
+ * @param {WebGLTexture} [texture] - a texture or undefined to use normal glCanvas
761
+ * @param {boolean} [clear] - should the render target be cleared
762
+ * @memberof WebGL */
763
+ function glSetRenderTarget(texture, clear=false)
764
+ {
765
+ if (texture)
766
+ {
767
+ glRenderTarget = texture;
768
+ glContext.bindFramebuffer(glContext.FRAMEBUFFER, glFramebuffer);
769
+ glContext.framebufferTexture2D(glContext.FRAMEBUFFER,
770
+ glContext.COLOR_ATTACHMENT0, glContext.TEXTURE_2D, texture, 0);
771
+ glPreRender(clear);
772
+ }
773
+ else
774
+ {
775
+ glFlush();
776
+ glRenderTarget = undefined;
777
+ glContext.bindFramebuffer(glContext.FRAMEBUFFER, null);
778
+
779
+ // use the backing store size, mainCanvasSize is css pixels and may
780
+ // still be the render target's size when unwinding a layer redraw
781
+ glContext.viewport(0, 0, glCanvas.width, glCanvas.height);
782
+ }
783
+ }
784
+
785
+ /** Clear out a rectangle area of the WebGL canvas or render target
786
+ * @param {number} x
787
+ * @param {number} y
788
+ * @param {number} width
789
+ * @param {number} height
790
+ * @memberof WebGL */
791
+ function glClearRect(x, y, width, height)
792
+ {
793
+ if (!glEnable) return;
794
+
795
+ // Enable scissor test to clear only the specified area
796
+ glContext.enable(glContext.SCISSOR_TEST);
797
+ glContext.scissor(x, y, width, height);
798
+ glContext.clearColor(0, 0, 0, 0);
799
+ glContext.clear(glContext.COLOR_BUFFER_BIT);
800
+ glContext.disable(glContext.SCISSOR_TEST);
801
+ }
802
+
803
+ ///////////////////////////////////////////////////////////////////////////////
804
+
805
+ // WebGL internal function to convert polygon to outline triangle strip
806
+ function glMakeOutline(points, width, wrap=true)
807
+ {
808
+ if (points.length < 2)
809
+ return [];
810
+
811
+ const halfWidth = width / 2;
812
+ const strip = [];
813
+ const n = points.length;
814
+ const e = 1e-6;
815
+ // miter ratio cap (dimensionless, matches SVG/Canvas2D convention)
816
+ const miterLimit = 10;
817
+ for (let i = 0; i < n; i++)
818
+ {
819
+ // for each vertex, calculate normal based on adjacent edges
820
+ const prev = points[wrap ? (i - 1 + n) % n : max(i - 1, 0)];
821
+ const curr = points[i];
822
+ const next = points[wrap ? (i + 1) % n : min(i + 1, n - 1)];
823
+
824
+ // direction from previous to current
825
+ const dx1 = curr.x - prev.x;
826
+ const dy1 = curr.y - prev.y;
827
+ const len1 = (dx1*dx1 + dy1*dy1)**.5;
828
+
829
+ // direction from current to next
830
+ const dx2 = next.x - curr.x;
831
+ const dy2 = next.y - curr.y;
832
+ const len2 = (dx2*dx2 + dy2*dy2)**.5;
833
+
834
+ if (len1 < e && len2 < e)
835
+ continue; // skip degenerate point
836
+
837
+ // calculate perpendicular normals for each edge
838
+ const nx1 = len1 > e ? -dy1 / len1 : 0;
839
+ const ny1 = len1 > e ? dx1 / len1 : 0;
840
+ const nx2 = len2 > e ? -dy2 / len2 : 0;
841
+ const ny2 = len2 > e ? dx2 / len2 : 0;
842
+
843
+ // average the normals for miter
844
+ let nx = nx1 + nx2;
845
+ let ny = ny1 + ny2;
846
+ const nlen = (nx*nx + ny*ny)**.5;
847
+ if (nlen < e)
848
+ {
849
+ // 180 degree turn - use perpendicular
850
+ nx = nx1;
851
+ ny = ny1;
852
+ }
853
+ else
854
+ {
855
+ // calculate miter length
856
+ nx /= nlen;
857
+ ny /= nlen;
858
+ const dot = nx1 * nx + ny1 * ny;
859
+ if (dot > e)
860
+ {
861
+ // scale normal by miter length, clamped to miterLimit
862
+ const miterLength = min(1 / dot, miterLimit);
863
+ nx *= miterLength;
864
+ ny *= miterLength;
865
+ }
866
+ }
867
+
868
+ // create inner and outer points along the normal
869
+ const inner = vec2(curr.x - nx * halfWidth, curr.y - ny * halfWidth);
870
+ const outer = vec2(curr.x + nx * halfWidth, curr.y + ny * halfWidth);
871
+ strip.push(inner);
872
+ strip.push(outer);
873
+ }
874
+ if (strip.length > 1 && wrap)
875
+ {
876
+ // close the loop
877
+ strip.push(strip[0]);
878
+ strip.push(strip[1]);
879
+ }
880
+ return strip;
881
+ }
882
+
883
+ // WebGL internal function to convert polys to tri strips
884
+ function glPolyStrip(points)
885
+ {
886
+ // validate input
887
+ if (points.length < 3)
888
+ return [];
889
+
890
+ // cross product helper: (b-a) x (c-a)
891
+ const cross = (a,b,c)=> (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
892
+
893
+ // calculate signed area of polygon
894
+ const signedArea = (poly)=>
895
+ {
896
+ let area = 0;
897
+ for (let i = poly.length; i--;)
898
+ {
899
+ const j = (i+1) % poly.length;
900
+ area += poly[i].cross(poly[j]);
901
+ }
902
+ return area;
903
+ }
904
+
905
+ // ensure counter-clockwise winding (slice first so we don't mutate caller's array)
906
+ if (signedArea(points) < 0)
907
+ points = points.slice().reverse();
908
+
909
+ // check if point is inside triangle
910
+ const e = 1e-9;
911
+ const pointInTriangle = (p, a, b, c)=>
912
+ {
913
+ const c1 = cross(a, b, p);
914
+ const c2 = cross(b, c, p);
915
+ const c3 = cross(c, a, p);
916
+ const negative = (c1<-e?1:0) + (c2<-e?1:0) + (c3<-e?1:0);
917
+ const positive = (c1> e?1:0) + (c2> e?1:0) + (c3> e?1:0);
918
+ return !(negative && positive);
919
+ };
920
+
921
+ // ear clipping triangulation
922
+ const indices = [];
923
+ for (let i = 0; i < points.length; ++i)
924
+ indices[i] = i;
925
+ const triangles = [];
926
+ let attempts = 0;
927
+ const maxAttempts = points.length ** 2 + 100;
928
+ while (indices.length > 3 && attempts++ < maxAttempts)
929
+ {
930
+ let foundEar = false;
931
+ for (let i = 0; i < indices.length; i++)
932
+ {
933
+ const i0 = indices[(i + indices.length - 1) % indices.length];
934
+ const i1 = indices[i];
935
+ const i2 = indices[(i + 1) % indices.length];
936
+ const a = points[i0], b = points[i1], c = points[i2];
937
+
938
+ // check if convex
939
+ if (cross(a, b, c) < e) continue;
940
+
941
+ // check if any other point is inside
942
+ let hasInside = false;
943
+ for (let j = 0; j < indices.length; j++)
944
+ {
945
+ const k = indices[j];
946
+ if (k === i0 || k === i1 || k === i2) continue;
947
+
948
+ const p = points[k];
949
+ hasInside = pointInTriangle(p, a, b, c);
950
+ if (hasInside) break;
951
+ }
952
+ if (hasInside) continue;
953
+
954
+ // found valid ear
955
+ triangles.push([i0, i1, i2]);
956
+ indices.splice(i, 1);
957
+ foundEar = true;
958
+ break;
959
+ }
960
+
961
+ // fallback for degenerate cases
962
+ if (!foundEar)
963
+ {
964
+ let worstIndex = -1, worstValue = Infinity;
965
+ for (let i = 0; i < indices.length; i++)
966
+ {
967
+ const i0 = indices[(i + indices.length - 1) % indices.length];
968
+ const i1 = indices[i];
969
+ const i2 = indices[(i + 1) % indices.length];
970
+ const value = abs(cross(points[i0], points[i1], points[i2]));
971
+ if (value < worstValue)
972
+ {
973
+ worstValue = value;
974
+ worstIndex = i;
975
+ }
976
+ }
977
+ if (worstIndex < 0) break;
978
+
979
+ const i0 = indices[(worstIndex + indices.length - 1) % indices.length];
980
+ const i1 = indices[worstIndex];
981
+ const i2 = indices[(worstIndex + 1) % indices.length];
982
+ triangles.push([i0, i1, i2]);
983
+ indices.splice(worstIndex, 1);
984
+ }
985
+ }
986
+
987
+ // add final triangle
988
+ if (indices.length === 3)
989
+ triangles.push([indices[0], indices[1], indices[2]]);
990
+ if (!triangles.length)
991
+ return [];
992
+
993
+ // convert triangles to triangle strip with degenerate connectors
994
+ const strip = [];
995
+ let [a0, b0, c0] = triangles[0];
996
+ strip.push(points[a0], points[b0], points[c0]);
997
+ for (let i = 1; i < triangles.length; i++)
998
+ {
999
+ // add degenerate bridge from last vertex to first of new triangle
1000
+ const [a, b, c] = triangles[i];
1001
+ strip.push(points[c0], points[a]);
1002
+ strip.push(points[a], points[b], points[c]);
1003
+ c0 = c;
1004
+ }
1005
+ return strip;
942
1006
  }