littlejsengine 1.25.0 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/FAQ.md +49 -11
  3. package/README.md +6 -2
  4. package/REFERENCE.md +120 -18
  5. package/dist/littlejs.d.ts +464 -99
  6. package/dist/littlejs.esm.js +2853 -704
  7. package/dist/littlejs.esm.min.js +3 -3
  8. package/dist/littlejs.js +2844 -701
  9. package/dist/littlejs.min.js +2 -2
  10. package/dist/littlejs.release.js +2658 -512
  11. package/package.json +4 -4
  12. package/plugins/box2d.js +110 -34
  13. package/plugins/gltf.js +74 -22
  14. package/plugins/lightSystem.js +410 -28
  15. package/plugins/math3d.js +4 -2
  16. package/plugins/medalSystem.js +19 -7
  17. package/plugins/newgrounds.js +4 -2
  18. package/plugins/parallax.js +1 -0
  19. package/plugins/particleEffects.js +44 -10
  20. package/plugins/pathFinder.js +9 -11
  21. package/plugins/pluginExport.js +9 -0
  22. package/plugins/postProcess.js +4 -2
  23. package/plugins/render3d.js +485 -2385
  24. package/plugins/render3dDebug.js +5 -8
  25. package/plugins/render3dEditor.js +81 -6
  26. package/plugins/render3dExtras.js +103 -31
  27. package/plugins/render3dLevel.js +53 -5
  28. package/plugins/render3dMesh.js +1158 -0
  29. package/plugins/render3dObject.js +1451 -0
  30. package/plugins/render3dVoxels.js +15 -3
  31. package/plugins/textureSheet.js +9 -8
  32. package/plugins/threejs.js +1 -1
  33. package/plugins/tweenSystem.js +76 -51
  34. package/plugins/uiSystem.js +37 -8
  35. package/plugins/wavedash.js +229 -0
  36. package/src/engine.js +184 -36
  37. package/src/engineAudio.js +37 -21
  38. package/src/engineBuild.mjs +6 -1
  39. package/src/engineDebug.js +22 -162
  40. package/src/engineDraw.js +139 -34
  41. package/src/engineEditor.js +79 -18
  42. package/src/engineExport.js +0 -3
  43. package/src/engineInput.js +27 -18
  44. package/src/engineMath.js +35 -14
  45. package/src/engineObject.js +60 -41
  46. package/src/engineParticles.js +58 -11
  47. package/src/engineRelease.js +1 -5
  48. package/src/engineSettings.js +48 -9
  49. package/src/engineTileLayer.js +11 -4
  50. package/src/engineUtilities.js +41 -9
  51. package/src/engineWebGL.js +37 -13
@@ -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
- const quadVertex =
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
- function freeShadows()
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.deleteProgram(ls.shadowCopyShader);
336
- gl.deleteProgram(ls.shadowStretchShader);
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 && freeShadows();
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
- const texel = worldSize / size;
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
- for (const o of engineObjects)
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
- if (o.destroyed || !o.castShadow) continue;
388
- // every color black, its alpha kept, set for each object since a render that left
389
- // setShadowTransparent on ends with it
390
- glColorMask = 0xff000000;
391
- setShader(o.shader); // its own Shader as in the main pass, so a snippet that cuts holes casts the same shape
392
- o.renderShadow();
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
- const pitch = Math.asin(clamp(-m9, -1, 1));
557
- if (abs(m9) < 1 - 1e-6)
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);