littlejsengine 1.25.0 → 1.26.1
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/CHANGELOG.md +99 -0
- package/FAQ.md +49 -11
- package/README.md +6 -2
- package/REFERENCE.md +129 -21
- package/dist/littlejs.d.ts +487 -102
- package/dist/littlejs.esm.js +2898 -719
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +2888 -716
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +2702 -527
- package/package.json +4 -4
- package/plugins/box2d.js +110 -34
- package/plugins/gltf.js +74 -22
- package/plugins/lightSystem.js +410 -28
- package/plugins/math3d.js +4 -2
- package/plugins/medalSystem.js +19 -7
- package/plugins/newgrounds.js +4 -2
- package/plugins/parallax.js +1 -0
- package/plugins/particleEffects.js +44 -10
- package/plugins/pathFinder.js +9 -11
- package/plugins/pluginExport.js +9 -0
- package/plugins/postProcess.js +9 -3
- package/plugins/render3d.js +499 -2389
- package/plugins/render3dDebug.js +5 -8
- package/plugins/render3dEditor.js +81 -6
- package/plugins/render3dExtras.js +103 -31
- package/plugins/render3dLevel.js +53 -5
- package/plugins/render3dMesh.js +1158 -0
- package/plugins/render3dObject.js +1451 -0
- package/plugins/render3dVoxels.js +15 -3
- package/plugins/textureSheet.js +9 -8
- package/plugins/threejs.js +1 -1
- package/plugins/tweenSystem.js +76 -51
- package/plugins/uiSystem.js +37 -8
- package/plugins/wavedash.js +229 -0
- package/src/engine.js +199 -40
- package/src/engineAudio.js +37 -21
- package/src/engineBuild.mjs +6 -1
- package/src/engineDebug.js +22 -162
- package/src/engineDraw.js +142 -34
- package/src/engineEditor.js +79 -18
- package/src/engineExport.js +1 -3
- package/src/engineInput.js +34 -24
- package/src/engineMath.js +35 -14
- package/src/engineObject.js +60 -41
- package/src/engineParticles.js +58 -11
- package/src/engineRelease.js +1 -5
- package/src/engineSettings.js +48 -9
- package/src/engineTileLayer.js +11 -4
- package/src/engineUtilities.js +41 -9
- package/src/engineWebGL.js +37 -13
package/plugins/lightSystem.js
CHANGED
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
* setShadowTransparent lets its color tint the light
|
|
23
23
|
* - Set light.glow for a soft hazy glow over a light, like a lamp at night; it is added over the lit scene after the
|
|
24
24
|
* lightmap, so it shows in the dark and sits in front of everything there
|
|
25
|
+
* - A DirectionalLight is a sun: one per scene, it lights everything from one direction, foreground casters throw long
|
|
26
|
+
* shadows and objects with castBackgroundShadow are lit only at their edges facing it
|
|
25
27
|
* - Must be constructed BEFORE PostProcessPlugin so post-process sees lit pixels
|
|
26
28
|
* @namespace LightSystem
|
|
27
29
|
*/
|
|
@@ -48,7 +50,7 @@ let lightSystem;
|
|
|
48
50
|
*/
|
|
49
51
|
class LightSystemPlugin
|
|
50
52
|
{
|
|
51
|
-
/** Create the global light system plugin
|
|
53
|
+
/** Create the global light system plugin
|
|
52
54
|
* @param {Vector2} [textureSize] - Size of the lightmap texture (defaults to following mainCanvasSize, which is css pixels, so the lightmap is not scaled by canvasPixelRatio; pass mainCanvasSize.scale(getCanvasPixelRatio()) for a full resolution lightmap)
|
|
53
55
|
* @param {Color} [ambientColor] - Color applied to unlit areas of the scene (defaults to BLACK = pitch dark). Set a small RGB like rgb(0.1,0.1,0.15) for a faint "moonlight" baseline so unlit areas aren't fully black.
|
|
54
56
|
* @example
|
|
@@ -64,6 +66,7 @@ class LightSystemPlugin
|
|
|
64
66
|
|
|
65
67
|
/** @property {boolean} - When false, the render pass is skipped entirely */
|
|
66
68
|
this.enabled = true;
|
|
69
|
+
this.shadersFailed = false; // a shader did not build on this device, so the system draws nothing
|
|
67
70
|
/** @property {Color} - Baseline color applied to unlit areas of the scene. Defaults to BLACK (pitch dark). Set to a small RGB for a faint ambient. The lightmap is cleared to this color each frame, then lights add on top, then the result multiplies the scene. */
|
|
68
71
|
this.ambientColor = (ambientColor || BLACK).copy();
|
|
69
72
|
/** @property {Vector2} - Size of the lightmap texture, follows mainCanvasSize (css pixels, so it is not scaled by canvasPixelRatio) unless a size was passed */
|
|
@@ -109,6 +112,10 @@ class LightSystemPlugin
|
|
|
109
112
|
/** @property {WebGLTexture|undefined} - The shadow map, casters drawn black on white around the camera, read only
|
|
110
113
|
* @type {WebGLTexture|undefined} */
|
|
111
114
|
this.shadowMap = undefined;
|
|
115
|
+
/** @property {WebGLTexture|undefined} - The background map, objects with castBackgroundShadow drawn black on
|
|
116
|
+
* white, the shadow map's size and place, made by a directional light when something casts into it and kept while the light is, read only
|
|
117
|
+
* @type {WebGLTexture|undefined} */
|
|
118
|
+
this.backgroundMap = undefined;
|
|
112
119
|
/** @property {WebGLTexture|undefined} - One of the two textures each light's shadow is built in
|
|
113
120
|
* @type {WebGLTexture|undefined} */
|
|
114
121
|
this.shadowTextureA = undefined;
|
|
@@ -134,9 +141,34 @@ class LightSystemPlugin
|
|
|
134
141
|
this.shadowMapOrigin = vec2();
|
|
135
142
|
/** @property {number} - World size the shadow map covers, set each shadow pass */
|
|
136
143
|
this.shadowMapWorldSize = 0;
|
|
144
|
+
/** @property {DirectionalLight|undefined} - The scene's directional light, a sun, or undefined, read only
|
|
145
|
+
* @type {DirectionalLight|undefined} */
|
|
146
|
+
this.directionalLight = undefined;
|
|
147
|
+
/** @property {number} - Pixels across the square textures a directional light is built in, covering the
|
|
148
|
+
* shadow map's area; larger is sharper and slower; a power of two, as shadowMapSize, so their texels line up */
|
|
149
|
+
this.directionalTextureSize = 512;
|
|
150
|
+
/** @property {WebGLTexture|undefined} - The directional light as built this frame, white where it reaches,
|
|
151
|
+
* over the shadow map's area, read only
|
|
152
|
+
* @type {WebGLTexture|undefined} */
|
|
153
|
+
this.directionalTexture = undefined;
|
|
154
|
+
/** @type {WebGLTexture|undefined} */
|
|
155
|
+
this.directionalTextureA = undefined; // where it is built, ping ponged
|
|
156
|
+
/** @type {WebGLTexture|undefined} */
|
|
157
|
+
this.directionalTextureB = undefined;
|
|
158
|
+
this.directionalTextureSizeAllocated = 0;
|
|
159
|
+
this.backgroundCasters = false; // anything has castBackgroundShadow, checked each shadow pass
|
|
160
|
+
/** @type {Object<string, WebGLProgram|WebGLVertexArrayObject>} */
|
|
161
|
+
this.directionalPrograms = {}; // its programs and their vertex arrays, by name
|
|
137
162
|
this.shadowMapSizeAllocated = 0; // sizes the textures were made at, to remake them on a change
|
|
138
163
|
this.shadowTextureSizeAllocated = 0;
|
|
139
164
|
|
|
165
|
+
// a full texture quad, the vertex shader of the shadow and directional programs
|
|
166
|
+
const quadVertex =
|
|
167
|
+
'#version 300 es\n' +
|
|
168
|
+
'precision highp float;'+
|
|
169
|
+
'in vec2 p;'+ // unit quad [0..1]
|
|
170
|
+
'out vec2 uv;'+
|
|
171
|
+
'void main(){gl_Position=vec4(p+p-1.,1,1);uv=p;}';
|
|
140
172
|
initLightSystem();
|
|
141
173
|
engineAddPlugin(undefined, lightSystemRender,
|
|
142
174
|
lightSystemContextLost, lightSystemContextRestored);
|
|
@@ -206,6 +238,7 @@ class LightSystemPlugin
|
|
|
206
238
|
'if(useShadow)c.rgb*=texture(shadowTexture,vUV).rgb;'+
|
|
207
239
|
'}'
|
|
208
240
|
);
|
|
241
|
+
if (lightSystemShadersFailed(lightSystem.lightShader)) return;
|
|
209
242
|
// the shadow texture is on unit 1, the engine's tracked texture stays on unit 0
|
|
210
243
|
glContext.useProgram(lightSystem.lightShader);
|
|
211
244
|
glContext.uniform1i(glUniformLocation(lightSystem.lightShader, 'shadowTexture'), 1);
|
|
@@ -230,10 +263,23 @@ class LightSystemPlugin
|
|
|
230
263
|
'}'
|
|
231
264
|
);
|
|
232
265
|
|
|
266
|
+
if (lightSystemShadersFailed(lightSystem.compositeShader)) return;
|
|
267
|
+
|
|
233
268
|
// one quad VAO per program, the engine's unit triangle strip through the named attribute
|
|
234
269
|
lightSystem.lightVAO = createQuadVAO(lightSystem.lightShader, 'g');
|
|
235
270
|
lightSystem.compositeVAO = createQuadVAO(lightSystem.compositeShader, 'p');
|
|
236
271
|
}
|
|
272
|
+
// a program that did not build in a release build draws nothing, so the light system turns itself off, said
|
|
273
|
+
// once, and the scene draws as it would without it; true when it is off
|
|
274
|
+
function lightSystemShadersFailed(...programs)
|
|
275
|
+
{
|
|
276
|
+
if (!lightSystem.shadersFailed && programs.some((program)=> glFailedPrograms.has(program)))
|
|
277
|
+
{
|
|
278
|
+
console.error('LightSystemPlugin: its shaders did not build on this device, the scene draws without it');
|
|
279
|
+
lightSystem.shadersFailed = true;
|
|
280
|
+
}
|
|
281
|
+
return lightSystem.shadersFailed;
|
|
282
|
+
}
|
|
237
283
|
function createQuadVAO(program, attribute)
|
|
238
284
|
{
|
|
239
285
|
const gl = glContext, vao = gl.createVertexArray();
|
|
@@ -266,13 +312,8 @@ class LightSystemPlugin
|
|
|
266
312
|
// put back the texture the engine tracks
|
|
267
313
|
if (glActiveTexture)
|
|
268
314
|
gl.bindTexture(gl.TEXTURE_2D, glActiveTexture);
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
'#version 300 es\n' +
|
|
272
|
-
'precision highp float;'+
|
|
273
|
-
'in vec2 p;'+ // unit quad [0..1]
|
|
274
|
-
'out vec2 uv;'+
|
|
275
|
-
'void main(){gl_Position=vec4(p+p-1.,1,1);uv=p;}';
|
|
315
|
+
if (ls.shadowCopyShader)
|
|
316
|
+
return; // the programs are kept while the textures are made again
|
|
276
317
|
|
|
277
318
|
// copy: the shadow map around the light into the light's texture, light at the center
|
|
278
319
|
ls.shadowCopyShader = glCreateProgram(quadVertex,
|
|
@@ -317,6 +358,7 @@ class LightSystemPlugin
|
|
|
317
358
|
'c=vec4(min(texture(s,uv).rgb+brightness*mask*b,b),1);'+
|
|
318
359
|
'}');
|
|
319
360
|
|
|
361
|
+
if (lightSystemShadersFailed(ls.shadowCopyShader, ls.shadowStretchShader)) return;
|
|
320
362
|
ls.shadowCopyVAO = createQuadVAO(ls.shadowCopyShader, 'p');
|
|
321
363
|
ls.shadowStretchVAO = createQuadVAO(ls.shadowStretchShader, 'p');
|
|
322
364
|
|
|
@@ -326,22 +368,20 @@ class LightSystemPlugin
|
|
|
326
368
|
gl.useProgram(ls.shadowStretchShader);
|
|
327
369
|
gl.uniform1i(glUniformLocation(ls.shadowStretchShader, 's'), 1);
|
|
328
370
|
}
|
|
329
|
-
|
|
371
|
+
// let go of the shadow textures, the programs stay, small, so a new size or a sun made again compiles nothing
|
|
372
|
+
function freeShadowTextures()
|
|
330
373
|
{
|
|
331
374
|
const gl = glContext, ls = lightSystem;
|
|
332
375
|
gl.deleteTexture(ls.shadowMap);
|
|
333
376
|
gl.deleteTexture(ls.shadowTextureA);
|
|
334
377
|
gl.deleteTexture(ls.shadowTextureB);
|
|
335
|
-
gl.
|
|
336
|
-
|
|
337
|
-
gl.deleteVertexArray(ls.shadowCopyVAO);
|
|
338
|
-
gl.deleteVertexArray(ls.shadowStretchVAO);
|
|
339
|
-
clearShadows();
|
|
378
|
+
gl.deleteTexture(ls.backgroundMap);
|
|
379
|
+
ls.shadowMap = ls.shadowTextureA = ls.shadowTextureB = ls.backgroundMap = undefined;
|
|
340
380
|
}
|
|
341
381
|
function clearShadows()
|
|
342
382
|
{
|
|
343
383
|
const ls = lightSystem;
|
|
344
|
-
ls.shadowMap = ls.shadowTextureA = ls.shadowTextureB = undefined;
|
|
384
|
+
ls.shadowMap = ls.shadowTextureA = ls.shadowTextureB = ls.backgroundMap = undefined;
|
|
345
385
|
ls.shadowCopyShader = ls.shadowStretchShader = undefined;
|
|
346
386
|
ls.shadowCopyVAO = ls.shadowStretchVAO = undefined;
|
|
347
387
|
}
|
|
@@ -354,8 +394,9 @@ class LightSystemPlugin
|
|
|
354
394
|
if (!ls.shadowMap || ls.shadowMapSize !== ls.shadowMapSizeAllocated
|
|
355
395
|
|| ls.shadowTextureSize !== ls.shadowTextureSizeAllocated)
|
|
356
396
|
{
|
|
357
|
-
ls.shadowMap &&
|
|
397
|
+
ls.shadowMap && freeShadowTextures();
|
|
358
398
|
initShadows();
|
|
399
|
+
if (ls.shadersFailed) return;
|
|
359
400
|
}
|
|
360
401
|
|
|
361
402
|
// a square of world space around the camera, rounded to its own texels so the
|
|
@@ -363,7 +404,13 @@ class LightSystemPlugin
|
|
|
363
404
|
const size = ls.shadowMapSize;
|
|
364
405
|
const view = mainCanvasSize.scale(1/cameraScale);
|
|
365
406
|
const worldSize = ls.shadowMapScale * max(view.x, view.y);
|
|
366
|
-
|
|
407
|
+
// with a sun, on the coarser of its texels and the map's, or its area would move by half its texels and a
|
|
408
|
+
// still caster's shadow edge would jump as the camera pans
|
|
409
|
+
const grid = ls.directionalLight ? min(size, ls.directionalTextureSize) : size;
|
|
410
|
+
ASSERT(!ls.directionalLight || max(size, ls.directionalTextureSize) % grid === 0,
|
|
411
|
+
'with a DirectionalLight, shadowMapSize and directionalTextureSize must each be a whole multiple of the ' +
|
|
412
|
+
'other, as powers of two are, or the shadows shimmer as the camera pans', size, ls.directionalTextureSize);
|
|
413
|
+
const texel = worldSize / grid;
|
|
367
414
|
const center = vec2(floor(cameraPos.x/texel)*texel, floor(cameraPos.y/texel)*texel);
|
|
368
415
|
ls.shadowMapOrigin = center.subtract(vec2(worldSize/2));
|
|
369
416
|
ls.shadowMapWorldSize = worldSize;
|
|
@@ -377,19 +424,38 @@ class LightSystemPlugin
|
|
|
377
424
|
cameraScale = size / worldSize;
|
|
378
425
|
cameraAngle = 0;
|
|
379
426
|
canvasClearColor = WHITE;
|
|
380
|
-
glSetRenderTarget(ls.shadowMap, true);
|
|
381
427
|
glSkipScreenSpace = true; // the map's camera would put them anywhere
|
|
382
428
|
ls.shadowPass = true;
|
|
383
429
|
try
|
|
384
430
|
{
|
|
385
|
-
|
|
431
|
+
// the foreground map, then the background map while a directional light needs it, each cleared to
|
|
432
|
+
// white, every caster black with its alpha kept, set for each object since a render that left
|
|
433
|
+
// setShadowTransparent on ends with it
|
|
434
|
+
const drawMap = (target, casts)=>
|
|
386
435
|
{
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
436
|
+
glSetRenderTarget(target, true);
|
|
437
|
+
for (const o of engineObjects)
|
|
438
|
+
{
|
|
439
|
+
if (o.destroyed || !casts(o)) continue;
|
|
440
|
+
glColorMask = 0xff000000;
|
|
441
|
+
setShader(o.shader); // its own Shader as in the main pass, so a snippet that cuts holes casts the same shape
|
|
442
|
+
o.renderShadow();
|
|
443
|
+
}
|
|
444
|
+
glFlush();
|
|
445
|
+
};
|
|
446
|
+
if (ls.shadows || ls.directionalLight?.castShadow)
|
|
447
|
+
drawMap(ls.shadowMap, (o)=> o.castShadow); // nothing reads it for a sun that casts no shadows
|
|
448
|
+
// the background map only when something casts into it, most scenes have nothing there
|
|
449
|
+
ls.backgroundCasters = !!ls.directionalLight &&
|
|
450
|
+
engineObjects.some((o)=> !o.destroyed && o.castBackgroundShadow);
|
|
451
|
+
if (ls.backgroundCasters)
|
|
452
|
+
{
|
|
453
|
+
if (!ls.backgroundMap)
|
|
454
|
+
{
|
|
455
|
+
ls.backgroundMap = createTexture(size);
|
|
456
|
+
glActiveTexture && glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
|
|
457
|
+
}
|
|
458
|
+
drawMap(ls.backgroundMap, (o)=> o.castBackgroundShadow);
|
|
393
459
|
}
|
|
394
460
|
}
|
|
395
461
|
finally
|
|
@@ -402,9 +468,218 @@ class LightSystemPlugin
|
|
|
402
468
|
[drawContext, mainCanvasSize, cameraPos, cameraScale, cameraAngle, canvasClearColor, glCustomShader] = saved;
|
|
403
469
|
}
|
|
404
470
|
}
|
|
471
|
+
// the directional light's textures and programs, made when one is first drawn and again when the size changes
|
|
472
|
+
function initDirectionalTextures()
|
|
473
|
+
{
|
|
474
|
+
const gl = glContext, ls = lightSystem, size = ls.directionalTextureSize;
|
|
475
|
+
ls.directionalTexture = createTexture(size);
|
|
476
|
+
ls.directionalTextureA = createTexture(size);
|
|
477
|
+
ls.directionalTextureB = createTexture(size);
|
|
478
|
+
ls.directionalTextureSizeAllocated = size;
|
|
479
|
+
glActiveTexture && gl.bindTexture(gl.TEXTURE_2D, glActiveTexture);
|
|
480
|
+
}
|
|
481
|
+
function freeDirectionalTextures()
|
|
482
|
+
{
|
|
483
|
+
const gl = glContext, ls = lightSystem;
|
|
484
|
+
for (const texture of [ls.directionalTexture, ls.directionalTextureA, ls.directionalTextureB])
|
|
485
|
+
gl.deleteTexture(texture);
|
|
486
|
+
ls.directionalTexture = ls.directionalTextureA = ls.directionalTextureB = undefined;
|
|
487
|
+
ls.directionalTextureSizeAllocated = 0;
|
|
488
|
+
}
|
|
489
|
+
// its programs, made once, a new texture size keeps them
|
|
490
|
+
function initDirectional()
|
|
491
|
+
{
|
|
492
|
+
const gl = glContext, ls = lightSystem, p = ls.directionalPrograms;
|
|
493
|
+
|
|
494
|
+
// the maps are stored world up at v=0, the work textures world up at v=1, so a map is read at 1-v
|
|
495
|
+
const header = '#version 300 es\nprecision highp float;in vec2 uv;out vec4 c;';
|
|
496
|
+
|
|
497
|
+
// seed: the darkest of four taps across a work texel, so a caster thinner than a texel still darkens it;
|
|
498
|
+
// the foreground when useF is on, times the background when useB is
|
|
499
|
+
p.seed = glCreateProgram(quadVertex, header +
|
|
500
|
+
'uniform sampler2D f,b;uniform float useF,useB,tap;'+
|
|
501
|
+
'void main(){vec3 o=vec3(1);'+
|
|
502
|
+
'for(int i=0;i<4;i++){vec2 m=uv+tap*(vec2(i%2,i/2)*2.-1.);m.y=1.-m.y;'+
|
|
503
|
+
'vec3 v=mix(vec3(1),texture(f,m).rgb,useF)*mix(vec3(1),texture(b,m).rgb,useB);o=min(o,v);}'+
|
|
504
|
+
'c=vec4(o,1);}');
|
|
505
|
+
|
|
506
|
+
// long shadow pass: the darker of this texel and the one a shift upstream made lighter by fade, so a
|
|
507
|
+
// shadow lightens with its distance from the caster; past the area upstream is open
|
|
508
|
+
p.shadow = glCreateProgram(quadVertex, header +
|
|
509
|
+
'uniform sampler2D s;uniform vec2 shift;uniform float fade;'+
|
|
510
|
+
'void main(){vec2 u=uv-shift;'+
|
|
511
|
+
'vec3 b=u==clamp(u,0.,1.)?texture(s,u).rgb+fade:vec3(1);'+
|
|
512
|
+
'c=vec4(min(texture(s,uv).rgb,b),1);}');
|
|
513
|
+
|
|
514
|
+
// background leak pass, the long shadow pass turned around: the brighter of this texel and the light a
|
|
515
|
+
// shift upstream made dimmer by fade, so light comes into a background area from its edges facing the
|
|
516
|
+
// light and fades evenly to nothing by backgroundDepth; past the area upstream is open sky
|
|
517
|
+
p.leak = glCreateProgram(quadVertex, header +
|
|
518
|
+
'uniform sampler2D s;uniform vec2 shift;uniform float fade;'+
|
|
519
|
+
'void main(){vec2 u=uv-shift;'+
|
|
520
|
+
'vec3 b=(u==clamp(u,0.,1.)?texture(s,u).rgb:vec3(1))-fade;'+
|
|
521
|
+
'c=vec4(max(texture(s,uv).rgb,b),1);}');
|
|
522
|
+
|
|
523
|
+
// combine: long shadows times the leak, and a caster's texels take some of the light just upstream
|
|
524
|
+
// of them, a rim on its side facing the light
|
|
525
|
+
p.combine = glCreateProgram(quadVertex, header +
|
|
526
|
+
'uniform sampler2D s,t,f;uniform vec2 rim;uniform float useF,useT;'+
|
|
527
|
+
'void main(){vec3 l=texture(s,uv).rgb*mix(vec3(1),texture(t,uv).rgb,useT);'+
|
|
528
|
+
'vec2 u=uv-rim;vec3 up=u==clamp(u,0.,1.)?texture(s,u).rgb*mix(vec3(1),texture(t,u).rgb,useT):vec3(1);'+
|
|
529
|
+
'vec3 dark=1.-mix(vec3(1),texture(f,vec2(uv.x,1.-uv.y)).rgb,useF);'+
|
|
530
|
+
'c=vec4(max(l,.8*up*dark),1);}');
|
|
531
|
+
|
|
532
|
+
// add: a world space quad over the area, the built light in the light's color, into the lightmap
|
|
533
|
+
p.add = glCreateProgram(
|
|
534
|
+
'#version 300 es\nprecision highp float;uniform mat4 m;uniform vec2 origin;uniform float worldSize;'+
|
|
535
|
+
// three times the area, its edge texels carried on by the clamp, so the sun reaches the whole view however
|
|
536
|
+
// small the map or turned the camera
|
|
537
|
+
'in vec2 g;out vec2 uv;void main(){uv=g*3.-1.;gl_Position=m*vec4(origin+uv*worldSize,1,1);}',
|
|
538
|
+
header + 'uniform sampler2D s;uniform vec4 color;'+
|
|
539
|
+
'void main(){c=vec4(texture(s,uv).rgb*color.rgb*color.a,1);}');
|
|
540
|
+
|
|
541
|
+
if (lightSystemShadersFailed(p.seed, p.shadow, p.leak, p.combine, p.add)) return;
|
|
542
|
+
p.seedVAO = createQuadVAO(p.seed, 'p');
|
|
543
|
+
p.shadowVAO = createQuadVAO(p.shadow, 'p');
|
|
544
|
+
p.leakVAO = createQuadVAO(p.leak, 'p');
|
|
545
|
+
p.combineVAO = createQuadVAO(p.combine, 'p');
|
|
546
|
+
p.addVAO = createQuadVAO(p.add, 'g');
|
|
547
|
+
|
|
548
|
+
// samplers on units 1, 2 and 3, the engine's tracked texture on unit 0 stays as it is
|
|
549
|
+
const units = (program, names)=>
|
|
550
|
+
{
|
|
551
|
+
gl.useProgram(program);
|
|
552
|
+
names.forEach((name, i)=> gl.uniform1i(glUniformLocation(program, name), i + 1));
|
|
553
|
+
};
|
|
554
|
+
units(p.seed, ['f', 'b']);
|
|
555
|
+
units(p.shadow, ['s']);
|
|
556
|
+
units(p.leak, ['s']);
|
|
557
|
+
units(p.combine, ['s', 't', 'f']);
|
|
558
|
+
units(p.add, ['s']);
|
|
559
|
+
}
|
|
560
|
+
// let go of the textures a directional light made, its work textures and the background map; its programs
|
|
561
|
+
// stay, small, so a sun made again, as a day and night or a level with its own does, compiles nothing
|
|
562
|
+
function freeDirectional()
|
|
563
|
+
{
|
|
564
|
+
freeDirectionalTextures();
|
|
565
|
+
glContext.deleteTexture(lightSystem.backgroundMap);
|
|
566
|
+
lightSystem.backgroundMap = undefined;
|
|
567
|
+
lightSystem.backgroundCasters = false;
|
|
568
|
+
}
|
|
569
|
+
function clearDirectional()
|
|
570
|
+
{
|
|
571
|
+
const ls = lightSystem;
|
|
572
|
+
ls.directionalTexture = ls.directionalTextureA = ls.directionalTextureB = undefined;
|
|
573
|
+
ls.directionalPrograms = {};
|
|
574
|
+
ls.directionalTextureSizeAllocated = 0;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// build the directional light into directionalTexture, over the shadow map's area: long shadows from the
|
|
578
|
+
// foreground, times the background leak, with a rim on casters; leaves no framebuffer bound
|
|
579
|
+
function lightSystemDirectionalPass()
|
|
580
|
+
{
|
|
581
|
+
const gl = glContext, ls = lightSystem, light = ls.directionalLight;
|
|
582
|
+
if (!ls.directionalPrograms.seed)
|
|
583
|
+
{
|
|
584
|
+
initDirectional();
|
|
585
|
+
if (ls.shadersFailed) return;
|
|
586
|
+
}
|
|
587
|
+
if (!ls.directionalTexture || ls.directionalTextureSize !== ls.directionalTextureSizeAllocated)
|
|
588
|
+
{
|
|
589
|
+
ls.directionalTexture && freeDirectionalTextures();
|
|
590
|
+
initDirectionalTextures();
|
|
591
|
+
}
|
|
592
|
+
const p = ls.directionalPrograms, N = ls.directionalTextureSize, W = ls.shadowMapWorldSize;
|
|
593
|
+
const d = light.sunDirection.normalize(-1), toUV = (texels)=> vec2(d.x * texels / N, d.y * texels / N);
|
|
594
|
+
const casts = light.castShadow ? 1 : 0;
|
|
595
|
+
const cap = ceil(log2(N)) + 1; // passes enough to cross the texture, so a long shadow fades out, never cut off
|
|
596
|
+
|
|
597
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, glFramebuffer);
|
|
598
|
+
gl.viewport(0, 0, N, N);
|
|
599
|
+
gl.disable(gl.BLEND);
|
|
600
|
+
const target = (texture)=> gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
|
601
|
+
const bind = (unit, texture)=> { gl.activeTexture(gl.TEXTURE0 + unit); gl.bindTexture(gl.TEXTURE_2D, texture); };
|
|
602
|
+
const use = (name)=> { gl.useProgram(p[name]); gl.bindVertexArray(p[name + 'VAO']); return p[name]; };
|
|
603
|
+
const draw = ()=> gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
604
|
+
const u = glUniformLocation;
|
|
605
|
+
|
|
606
|
+
// long shadows: the foreground seeded into A, then passes shifting twice as far each time, ping ponged
|
|
607
|
+
let program = use('seed');
|
|
608
|
+
bind(1, ls.shadowMap); bind(2, ls.backgroundMap || ls.shadowMap); // a texture on the unit, though unread
|
|
609
|
+
gl.uniform1f(u(program, 'useF'), casts);
|
|
610
|
+
gl.uniform1f(u(program, 'useB'), 0);
|
|
611
|
+
gl.uniform1f(u(program, 'tap'), .25 / N);
|
|
612
|
+
target(ls.directionalTextureA); draw();
|
|
613
|
+
let src = ls.directionalTextureA, dst = ls.directionalTextureB;
|
|
614
|
+
const L = light.shadowLength * N / W;
|
|
615
|
+
if (casts && L > 0)
|
|
616
|
+
{
|
|
617
|
+
program = use('shadow');
|
|
618
|
+
const passes = clamp(ceil(log2(max(L, 1))), 1, cap);
|
|
619
|
+
for (let k = 0; k < passes; ++k)
|
|
620
|
+
{
|
|
621
|
+
const shift = toUV(2**k);
|
|
622
|
+
gl.uniform2f(u(program, 'shift'), shift.x, shift.y);
|
|
623
|
+
gl.uniform1f(u(program, 'fade'), 2**k / L);
|
|
624
|
+
bind(1, src); target(dst); draw();
|
|
625
|
+
[src, dst] = [dst, src];
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const shadows = src, free = dst; // the long shadows, and the other work texture
|
|
629
|
+
|
|
630
|
+
// the background leak, seeded from the background alone into the free texture, only when something casts
|
|
631
|
+
// into the background map; with nothing there the combine leaves it out; the foreground is in the long
|
|
632
|
+
// shadows already, and seeding it here too darkened a see through caster twice, only beside a background
|
|
633
|
+
const leaks = ls.backgroundCasters;
|
|
634
|
+
if (leaks)
|
|
635
|
+
{
|
|
636
|
+
program = use('seed');
|
|
637
|
+
bind(1, ls.shadowMap); bind(2, ls.backgroundMap);
|
|
638
|
+
gl.uniform1f(u(program, 'useF'), 0);
|
|
639
|
+
gl.uniform1f(u(program, 'useB'), 1);
|
|
640
|
+
target(free); draw();
|
|
641
|
+
}
|
|
642
|
+
let leakSrc = free, leakDst = ls.directionalTexture;
|
|
643
|
+
const D = light.backgroundDepth * N / W;
|
|
644
|
+
if (leaks && D > 0)
|
|
645
|
+
{
|
|
646
|
+
// passes shifting twice as far each time, as the long shadows, so the light fades in evenly by D
|
|
647
|
+
program = use('leak');
|
|
648
|
+
const passes = clamp(ceil(log2(max(D, 1))), 1, cap);
|
|
649
|
+
for (let k = 0; k < passes; ++k)
|
|
650
|
+
{
|
|
651
|
+
const shift = toUV(2**k);
|
|
652
|
+
gl.uniform2f(u(program, 'shift'), shift.x, shift.y);
|
|
653
|
+
gl.uniform1f(u(program, 'fade'), 2**k / D);
|
|
654
|
+
bind(1, leakSrc); target(leakDst); draw();
|
|
655
|
+
[leakSrc, leakDst] = [leakDst, leakSrc];
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// combine into the one of the three textures left, long shadows times the leak, with a rim on casters;
|
|
660
|
+
// it is the built light from now on, the other two its work textures
|
|
661
|
+
const textures = [ls.directionalTexture, ls.directionalTextureA, ls.directionalTextureB];
|
|
662
|
+
const built = textures.find((texture)=> texture !== shadows && texture !== leakSrc);
|
|
663
|
+
program = use('combine');
|
|
664
|
+
bind(1, shadows); bind(2, leakSrc); bind(3, ls.shadowMap);
|
|
665
|
+
const rim = toUV(3);
|
|
666
|
+
gl.uniform2f(u(program, 'rim'), rim.x, rim.y);
|
|
667
|
+
gl.uniform1f(u(program, 'useF'), casts);
|
|
668
|
+
gl.uniform1f(u(program, 'useT'), leaks ? 1 : 0);
|
|
669
|
+
target(built); draw();
|
|
670
|
+
[ls.directionalTextureA, ls.directionalTextureB] = textures.filter((texture)=> texture !== built);
|
|
671
|
+
ls.directionalTexture = built;
|
|
672
|
+
|
|
673
|
+
// hand the engine its state back: unit 0 active with its texture, no framebuffer
|
|
674
|
+
bind(1, null); bind(2, null); bind(3, null);
|
|
675
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
676
|
+
glActiveTexture && gl.bindTexture(gl.TEXTURE_2D, glActiveTexture);
|
|
677
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
678
|
+
gl.enable(gl.BLEND);
|
|
679
|
+
}
|
|
405
680
|
function lightSystemRender()
|
|
406
681
|
{
|
|
407
|
-
if (headlessMode || !glEnable) return;
|
|
682
|
+
if (headlessMode || !glEnable || lightSystem.shadersFailed) return;
|
|
408
683
|
if (!lightSystem.enabled) return;
|
|
409
684
|
if (!lightSystem.texture)
|
|
410
685
|
{
|
|
@@ -418,9 +693,24 @@ class LightSystemPlugin
|
|
|
418
693
|
glFlush();
|
|
419
694
|
const prevAdditive = glAdditive;
|
|
420
695
|
|
|
421
|
-
// 1b. the shadow pass draws every caster black into the shadow map
|
|
422
|
-
if (lightSystem.shadows)
|
|
696
|
+
// 1b. the shadow pass draws every caster black into the shadow map, for shadows or a directional light
|
|
697
|
+
if (lightSystem.shadows || lightSystem.directionalLight)
|
|
423
698
|
lightSystemShadowPass();
|
|
699
|
+
if (lightSystem.shadersFailed) return;
|
|
700
|
+
|
|
701
|
+
// 1c. the directional light is built from the maps, before the lightmap is bound
|
|
702
|
+
const sun = lightSystem.directionalLight;
|
|
703
|
+
if (sun)
|
|
704
|
+
lightSystemDirectionalPass();
|
|
705
|
+
else
|
|
706
|
+
{
|
|
707
|
+
// the sun is gone, so are its textures, and the shadow map it made when shadows are off
|
|
708
|
+
if (lightSystem.directionalTexture || lightSystem.backgroundMap)
|
|
709
|
+
freeDirectional();
|
|
710
|
+
if (!lightSystem.shadows && lightSystem.shadowMap)
|
|
711
|
+
freeShadowTextures();
|
|
712
|
+
}
|
|
713
|
+
if (lightSystem.shadersFailed) return;
|
|
424
714
|
|
|
425
715
|
// an automatic size follows the canvas, so reallocate the lightmap when
|
|
426
716
|
// the canvas changed size, after the flush so the batch keeps its texture
|
|
@@ -476,6 +766,26 @@ class LightSystemPlugin
|
|
|
476
766
|
o.renderLight();
|
|
477
767
|
}
|
|
478
768
|
|
|
769
|
+
// 3a. the directional light, added over the shadow map's area in its color
|
|
770
|
+
if (sun && lightSystem.directionalTexture)
|
|
771
|
+
{
|
|
772
|
+
glFlush();
|
|
773
|
+
const gl = glContext, p = lightSystem.directionalPrograms, as = p.add, c = sun.color;
|
|
774
|
+
gl.useProgram(as);
|
|
775
|
+
gl.bindVertexArray(p.addVAO);
|
|
776
|
+
gl.uniformMatrix4fv(glUniformLocation(as, 'm'), false, glTransform);
|
|
777
|
+
gl.uniform2f(glUniformLocation(as, 'origin'), lightSystem.shadowMapOrigin.x, lightSystem.shadowMapOrigin.y);
|
|
778
|
+
gl.uniform1f(glUniformLocation(as, 'worldSize'), lightSystem.shadowMapWorldSize);
|
|
779
|
+
gl.uniform4f(glUniformLocation(as, 'color'), c.r, c.g, c.b, c.a);
|
|
780
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
781
|
+
gl.bindTexture(gl.TEXTURE_2D, lightSystem.directionalTexture);
|
|
782
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
783
|
+
gl.enable(gl.BLEND);
|
|
784
|
+
gl.blendFunc(gl.ONE, gl.ONE);
|
|
785
|
+
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
786
|
+
glSetInstancedMode(true);
|
|
787
|
+
}
|
|
788
|
+
|
|
479
789
|
// 3b. emissive objects draw their shape in gray at their emissive level, white at 1, adding that much
|
|
480
790
|
// light where they are so they show their own colors; text goes to the 1x1 canvas as in the
|
|
481
791
|
// shadow pass, so it is not drawn twice
|
|
@@ -555,10 +865,12 @@ class LightSystemPlugin
|
|
|
555
865
|
lightSystem.lightVAO = undefined;
|
|
556
866
|
lightSystem.compositeVAO = undefined;
|
|
557
867
|
clearShadows();
|
|
868
|
+
clearDirectional();
|
|
558
869
|
LOG('LightSystemPlugin: WebGL context lost');
|
|
559
870
|
}
|
|
560
871
|
function lightSystemContextRestored()
|
|
561
872
|
{
|
|
873
|
+
lightSystem.shadersFailed = false; // tried again on the new context, as the 3D renderer is
|
|
562
874
|
initLightSystem();
|
|
563
875
|
LOG('LightSystemPlugin: WebGL context restored');
|
|
564
876
|
}
|
|
@@ -570,6 +882,9 @@ class LightSystemPlugin
|
|
|
570
882
|
{
|
|
571
883
|
this.shadowMapSize = glClampTextureSize(this.shadowMapSize);
|
|
572
884
|
this.shadowTextureSize = glClampTextureSize(this.shadowTextureSize);
|
|
885
|
+
ASSERT(isNumber(this.directionalTextureSize) && this.directionalTextureSize >= 1,
|
|
886
|
+
'directionalTextureSize is texels, 1 or more, taken down to whole texels', this.directionalTextureSize);
|
|
887
|
+
this.directionalTextureSize = glClampTextureSize(max(1, floor(this.directionalTextureSize) || 1));
|
|
573
888
|
const size = this.textureSize;
|
|
574
889
|
if (!size) return;
|
|
575
890
|
const x = glClampTextureSize(size.x), y = glClampTextureSize(size.y);
|
|
@@ -762,3 +1077,70 @@ class Light extends EngineObject
|
|
|
762
1077
|
drawTile(this.pos, size, this.glowTileInfo, this.color, 0, false, undefined, true, false);
|
|
763
1078
|
}
|
|
764
1079
|
}
|
|
1080
|
+
|
|
1081
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
1082
|
+
|
|
1083
|
+
/**
|
|
1084
|
+
* A DirectionalLight is a sun for the 2D light system: it lights the whole scene from one direction, added into the
|
|
1085
|
+
* lightmap with the point lights
|
|
1086
|
+
* - One at a time, made after the LightSystemPlugin, lightSystem.directionalLight is the one; a debug build asserts on
|
|
1087
|
+
* a second while the first lives, a release build destroys the first
|
|
1088
|
+
* - Its castShadow lets foreground casters, objects with castShadow, throw long shadows across open space, fading out
|
|
1089
|
+
* by shadowLength; a light does not need lightSystem.shadows for that
|
|
1090
|
+
* - Objects with castBackgroundShadow, a background layer, are dark to it inside and lit at the edges that face it,
|
|
1091
|
+
* fading in by backgroundDepth
|
|
1092
|
+
* @extends EngineObject
|
|
1093
|
+
* @memberof LightSystem
|
|
1094
|
+
* @example
|
|
1095
|
+
* new DirectionalLight(vec2(-1, 1), hsl(.1, .3, 1)); // a warm sun up and to the left, shining down and to the right
|
|
1096
|
+
*/
|
|
1097
|
+
class DirectionalLight extends EngineObject
|
|
1098
|
+
{
|
|
1099
|
+
/** Create the scene's directional light
|
|
1100
|
+
* @param {Vector2} [sunDirection] - Toward the sun, it shines the other way, as render3D.sunDirection
|
|
1101
|
+
* @param {Color} [color] - Color of the light; alpha modulates intensity */
|
|
1102
|
+
constructor(sunDirection=vec2(-1, 1), color=WHITE)
|
|
1103
|
+
{
|
|
1104
|
+
ASSERT(!!lightSystem, 'make a LightSystemPlugin before a DirectionalLight');
|
|
1105
|
+
ASSERT(isVector2(sunDirection) && !!(sunDirection.x || sunDirection.y),
|
|
1106
|
+
'DirectionalLight: sunDirection is a vec2 that is not zero, toward the sun', sunDirection);
|
|
1107
|
+
ASSERT(!lightSystem?.directionalLight, 'there is one DirectionalLight at a time, destroy the old one first');
|
|
1108
|
+
super(vec2(), vec2(), undefined, 0, color);
|
|
1109
|
+
this.mass = 0; // it does not fall in a game with gravity
|
|
1110
|
+
|
|
1111
|
+
/** @property {Vector2} - Toward the sun, it shines the other way, as render3D.sunDirection */
|
|
1112
|
+
this.sunDirection = sunDirection.copy();
|
|
1113
|
+
/** @property {number} - World units a long shadow reaches before it has faded out; a caster casts only from
|
|
1114
|
+
* inside the shadow map, shadowMapScale views across, so past (shadowMapScale - 1) / 2 of a view beyond the
|
|
1115
|
+
* screen it throws none in */
|
|
1116
|
+
this.shadowLength = 20;
|
|
1117
|
+
/** @property {number} - World units the light gets into a background area from its edges facing it */
|
|
1118
|
+
this.backgroundDepth = 3;
|
|
1119
|
+
// castShadow is EngineObject's, true: foreground casters throw long shadows; it draws nothing, so never casts
|
|
1120
|
+
// in a release build, with no assert, a second takes over and the first goes, not left alive doing nothing
|
|
1121
|
+
lightSystem?.directionalLight?.destroy();
|
|
1122
|
+
lightSystem && (lightSystem.directionalLight = this);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/** Check its settings, called automatically each frame */
|
|
1126
|
+
update()
|
|
1127
|
+
{
|
|
1128
|
+
ASSERT(isVector2(this.sunDirection) && !!(this.sunDirection.x || this.sunDirection.y),
|
|
1129
|
+
'DirectionalLight: sunDirection is a vec2 that is not zero, toward the sun', this.sunDirection);
|
|
1130
|
+
ASSERT(this.shadowLength >= 0 && this.backgroundDepth >= 0,
|
|
1131
|
+
'DirectionalLight: shadowLength and backgroundDepth are world units, 0 or more', this.shadowLength,
|
|
1132
|
+
this.backgroundDepth);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/** A directional light draws nothing of its own, it is added into the lightmap by the plugin */
|
|
1136
|
+
render() {}
|
|
1137
|
+
|
|
1138
|
+
/** Destroy the light, the scene goes without it from the next frame
|
|
1139
|
+
* @param {boolean} [immediate] */
|
|
1140
|
+
destroy(immediate)
|
|
1141
|
+
{
|
|
1142
|
+
if (lightSystem?.directionalLight === this)
|
|
1143
|
+
lightSystem.directionalLight = undefined;
|
|
1144
|
+
super.destroy(immediate);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
package/plugins/math3d.js
CHANGED
|
@@ -553,8 +553,10 @@ class Matrix4
|
|
|
553
553
|
const m = this.m, s = this.getScale();
|
|
554
554
|
const sx = s.x || 1, sy = s.y || 1, sz = s.z || 1;
|
|
555
555
|
const m1 = m[1] / sx, m5 = m[5] / sy, m8 = m[8] / sz, m9 = m[9] / sz, m10 = m[10] / sz;
|
|
556
|
-
|
|
557
|
-
|
|
556
|
+
// the pitch from its cosine as well as its sine, which stays exact near straight up where the sine alone
|
|
557
|
+
// loses its precision
|
|
558
|
+
const c = hypot(m8, m10), pitch = atan2(-m9, c);
|
|
559
|
+
if (c > 1e-9)
|
|
558
560
|
return new Vector3(pitch, atan2(m8, m10), atan2(m1, m5));
|
|
559
561
|
// straight up or down: yaw and roll turn about the same axis, so the roll is zero and yaw takes it all
|
|
560
562
|
return new Vector3(pitch, atan2(m[4] / sy * -m9, m[0] / sx), 0);
|