littlejsengine 1.19.3 → 1.21.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 (54) hide show
  1. package/FAQ.md +841 -820
  2. package/README.md +29 -25
  3. package/dist/littlejs.d.ts +2857 -955
  4. package/dist/littlejs.esm.js +33428 -20995
  5. package/dist/littlejs.esm.min.js +4 -3
  6. package/dist/littlejs.js +33386 -20996
  7. package/dist/littlejs.min.js +2 -2
  8. package/dist/littlejs.release.js +27841 -20422
  9. package/package.json +10 -5
  10. package/plugins/audioEffects.js +28 -24
  11. package/plugins/box2d.js +2516 -2073
  12. package/plugins/drawUtilities.js +206 -31
  13. package/plugins/gltf.js +786 -0
  14. package/plugins/jsconfig.json +19 -0
  15. package/plugins/lightSystem.js +719 -332
  16. package/plugins/math3d.js +148 -27
  17. package/plugins/medalSystem.js +326 -280
  18. package/plugins/newgrounds.js +400 -199
  19. package/plugins/pathFinder.js +208 -175
  20. package/plugins/pluginExport.js +25 -0
  21. package/plugins/postProcess.js +96 -24
  22. package/plugins/render3d.js +2067 -1632
  23. package/plugins/render3dDebug.js +110 -0
  24. package/plugins/render3dEditor.js +1812 -0
  25. package/plugins/render3dExtras.js +1948 -0
  26. package/plugins/render3dLevel.js +189 -0
  27. package/plugins/render3dVoxels.js +514 -0
  28. package/plugins/sceneSystem.js +58 -0
  29. package/plugins/textureSheet.js +496 -458
  30. package/plugins/threejs.js +183 -163
  31. package/plugins/tweakables.js +411 -0
  32. package/plugins/tweenSystem.js +265 -145
  33. package/plugins/uiSystem.js +2024 -1687
  34. package/plugins/zzfxm.js +159 -159
  35. package/src/engine.js +378 -97
  36. package/src/engineAudio.js +231 -127
  37. package/src/engineBuild.mjs +292 -256
  38. package/src/engineDebug.js +967 -743
  39. package/src/engineDraw.js +391 -198
  40. package/src/engineEditor.js +2452 -0
  41. package/src/engineExport.js +18 -0
  42. package/src/engineInput.js +1466 -1293
  43. package/src/engineLogo.js +19 -0
  44. package/src/engineMath.js +1274 -1252
  45. package/src/engineObject.js +197 -82
  46. package/src/engineParticles.js +583 -532
  47. package/src/engineRelease.js +79 -45
  48. package/src/engineSettings.js +115 -29
  49. package/src/engineTileLayer.js +995 -741
  50. package/src/engineUtilities.js +19 -13
  51. package/src/engineWebGL.js +239 -82
  52. package/src/jsconfig.json +17 -13
  53. package/plugins/box2d.wasm.js +0 -630
  54. package/plugins/box2d.wasm.wasm +0 -0
@@ -32,7 +32,19 @@ let glContext;
32
32
  let glAntialias = true;
33
33
 
34
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;
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;
36
48
 
37
49
  // WebGL internal constants
38
50
  const gl_ARRAY_BUFFER_SIZE = 5e5;
@@ -45,7 +57,6 @@ const gl_MAX_POLY_VERTEXES = gl_ARRAY_BUFFER_SIZE / gl_POLY_VERTEX_BYTE_STRIDE |
45
57
 
46
58
  ///////////////////////////////////////////////////////////////////////////////
47
59
 
48
- // Initialize WebGL, called automatically by the engine
49
60
  // the sprite vertex shader, shared by the engine's program and every Shader so one vertex layout fits all
50
61
  const gl_VERTEX_SOURCE =
51
62
  '#version 300 es\n' + // specify GLSL ES version
@@ -66,11 +77,19 @@ const gl_VERTEX_SOURCE =
66
77
  'l=g;d=c;e=a;'+ // pass local uv and colors to fragment shader
67
78
  '}'; // end of shader
