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.
- package/FAQ.md +841 -820
- package/README.md +29 -25
- package/dist/littlejs.d.ts +2857 -955
- package/dist/littlejs.esm.js +33428 -20995
- package/dist/littlejs.esm.min.js +4 -3
- package/dist/littlejs.js +33386 -20996
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +27841 -20422
- package/package.json +10 -5
- package/plugins/audioEffects.js +28 -24
- package/plugins/box2d.js +2516 -2073
- package/plugins/drawUtilities.js +206 -31
- package/plugins/gltf.js +786 -0
- package/plugins/jsconfig.json +19 -0
- package/plugins/lightSystem.js +719 -332
- package/plugins/math3d.js +148 -27
- package/plugins/medalSystem.js +326 -280
- package/plugins/newgrounds.js +400 -199
- package/plugins/pathFinder.js +208 -175
- package/plugins/pluginExport.js +25 -0
- package/plugins/postProcess.js +96 -24
- package/plugins/render3d.js +2067 -1632
- package/plugins/render3dDebug.js +110 -0
- package/plugins/render3dEditor.js +1812 -0
- package/plugins/render3dExtras.js +1948 -0
- package/plugins/render3dLevel.js +189 -0
- package/plugins/render3dVoxels.js +514 -0
- package/plugins/sceneSystem.js +58 -0
- package/plugins/textureSheet.js +496 -458
- package/plugins/threejs.js +183 -163
- package/plugins/tweakables.js +411 -0
- package/plugins/tweenSystem.js +265 -145
- package/plugins/uiSystem.js +2024 -1687
- package/plugins/zzfxm.js +159 -159
- package/src/engine.js +378 -97
- package/src/engineAudio.js +231 -127
- package/src/engineBuild.mjs +292 -256
- package/src/engineDebug.js +967 -743
- package/src/engineDraw.js +391 -198
- package/src/engineEditor.js +2452 -0
- package/src/engineExport.js +18 -0
- package/src/engineInput.js +1466 -1293
- package/src/engineLogo.js +19 -0
- package/src/engineMath.js +1274 -1252
- package/src/engineObject.js +197 -82
- package/src/engineParticles.js +583 -532
- package/src/engineRelease.js +79 -45
- package/src/engineSettings.js +115 -29
- package/src/engineTileLayer.js +995 -741
- package/src/engineUtilities.js +19 -13
- package/src/engineWebGL.js +239 -82
- package/src/jsconfig.json +17 -13
- package/plugins/box2d.wasm.js +0 -630
- package/plugins/box2d.wasm.wasm +0 -0
package/src/engineWebGL.js
CHANGED
|
@@ -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
|
|
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
|
|
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 =
|
|
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
|
-
'
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
336
|
-
if (
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
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
|
|
360
|
-
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_S,
|
|
361
|
-
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_T,
|
|
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);
|
|
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
|
-
|
|
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
|
|
465
|
-
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_S,
|
|
466
|
-
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_T,
|
|
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
|
|
499
|
-
if (
|
|
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
|
-
/**
|
|
585
|
+
/** Internal: tells WebGL to create or update the glTexture and start tracking it, TextureInfo calls it
|
|
507
586
|
* @param {TextureInfo} textureInfo
|
|
508
|
-
* @
|
|
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
|
-
/**
|
|
604
|
+
/** Internal: tells WebGL to destroy the glTexture and stop tracking it, TextureInfo calls it
|
|
526
605
|
* @param {TextureInfo} textureInfo
|
|
527
|
-
* @
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
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
|
+
}
|