68
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
69
91
  function glInit(rootElement)
70
92
  {
71
- // keep set of texture infos so they can be restored if context is lost
72
- glTextureInfos = new Set;
73
-
74
93
  if (!glEnable || headlessMode)
75
94
  {
76
95
  glCanBeEnabled = false;
@@ -93,12 +112,15 @@ function glInit(rootElement)
93
112
  // attach the WebGL canvas;
94
113
  rootElement.appendChild(glCanvas);
95
114
 
96
- // startup webgl
115
+ // startup webgl, and make the textures of any texture infos made before it
97
116
  initWebGL();
117
+ for (const info of glTextureInfos)
118
+ info.glTexture ||= glCreateTexture(info.image, info.wrap);
98
119
 
99
120
  // setup context lost and restore handlers
100
121
  glCanvas.addEventListener('webglcontextlost', (e)=>
101
122
  {
123
+ glEnableBeforeLoss = glEnable;
102
124
  glEnable = false; // disable WebGL rendering
103
125
  glCanvas.style.display = 'none'; // hide the gl canvas
104
126
  e.preventDefault(); // prevent default to allow restoration
@@ -113,7 +135,7 @@ function glInit(rootElement)
113
135
  shader.program = undefined;
114
136
  glBatchShader = undefined;
115
137
  glProgramCustom = true;
116
- glUniformLocations = new Map; // the programs those belonged to are gone
138
+ glUniformLocations = new WeakMap; // the programs those belonged to are gone
117
139
  // drop any partially-filled batch so the next glFlush doesn't
118
140
  // upload stale glBatchCount against fresh empty buffers on restore
119
141
  glBatchCount = 0;
@@ -122,15 +144,23 @@ function glInit(rootElement)
122
144
  });
123
145
  glCanvas.addEventListener('webglcontextrestored', ()=>
124
146
  {
125
- glEnable = true; // re-enable WebGL rendering
147
+ glEnable = glEnableBeforeLoss; // WebGL rendering as it was before the loss
126
148
  glCanvas.style.display = ''; // show the gl canvas
127
149
  LOG('WebGL context restored, reinitializing...');
128
150
 
129
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
130
156
  initWebGL();
131
157
  for (const info of glTextureInfos)
132
158
  info.glTexture = glCreateTexture(info.image, info.wrap);
133
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();
134
164
  });
135
165
 
136
166
  function initWebGL()
@@ -140,13 +170,16 @@ function glInit(rootElement)
140
170
  '#version 300 es\n' + // specify GLSL ES version
141
171
  'precision highp float;'+ // use highp for accuracy
142
172
  'uniform sampler2D s;'+ // texture
173
+ 'uniform bool premultipliedTexture;'+ // is the texture premultiplied, a render target or a smooth image
143
174
  'in vec2 v;'+ // in: uv
144
175
  'in vec4 d,e;'+ // in: color, additiveColor
145
176
  'out vec4 c;'+ // out: color
146
177
  'void main(){'+ // shader entry point
147
- 'c=texture(s,v)*d+e;'+ // modulate texture by color plus additive
178
+ 'vec4 t=texture(s,v);'+ // sample the texture
179
+ gl_FRAGMENT_TINT_SOURCE + // apply color and additive
148
180
  '}' // end of shader
149
181
  );
182
+ glShaderPremultiplied = false; // a new program starts with its uniforms at 0
150
183
 
151
184
  // setup poly rendering shaders
152
185
  glPolyShader = glCreateProgram(
@@ -203,7 +236,8 @@ function glInit(rootElement)
203
236
  // configure instanced vertex attributes
204
237
  offset = 0, shader = glShader, stride = gl_INSTANCE_BYTE_STRIDE;
205
238
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
206
- initVertexAttrib('g', glContext.FLOAT, 0, 2); // geometry
239
+ // geometry, its own packed buffer: typeSize 0 gives stride 0 and does not advance the offset
240
+ initVertexAttrib('g', glContext.FLOAT, 0, 2);
207
241
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
208
242
  glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
209
243
  initVertexAttrib('p', glContext.FLOAT, 4, 4, 1); // position & size
@@ -232,6 +266,7 @@ function glSetInstancedMode(force=false)
232
266
  glPolyMode = false;
233
267
  glContext.useProgram(glShader);
234
268
  glContext.bindVertexArray(glInstancedVAO);
269
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer); // a VAO does not keep this binding
235
270
  }
236
271
 
237
272
  function glSetPolyMode()
@@ -279,6 +314,8 @@ function glPreRender(clear=true)
279
314
  const p = vec2(-1).subtract(rotatedCam.multiply(s));
280
315
  const ca = cos(cameraAngle);
281
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
282
319
  const transform = [
283
320
  s.x * ca, s.y * sa, 0, 0,
284
321
  -s.x * sa, s.y * ca, 0, 0,
@@ -290,8 +327,7 @@ function glPreRender(clear=true)
290
327
  const initUniform = (program, uniform, value)=>
291
328
  {
292
329
  glContext.useProgram(program);
293
- const location = glContext.getUniformLocation(program, uniform);
294
- glContext.uniformMatrix4fv(location, false, value);
330
+ glContext.uniformMatrix4fv(glUniformLocation(program, uniform), false, value);
295
331
  }
296
332
  initUniform(glPolyShader, 'm', transform);
297
333
  initUniform(glShader, 'm', transform);
@@ -314,15 +350,15 @@ function glPreRender(clear=true)
314
350
  glSetInstancedMode(true);
315
351
  }
316
352
 
317
- /** Clear the canvas and setup the viewport
353
+ /** Clear the canvas or render target to canvasClearColor
318
354
  * @memberof WebGL */
319
355
  function glClearCanvas()
320
356
  {
321
357
  if (!glContext) return;
322
358
 
323
- // clear using the canvasClearColor
359
+ // clear using the canvasClearColor, premultiplied like everything the blend writes
324
360
  const color = canvasClearColor;
325
- glContext.clearColor(color.r, color.g, color.b, color.a);
361
+ glContext.clearColor(color.r*color.a, color.g*color.a, color.b*color.a, color.a);
326
362
  glContext.clear(glContext.COLOR_BUFFER_BIT);
327
363
  }
328
364
 
@@ -332,18 +368,36 @@ function glClearCanvas()
332
368
  * @memberof WebGL */
333
369
  function glSetTexture(texture)
334
370
  {
335
- // must flush cache with the old texture to set a new one
336
- if (!glContext || texture === glActiveTexture) return;
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
+ }
337
381
 
338
- glFlush();
339
- glActiveTexture = texture;
340
- glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
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);
341
388
  }
342
389
 
343
- /** Set the wrap mode (REPEAT or CLAMP_TO_EDGE) on an existing WebGL texture
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
344
397
  * Flushes the current batch only if the texture is the active one
345
398
  * @param {WebGLTexture} texture
346
- * @param {boolean} [wrap] - true for REPEAT, false for CLAMP_TO_EDGE
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]
347
401
  * @memberof WebGL */
348
402
  function glSetTextureWrap(texture, wrap=true)
349
403
  {
@@ -356,9 +410,9 @@ function glSetTextureWrap(texture, wrap=true)
356
410
  else
357
411
  glContext.bindTexture(glContext.TEXTURE_2D, texture);
358
412
 
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);
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);
362
416
 
363
417
  if (!isCurrent && glActiveTexture)
364
418
  glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
@@ -423,16 +477,17 @@ function glShaderProgram(shader)
423
477
  'uniform sampler2D iChannel0;' + // the texture
424
478
  'uniform vec3 iResolution;' + // canvas size in pixels
425
479
  'uniform float iTime;' + // engine time
480
+ 'uniform bool premultipliedTexture;' + // is the texture premultiplied, a render target or a smooth image
426
481
  'in vec2 v,l;in vec4 d,e;out vec4 c;\n' + // a define needs its own line
427
482
  '#define localUV l\n' +
428
483
  shader.fragmentCode + '\n' +
429
- 'void main(){vec4 t;mainImage(t,v);c=t*d+e;}');
484
+ 'void main(){vec4 t;mainImage(t,v);' + gl_FRAGMENT_TINT_SOURCE + '}');
430
485
  }
431
486
 
432
487
  /** Create WebGL texture from an image and init the texture settings
433
488
  * Restores the active texture when done
434
- * @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas} [image]
435
- * @param {boolean} [wrap] - true for REPEAT, false for CLAMP_TO_EDGE
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
436
491
  * @return {WebGLTexture}
437
492
  * @memberof WebGL */
438
493
  function glCreateTexture(image, wrap=false)
@@ -446,7 +501,10 @@ function glCreateTexture(image, wrap=false)
446
501
  {
447
502
  glSetTextureData(texture, image);
448
503
  glContext.bindTexture(glContext.TEXTURE_2D, texture);
449
- mipMap = !tilesPixelated && isPowerOfTwo(image.width) && isPowerOfTwo(image.height);
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);
450
508
  }
451
509
  else
452
510
  {
@@ -461,11 +519,14 @@ function glCreateTexture(image, wrap=false)
461
519
  const minFilter = mipMap ? glContext.LINEAR_MIPMAP_LINEAR : magFilter;
462
520
  glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MAG_FILTER, magFilter);
463
521
  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);
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);
467
525
  if (mipMap)
526
+ {
468
527
  glContext.generateMipmap(glContext.TEXTURE_2D);
528
+ glMipmappedTextures.add(texture);
529
+ }
469
530
 
470
531
  // rebind active texture
471
532
  glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
@@ -484,28 +545,46 @@ function glDeleteTexture(texture)
484
545
 
485
546
  /** Set WebGL texture data from an image, restores the active texture when done
486
547
  * @param {WebGLTexture} texture
487
- * @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas} image
548
+ * @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap} image
488
549
  * @memberof WebGL */
489
550
  function glSetTextureData(texture, image)
490
551
  {
491
552
  if (!glContext) return;
492
553
 
493
- // build the texture
554
+ // build the texture, after drawing what was queued with the old image
494
555
  ASSERT(image?.width > 0, 'Invalid image data.');
556
+ texture === glActiveTexture && glFlush();
495
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);
496
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
497
576
 
498
- // keep mipmaps in sync with new level 0 data (same condition as glCreateTexture)
499
- if (!tilesPixelated && isPowerOfTwo(image.width) && isPowerOfTwo(image.height))
577
+ // keep mipmaps in sync with new level 0 data, for any texture that has them
578
+ if (glMipmappedTextures.has(texture))
500
579
  glContext.generateMipmap(glContext.TEXTURE_2D);
501
580
 
502
581
  // rebind active texture
503
582
  glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
504
583
  }
505
584
 
506
- /** Tells WebGL to create or update the glTexture and start tracking it
585
+ /** Internal: tells WebGL to create or update the glTexture and start tracking it, TextureInfo calls it
507
586
  * @param {TextureInfo} textureInfo
508
- * @memberof WebGL */
587
+ * @ignore */
509
588
  function glRegisterTextureInfo(textureInfo)
510
589
  {
511
590
  if (headlessMode) return;
@@ -522,9 +601,9 @@ function glRegisterTextureInfo(textureInfo)
522
601
  textureInfo.glTexture = glCreateTexture(textureInfo.image, textureInfo.wrap);
523
602
  }
524
603
 
525
- /** Tells WebGL to destroy the glTexture and stop tracking it
604
+ /** Internal: tells WebGL to destroy the glTexture and stop tracking it, TextureInfo calls it
526
605
  * @param {TextureInfo} textureInfo
527
- * @memberof WebGL */
606
+ * @ignore */
528
607
  function glUnregisterTextureInfo(textureInfo)
529
608
  {
530
609
  if (headlessMode) return;
@@ -532,9 +611,16 @@ function glUnregisterTextureInfo(textureInfo)
532
611
  // delete texture info from tracking list even if gl is not enabled
533
612
  glTextureInfos.delete(textureInfo);
534
613
 
535
- // unset and destroy the texture
614
+ // unset and destroy the texture, drawing what is batched with it first, since deleting unbinds it
536
615
  const glTexture = textureInfo.glTexture;
537
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);
538
624
  glDeleteTexture(glTexture);
539
625
  }
540
626
 
@@ -544,9 +630,14 @@ function glFlush()
544
630
  {
545
631
  if (glEnable && glContext && glBatchCount)
546
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
+
547
637
  // set blend mode
638
+ const sourceBlend = premultiplied ? glContext.ONE : glContext.SRC_ALPHA;
548
639
  const destBlend = glBatchAdditive ? glContext.ONE : glContext.ONE_MINUS_SRC_ALPHA;
549
- glContext.blendFuncSeparate(glContext.SRC_ALPHA, destBlend, glContext.ONE, destBlend);
640
+ glContext.blendFuncSeparate(sourceBlend, destBlend, glContext.ONE, destBlend);
550
641
  glContext.enable(glContext.BLEND);
551
642
 
552
643
  // a Shader's program for this batch, or the engine's own again after one
@@ -560,10 +651,20 @@ function glFlush()
560
651
  const uniform = (name)=> glUniformLocation(program, name);
561
652
  glContext.uniformMatrix4fv(uniform('m'), false, glTransform);
562
653
  glContext.uniform1f(uniform('iTime'), time);
563
- glContext.uniform3f(uniform('iResolution'), glCanvas.width, glCanvas.height, 1);
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);
564
659
  }
565
660
  }
566
-
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
+
567
668
  const byteLength = glBatchCount *
568
669
  (glPolyMode ? gl_INDICES_PER_POLY_VERTEX : gl_INDICES_PER_INSTANCE);
569
670
  glContext.bufferSubData(glContext.ARRAY_BUFFER, 0, glPositionData, 0, byteLength);
@@ -581,7 +682,7 @@ function glFlush()
581
682
  glBatchShader = glCustomShader;
582
683
  }
583
684
 
584
- /** Flush any sprites still in the buffer and copy to main canvas
685
+ /** Flush any sprites still in the buffer and copy the WebGL canvas to a 2D context
585
686
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
586
687
  * @memberof WebGL */
587
688
  function glCopyToContext(context)
@@ -631,8 +732,8 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
631
732
  glPositionData[offset++] = uv0Y;
632
733
  glPositionData[offset++] = uv1X;
633
734
  glPositionData[offset++] = uv1Y;
634
- glColorData[offset++] = rgba;
635
- glColorData[offset++] = rgbaAdditive;
735
+ glColorData[offset++] = rgba & glColorMask;
736
+ glColorData[offset++] = rgbaAdditive & glColorMask | glColorAdditive;
636
737
  glPositionData[offset++] = angle;
637
738
  }
638
739
 
@@ -678,7 +779,7 @@ function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
678
779
  glDrawPoints(drawPoints, rgba);
679
780
  }
680
781
 
681
- /** Transform and add a polygon to the gl draw list
782
+ /** Transform and add a polygon's outline to the gl draw list
682
783
  * @param {Array<Vector2>} points - Array of Vector2 points
683
784
  * @param {number} rgba - Color of the polygon as a 32-bit integer
684
785
  * @param {number} lineWidth - Width of the outline
@@ -700,36 +801,18 @@ function glDrawOutlineTransform(points, rgba, lineWidth, x, y, sx, sy, angle, wr
700
801
  * @param {number} rgba - Color as a 32-bit integer
701
802
  * @memberof WebGL */
702
803
  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
- }
804
+ { glDrawPolyStrip(points, rgba); }
727
805
 
728
806
  /** Add a list of colored points to the gl draw list
729
807
  * @param {Array<Vector2>} points - Array of Vector2 points in tri strip order
730
808
  * @param {Array<number>} pointColors - Array of 32-bit integer colors
731
809
  * @memberof WebGL */
732
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)
733
816
  {
734
817
  if (!glEnable || points.length < 3)
735
818
  return; // needs at least 3 points to have area
@@ -741,29 +824,46 @@ function glDrawColoredPoints(points, pointColors)
741
824
  ASSERT(vertCount < gl_MAX_POLY_VERTEXES, 'poly exceeds max batch size');
742
825
  if (vertCount >= gl_MAX_POLY_VERTEXES) return; // release-build safety net
743
826
  glSetPolyMode();
744
-
827
+
745
828
  // setup triangle strip with degenerate verts at start and end
746
829
  let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
747
830
  for (let i = vertCount; i--;)
748
831
  {
749
832
  const j = clamp(i-1, 0, vertCount-3);
750
833
  const point = points[j];
751
- const color = pointColors[j];
834
+ const color = pointColors ? pointColors[j] : rgba;
752
835
  glPositionData[offset++] = point.x;
753
836
  glPositionData[offset++] = point.y;
754
- glColorData[offset++] = color;
837
+ glColorData[offset++] = color & glColorMask | glColorAdditive;
755
838
  }
756
839
  glBatchCount += vertCount;
757
840
  }
758
841
 
759
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
760
844
  * @param {WebGLTexture} [texture] - a texture or undefined to use normal glCanvas
761
- * @param {boolean} [clear] - should the render target be cleared
845
+ * @param {boolean} [clear] - should the render target be cleared, to canvasClearColor, CLEAR_BLACK for a transparent one
762
846
  * @memberof WebGL */
763
847
  function glSetRenderTarget(texture, clear=false)
764
848
  {
849
+ // what was batched so far draws where it was meant to, before the target changes
850
+ glFlush();
851
+ const previousTarget = glRenderTarget;
765
852
  if (texture)
766
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]);
767
867
  glRenderTarget = texture;
768
868
  glContext.bindFramebuffer(glContext.FRAMEBUFFER, glFramebuffer);
769
869
  glContext.framebufferTexture2D(glContext.FRAMEBUFFER,
@@ -772,17 +872,49 @@ function glSetRenderTarget(texture, clear=false)
772
872
  }
773
873
  else
774
874
  {
775
- glFlush();
776
875
  glRenderTarget = undefined;
777
- glContext.bindFramebuffer(glContext.FRAMEBUFFER, null);
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);
778
888
 
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);
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
+ }
782
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);
783
913
  }
784
914
 
785
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
786
918
  * @param {number} x
787
919
  * @param {number} y
788
920
  * @param {number} width
@@ -791,6 +923,7 @@ function glSetRenderTarget(texture, clear=false)
791
923
  function glClearRect(x, y, width, height)
792
924
  {
793
925
  if (!glEnable) return;
926
+ glFlush(); // tiles batched before the clear go down before it, not over what replaces them
794
927
 
795
928
  // Enable scissor test to clear only the specified area
796
929
  glContext.enable(glContext.SCISSOR_TEST);
@@ -906,8 +1039,32 @@ function glPolyStrip(points)
906
1039
  if (signedArea(points) < 0)
907
1040
  points = points.slice().reverse();
908
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
+
909
1067
  // check if point is inside triangle
910
- const e = 1e-9;
911
1068
  const pointInTriangle = (p, a, b, c)=>
912
1069
  {
913
1070
  const c1 = cross(a, b, p);
package/src/jsconfig.json CHANGED
@@ -1,13 +1,17 @@
1
- {
2
- "compilerOptions": {
3
- "checkJs": true,
4
- "target": "ES2020",
5
- "module": "esnext",
6
- "noImplicitAny": false,
7
- "strict": false
8
- },
9
- "exclude": [
10
- "engineRelease.js",
11
- "engineExport.js"
12
- ]
13
- }
1
+ {
2
+ "compilerOptions": {
3
+ "checkJs": true,
4
+ "target": "ES2020",
5
+ "module": "esnext",
6
+ "noImplicitAny": false,
7
+ "strict": false
8
+ },
9
+ "include": [
10
+ "*.js",
11
+ "../plugins/math3d.js"
12
+ ],
13
+ "exclude": [
14
+ "engineRelease.js",
15
+ "engineExport.js"
16
+ ]
17
+ }