littlejsengine 1.19.3 → 1.20.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 (50) hide show
  1. package/FAQ.md +841 -820
  2. package/README.md +29 -25
  3. package/dist/littlejs.d.ts +2455 -935
  4. package/dist/littlejs.esm.js +30344 -21110
  5. package/dist/littlejs.esm.min.js +4 -3
  6. package/dist/littlejs.js +30259 -21052
  7. package/dist/littlejs.min.js +2 -2
  8. package/dist/littlejs.release.js +26567 -20455
  9. package/package.json +10 -5
  10. package/plugins/audioEffects.js +28 -24
  11. package/plugins/box2d.js +2516 -2073
  12. package/plugins/drawUtilities.js +206 -31
  13. package/plugins/gltf.js +740 -0
  14. package/plugins/jsconfig.json +19 -0
  15. package/plugins/lightSystem.js +719 -332
  16. package/plugins/math3d.js +58 -17
  17. package/plugins/medalSystem.js +326 -280
  18. package/plugins/newgrounds.js +400 -199
  19. package/plugins/pathFinder.js +208 -175
  20. package/plugins/pluginExport.js +15 -0
  21. package/plugins/postProcess.js +56 -13
  22. package/plugins/render3d.js +1643 -1577
  23. package/plugins/render3dDebug.js +109 -0
  24. package/plugins/render3dExtras.js +1754 -0
  25. package/plugins/render3dVoxels.js +502 -0
  26. package/plugins/textureSheet.js +496 -458
  27. package/plugins/threejs.js +183 -163
  28. package/plugins/tweakables.js +411 -0
  29. package/plugins/tweenSystem.js +263 -145
  30. package/plugins/uiSystem.js +2024 -1687
  31. package/plugins/zzfxm.js +159 -159
  32. package/src/engine.js +191 -75
  33. package/src/engineAudio.js +229 -125
  34. package/src/engineBuild.mjs +289 -256
  35. package/src/engineDebug.js +912 -742
  36. package/src/engineDraw.js +295 -193
  37. package/src/engineEditor.js +2431 -0
  38. package/src/engineExport.js +12 -0
  39. package/src/engineInput.js +1462 -1293
  40. package/src/engineMath.js +1274 -1252
  41. package/src/engineObject.js +194 -80
  42. package/src/engineParticles.js +583 -532
  43. package/src/engineRelease.js +67 -45
  44. package/src/engineSettings.js +77 -29
  45. package/src/engineTileLayer.js +995 -741
  46. package/src/engineUtilities.js +19 -13
  47. package/src/engineWebGL.js +239 -82
  48. package/src/jsconfig.json +17 -13
  49. package/plugins/box2d.wasm.js +0 -630
  50. package/plugins/box2d.wasm.wasm +0 -0
@@ -1,332 +1,719 @@
1
- /**
2
- * LittleJS Light System Plugin
3
- * - Adds 2D dynamic lighting to the scene
4
- * - Lights are first-class EngineObjects (the Light class)
5
- * - Each Light draws a soft falloff blob of its color into a shared lightmap
6
- * - Lights accumulate ADDITIVELY in the lightmap (red + blue = magenta)
7
- * - The lightmap is then MULTIPLIED with the scene during composite, so unlit
8
- * areas go to the ambient color and lit areas show the scene tinted by the
9
- * accumulated light color
10
- * - Draw the world at full brightness — the lightmap does the darkening
11
- * - Any EngineObject may override renderLight() to additively contribute to the
12
- * lightmap (e.g. emissive lava tiles, weapon flashes, glowing crystals)
13
- * - Must be constructed BEFORE PostProcessPlugin so post-process sees lit pixels
14
- * @namespace LightSystem
15
- */
16
-
17
- 'use strict';
18
-
19
- ///////////////////////////////////////////////////////////////////////////////
20
-
21
- /** Global Light System plugin object
22
- * @type {LightSystemPlugin}
23
- * @memberof LightSystem */
24
- let lightSystem;
25
-
26
- ///////////////////////////////////////////////////////////////////////////////
27
-
28
- /**
29
- * LightSystemPlugin
30
- * - Owns the offscreen lightmap texture, falloff/composite shaders, and the
31
- * per-frame render pass that multiplies the lightmap onto the WebGL scene
32
- * - The composite is MULTIPLICATIVE: unlit areas get the ambient color, lit
33
- * areas show the scene tinted by the accumulated light color. So you should
34
- * draw your world at full brightness — the lightmap handles the darkening.
35
- * @memberof LightSystem
36
- */
37
- class LightSystemPlugin
38
- {
39
- /** Create the global light system plugin.
40
- * @param {Vector2} [textureSize] - Size of the lightmap texture (defaults to mainCanvasSize, which is css pixels, so the lightmap is not scaled by canvasPixelRatio; pass mainCanvasSize.scale(getCanvasPixelRatio()) for a full resolution lightmap)
41
- * @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.
42
- * @example
43
- * // simplest usage
44
- * new LightSystemPlugin();
45
- */
46
- constructor(textureSize, ambientColor)
47
- {
48
- ASSERT(!lightSystem, 'LightSystemPlugin already initialized');
49
- ASSERT(!postProcess, 'LightSystemPlugin must be created before PostProcessPlugin');
50
- lightSystem = this;
51
-
52
- /** @property {boolean} - When false, the render pass is skipped entirely */
53
- this.enabled = true;
54
- /** @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. */
55
- this.ambientColor = (ambientColor || BLACK).copy();
56
- /** @property {Vector2} - Size of the lightmap texture (set at construction; falls back to mainCanvasSize in css pixels at init time, so it is not scaled by canvasPixelRatio) */
57
- this.textureSize = textureSize ? textureSize.copy() : undefined;
58
-
59
- /** @property {WebGLTexture} - The lightmap texture */
60
- this.texture = undefined;
61
- /** @property {WebGLProgram} - Shader for drawing per-Light falloff blobs into the lightmap */
62
- this.lightShader = undefined;
63
- /** @property {WebGLProgram} - Shader for compositing the lightmap over the main scene */
64
- this.compositeShader = undefined;
65
- /** @property {WebGLVertexArrayObject} - Vertex array object for the light shader */
66
- this.lightVAO = undefined;
67
- /** @property {WebGLVertexArrayObject} - Vertex array object for the composite shader */
68
- this.compositeVAO = undefined;
69
-
70
- initLightSystem();
71
- engineAddPlugin(undefined, lightSystemRender,
72
- lightSystemContextLost, lightSystemContextRestored);
73
-
74
- function initLightSystem()
75
- {
76
- if (headlessMode) return;
77
- if (!glEnable)
78
- {
79
- console.warn('LightSystemPlugin: WebGL not enabled!');
80
- return;
81
- }
82
-
83
- // resolve texture size default at init time (mainCanvasSize may
84
- // not be set yet at the moment the constructor first ran)
85
- if (!lightSystem.textureSize)
86
- lightSystem.textureSize = mainCanvasSize.copy();
87
-
88
- // allocate the lightmap texture with null data at textureSize
89
- lightSystem.texture = glContext.createTexture();
90
- glContext.bindTexture(glContext.TEXTURE_2D, lightSystem.texture);
91
- glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA,
92
- lightSystem.textureSize.x, lightSystem.textureSize.y, 0,
93
- glContext.RGBA, glContext.UNSIGNED_BYTE, null);
94
- glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MAG_FILTER, glContext.LINEAR);
95
- glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, glContext.LINEAR);
96
- glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_S, glContext.CLAMP_TO_EDGE);
97
- glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_T, glContext.CLAMP_TO_EDGE);
98
-
99
- // light falloff shader: one quad per Light, fragment computes radial falloff
100
- lightSystem.lightShader = glCreateProgram(
101
- '#version 300 es\n' +
102
- 'precision highp float;'+
103
- 'uniform mat4 m;'+
104
- 'uniform vec2 lightPos;'+
105
- 'uniform float radius;'+
106
- 'in vec2 g;'+ // unit quad geometry [0..1]
107
- 'out vec2 vWorldPos;'+
108
- 'void main(){'+
109
- 'vec2 worldP=lightPos+(g-.5)*2.*radius;'+
110
- 'gl_Position=m*vec4(worldP,1,1);'+
111
- 'vWorldPos=worldP;'+
112
- '}'
113
- ,
114
- '#version 300 es\n' +
115
- 'precision highp float;'+
116
- 'uniform vec2 lightPos;'+
117
- 'uniform float radius;'+
118
- 'uniform float fadeRange;'+
119
- 'uniform vec4 color;'+
120
- 'in vec2 vWorldPos;'+
121
- 'out vec4 c;'+
122
- 'void main(){'+
123
- 'float dist=distance(vWorldPos,lightPos);'+
124
- 'float t=clamp((radius-dist)/max(fadeRange,1e-6),0.,1.);'+
125
- 'c=vec4(color.rgb*t*color.a,1.);'+
126
- '}'
127
- );
128
-
129
- // composite shader: fullscreen quad, samples the lightmap
130
- lightSystem.compositeShader = glCreateProgram(
131
- '#version 300 es\n' +
132
- 'precision highp float;'+
133
- 'in vec2 p;'+
134
- 'void main(){'+
135
- 'gl_Position=vec4(p+p-1.,1,1);'+
136
- '}'
137
- ,
138
- '#version 300 es\n' +
139
- 'precision highp float;'+
140
- 'uniform sampler2D s;'+
141
- 'uniform vec3 iResolution;'+
142
- 'out vec4 c;'+
143
- 'void main(){'+
144
- 'vec2 uv=gl_FragCoord.xy/iResolution.xy;'+
145
- 'c=vec4(texture(s,uv).rgb,1.);'+
146
- '}'
147
- );
148
-
149
- // VAO for the per-Light quad — reuses the engine unit triangle-strip
150
- lightSystem.lightVAO = glContext.createVertexArray();
151
- glContext.bindVertexArray(lightSystem.lightVAO);
152
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
153
- const gLight = glContext.getAttribLocation(lightSystem.lightShader, 'g');
154
- glContext.enableVertexAttribArray(gLight);
155
- glContext.vertexAttribPointer(gLight, 2, glContext.FLOAT, false, 8, 0);
156
-
157
- // VAO for the composite fullscreen quad — same buffer, attribute named 'p'
158
- lightSystem.compositeVAO = glContext.createVertexArray();
159
- glContext.bindVertexArray(lightSystem.compositeVAO);
160
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
161
- const pComp = glContext.getAttribLocation(lightSystem.compositeShader, 'p');
162
- glContext.enableVertexAttribArray(pComp);
163
- glContext.vertexAttribPointer(pComp, 2, glContext.FLOAT, false, 8, 0);
164
- }
165
- function lightSystemRender()
166
- {
167
- if (headlessMode || !glEnable) return;
168
- if (!lightSystem.enabled) return;
169
- if (!lightSystem.texture) return; // init failed or context lost
170
-
171
- // 1. flush any in-flight sprite batch from earlier render passes
172
- glFlush();
173
- const prevAdditive = glAdditive;
174
-
175
- // 2. bind lightmap as render target, clear to ambientColor
176
- const ac = lightSystem.ambientColor;
177
- glContext.bindFramebuffer(glContext.FRAMEBUFFER, glFramebuffer);
178
- glContext.framebufferTexture2D(glContext.FRAMEBUFFER,
179
- glContext.COLOR_ATTACHMENT0, glContext.TEXTURE_2D, lightSystem.texture, 0);
180
- glContext.viewport(0, 0, lightSystem.textureSize.x, lightSystem.textureSize.y);
181
- glContext.clearColor(ac.r, ac.g, ac.b, ac.a);
182
- glContext.clear(glContext.COLOR_BUFFER_BIT);
183
-
184
- // 3. walk engineObjects calling renderLight() — additive blend
185
- // (lightmap accumulates raw additive color contributions)
186
- setAdditiveBlendMode();
187
- glContext.enable(glContext.BLEND);
188
- glContext.blendFunc(glContext.ONE, glContext.ONE);
189
-
190
- for (const o of engineObjects)
191
- o.destroyed || o.renderLight();
192
-
193
- // 4. drain any sprite-batched draws (e.g. drawTile inside a
194
- // custom renderLight override) so they hit the FBO, not the
195
- // canvas after we unbind
196
- glFlush();
197
- glContext.bindFramebuffer(glContext.FRAMEBUFFER, null);
198
-
199
- // backing store size, mainCanvasSize is css pixels
200
- glContext.viewport(0, 0, glCanvas.width, glCanvas.height);
201
-
202
- // 5. composite: fullscreen quad, multiplicative blend onto glCanvas
203
- // (scene * lightmap — unlit areas go to black, lit areas are
204
- // the scene tinted by the accumulated light color)
205
- glContext.useProgram(lightSystem.compositeShader);
206
- glContext.bindVertexArray(lightSystem.compositeVAO);
207
- glContext.activeTexture(glContext.TEXTURE0);
208
- glContext.bindTexture(glContext.TEXTURE_2D, lightSystem.texture);
209
- const cs = lightSystem.compositeShader;
210
- glContext.uniform1i(glContext.getUniformLocation(cs, 's'), 0);
211
- glContext.uniform3f(glContext.getUniformLocation(cs, 'iResolution'),
212
- mainCanvas.width, mainCanvas.height, 1);
213
- glContext.blendFunc(glContext.DST_COLOR, glContext.ZERO);
214
- glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, 4);
215
-
216
- // 6. restore engine state so subsequent draws use the engine's
217
- // tracked texture binding (otherwise glSetTexture would think
218
- // the prior texture was still bound when actually the lightmap
219
- // is, and any debug text / future draw could sample the lightmap)
220
- if (glActiveTexture)
221
- glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
222
- setAdditiveBlendMode(prevAdditive);
223
- glSetInstancedMode(true);
224
- }
225
- function lightSystemContextLost()
226
- {
227
- lightSystem.texture = undefined;
228
- lightSystem.lightShader = undefined;
229
- lightSystem.compositeShader = undefined;
230
- lightSystem.lightVAO = undefined;
231
- lightSystem.compositeVAO = undefined;
232
- LOG('LightSystemPlugin: WebGL context lost');
233
- }
234
- function lightSystemContextRestored()
235
- {
236
- initLightSystem();
237
- LOG('LightSystemPlugin: WebGL context restored');
238
- }
239
- }
240
-
241
- /** Draw a single Light's falloff blob into the currently bound lightmap.
242
- * Called by Light.renderLight() during the plugin's render pass.
243
- * @param {Light} light */
244
- drawLight(light)
245
- {
246
- if (headlessMode || !glEnable || !this.lightShader) return;
247
-
248
- // drain any sprite-batched draws queued by a previous custom
249
- // renderLight() override (e.g. drawRect inside a LavaTile). They were
250
- // queued in the engine's instanced-vertex format and must flush with
251
- // the engine's shader+VAO bound — NOT this plugin's light shader.
252
- glFlush();
253
-
254
- glContext.useProgram(this.lightShader);
255
- glContext.bindVertexArray(this.lightVAO);
256
-
257
- // re-apply the engine camera transform onto this shader. Divide by
258
- // mainCanvasSize (not textureSize) so world→NDC matches the main
259
- // pass; the viewport handles the lightmap's actual resolution.
260
- // No y-flip here: the composite samples this FBO with
261
- // gl_FragCoord/iResolution (origin bottom-left), so storing world
262
- // +Y at the top of the texture lines up with the canvas convention.
263
- const s = vec2(2*cameraScale).divide(mainCanvasSize);
264
- const rotatedCam = cameraPos.rotate(-cameraAngle);
265
- const p = vec2(-1).subtract(rotatedCam.multiply(s));
266
- const ca = cos(cameraAngle);
267
- const sa = sin(cameraAngle);
268
- const transform = [
269
- s.x * ca, s.y * sa, 0, 0,
270
- -s.x * sa, s.y * ca, 0, 0,
271
- 1, 1, 1, 0,
272
- p.x, p.y, 0, 1];
273
-
274
- const ls = this.lightShader;
275
- glContext.uniformMatrix4fv(glContext.getUniformLocation(ls, 'm'), false, transform);
276
- glContext.uniform2f(glContext.getUniformLocation(ls, 'lightPos'), light.pos.x, light.pos.y);
277
- glContext.uniform1f(glContext.getUniformLocation(ls, 'radius'), light.radius);
278
- glContext.uniform1f(glContext.getUniformLocation(ls, 'fadeRange'), light.fadeRange);
279
- const c = light.color;
280
- glContext.uniform4f(glContext.getUniformLocation(ls, 'color'), c.r, c.g, c.b, c.a);
281
-
282
- glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, 4);
283
-
284
- // restore engine's instanced shader+VAO so subsequent renderLight()
285
- // overrides that batch through drawRect/drawTile work correctly
286
- glSetInstancedMode(true);
287
- }
288
- }
289
-
290
- ///////////////////////////////////////////////////////////////////////////////
291
-
292
- /**
293
- * A Light is an EngineObject that contributes a soft additive blob of color
294
- * to the LightSystem plugin's lightmap.
295
- * @extends EngineObject
296
- * @memberof LightSystem
297
- * @example
298
- * new Light(vec2(5, 5), 4, rgb(1, 0.5, 0)); // orange light, full soft blob
299
- * new Light(vec2(0, 0), 8, rgb(1, 1, 1), 2); // white core with 2-unit soft halo
300
- */
301
- class Light extends EngineObject
302
- {
303
- /** Create a light object and add it to the engine object list
304
- * @param {Vector2} pos - World space position
305
- * @param {number} radius - Total extent of the light in world units
306
- * @param {Color} [color] - Color of the light; alpha modulates intensity
307
- * @param {number} [fadeRange] - Width of the soft edge in world units (defaults to radius) */
308
- constructor(pos, radius, color, fadeRange)
309
- {
310
- super(pos, vec2(1), undefined, 0, color);
311
- ASSERT(isNumber(radius) && radius >= 0, 'Light radius must be a non-negative number');
312
- ASSERT(fadeRange === undefined || (isNumber(fadeRange) && fadeRange >= 0),
313
- 'Light fadeRange must be a non-negative number when provided');
314
-
315
- /** @property {number} - Total extent of the light in world units */
316
- this.radius = radius;
317
- /** @property {number} - Width of the soft edge in world units */
318
- this.fadeRange = fadeRange === undefined ? radius : fadeRange;
319
- }
320
-
321
- /** Lights are invisible in the main render pass — they only contribute
322
- * to the lightmap via renderLight(). */
323
- render() {}
324
-
325
- /** Draw this light's falloff blob into the lightmap.
326
- * Called by LightSystemPlugin during its render pass. No-op when the
327
- * plugin or WebGL is unavailable. */
328
- renderLight()
329
- {
330
- lightSystem && lightSystem.drawLight(this);
331
- }
332
- }
1
+ /**
2
+ * LittleJS Light System Plugin
3
+ * - Adds 2D dynamic lighting to the scene
4
+ * - Lights are first-class EngineObjects (the Light class)
5
+ * - Each Light draws a soft falloff blob of its color into a shared lightmap
6
+ * - Lights accumulate ADDITIVELY in the lightmap (red + blue = magenta)
7
+ * - The lightmap is then MULTIPLIED with the scene during composite, so unlit
8
+ * areas go to the ambient color and lit areas show the scene tinted by the
9
+ * accumulated light color
10
+ * - Draw the world at full brightness — the lightmap does the darkening
11
+ * - The pass runs after gameRenderPost, so a HUD drawn there with WebGL is
12
+ * darkened too; draw the HUD with useWebGL=false (the main canvas) or from
13
+ * a plugin created after this one
14
+ * - Any EngineObject may override renderLight() to additively contribute to the
15
+ * lightmap (e.g. lava that lights the floor around it, weapon flashes, glowing crystals)
16
+ * - Set obj.emissive to 1 to show an object at full brightness in its own colors, lit or not, or between 0 and 1 for
17
+ * partly: it draws its shape into the lightmap through renderEmissive(), which calls render() by default; exact for
18
+ * solid pixels, a partly transparent one is self lit by its alpha too, so it shows darker
19
+ * - Set lightSystem.shadows for objects to block light: each frame every object draws black into a
20
+ * shadow map through renderShadow(), which calls render() by default; obj.castShadow = false keeps it
21
+ * out (a floor TileLayer, a background), a draw's alpha sets how much light it blocks, and
22
+ * setShadowTransparent lets its color tint the light
23
+ * - Must be constructed BEFORE PostProcessPlugin so post-process sees lit pixels
24
+ * @namespace LightSystem
25
+ */
26
+
27
+ 'use strict';
28
+
29
+ ///////////////////////////////////////////////////////////////////////////////
30
+
31
+ /** Global Light System plugin object
32
+ * @type {LightSystemPlugin}
33
+ * @memberof LightSystem */
34
+ let lightSystem;
35
+
36
+ ///////////////////////////////////////////////////////////////////////////////
37
+
38
+ /**
39
+ * LightSystemPlugin
40
+ * - Owns the offscreen lightmap texture, falloff/composite shaders, and the
41
+ * per-frame render pass that multiplies the lightmap onto the WebGL scene
42
+ * - The composite is MULTIPLICATIVE: unlit areas get the ambient color, lit
43
+ * areas show the scene tinted by the accumulated light color. So you should
44
+ * draw your world at full brightness — the lightmap handles the darkening.
45
+ * @memberof LightSystem
46
+ */
47
+ class LightSystemPlugin
48
+ {
49
+ /** Create the global light system plugin.
50
+ * @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)
51
+ * @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.
52
+ * @example
53
+ * // simplest usage
54
+ * new LightSystemPlugin();
55
+ */
56
+ constructor(textureSize, ambientColor)
57
+ {
58
+ ASSERT(engineInitialized || headlessMode, 'create the plugin after engineInit, e.g. in gameInit');
59
+ ASSERT(!lightSystem, 'LightSystemPlugin already initialized');
60
+ ASSERT(!postProcess, 'LightSystemPlugin must be created before PostProcessPlugin');
61
+ lightSystem = this;
62
+
63
+ /** @property {boolean} - When false, the render pass is skipped entirely */
64
+ this.enabled = true;
65
+ /** @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. */
66
+ this.ambientColor = (ambientColor || BLACK).copy();
67
+ /** @property {Vector2} - Size of the lightmap texture, follows mainCanvasSize (css pixels, so it is not scaled by canvasPixelRatio) unless a size was passed */
68
+ this.textureSize = textureSize ? textureSize.copy() : undefined;
69
+ /** @property {boolean} - True when no size was passed, so the lightmap follows mainCanvasSize */
70
+ this.textureSizeAuto = !textureSize;
71
+
72
+ /** @property {WebGLTexture|undefined} - The lightmap texture
73
+ * @type {WebGLTexture|undefined} */
74
+ this.texture = undefined;
75
+ /** @property {WebGLProgram|undefined} - Shader for drawing per-Light falloff blobs into the lightmap
76
+ * @type {WebGLProgram|undefined} */
77
+ this.lightShader = undefined;
78
+ /** @property {WebGLProgram|undefined} - Shader for compositing the lightmap over the main scene
79
+ * @type {WebGLProgram|undefined} */
80
+ this.compositeShader = undefined;
81
+ /** @property {WebGLVertexArrayObject|undefined} - Vertex array object for the light shader
82
+ * @type {WebGLVertexArrayObject|undefined} */
83
+ this.lightVAO = undefined;
84
+ /** @property {WebGLVertexArrayObject|undefined} - Vertex array object for the composite shader
85
+ * @type {WebGLVertexArrayObject|undefined} */
86
+ this.compositeVAO = undefined;
87
+
88
+ /** @property {boolean} - Cast shadows: every object draws black into a shadow map once a frame and each light's rays stop at them; off by default and free when off */
89
+ this.shadows = false;
90
+ /** @property {number} - Pixels across the square shadow map, made again when changed */
91
+ this.shadowMapSize = 1024;
92
+ /** @property {number} - How many times the larger side of the view the shadow map covers, so casters just off screen still cast in; raise it when lights reach further than a view past the screen */
93
+ this.shadowMapScale = 2;
94
+ /** @property {number} - Pixels across each light's own shadow texture, made again when changed; larger is sharper,
95
+ * and a gap between casters narrower than about 4*radius/shadowTextureSize world units closes */
96
+ this.shadowTextureSize = 256;
97
+ /** @property {number} - Stretch passes per shadow casting light, fewer is cheaper and shorter shadows */
98
+ this.shadowPassCount = 16;
99
+ /** @property {number} - How much light bleeds into a caster's near side, 0 for hard edged casters, 1 for most;
100
+ * the bleed reaches further in under a bigger light, so a thin wall under a big one lets some through, lower it
101
+ * for those */
102
+ this.shadowSoftness = .5;
103
+ /** @property {boolean} - True while the shadow pass runs, read only, so a render() can skip parts that should not cast */
104
+ this.shadowPass = false;
105
+ /** @property {boolean} - True while emissive objects draw into the lightmap, read only, see EngineObject.emissive */
106
+ this.emissivePass = false;
107
+ /** @property {WebGLTexture|undefined} - The shadow map, casters drawn black on white around the camera, read only
108
+ * @type {WebGLTexture|undefined} */
109
+ this.shadowMap = undefined;
110
+ /** @property {WebGLTexture|undefined} - One of the two textures each light's shadow is built in
111
+ * @type {WebGLTexture|undefined} */
112
+ this.shadowTextureA = undefined;
113
+ /** @property {WebGLTexture|undefined} - The other
114
+ * @type {WebGLTexture|undefined} */
115
+ this.shadowTextureB = undefined;
116
+ /** @property {WebGLProgram|undefined} - Copies the shadow map around a light into its texture
117
+ * @type {WebGLProgram|undefined} */
118
+ this.shadowCopyShader = undefined;
119
+ /** @property {WebGLProgram|undefined} - One stretch pass of a light's shadow texture
120
+ * @type {WebGLProgram|undefined} */
121
+ this.shadowStretchShader = undefined;
122
+ /** @property {WebGLVertexArrayObject|undefined} - Vertex array object for the copy shader
123
+ * @type {WebGLVertexArrayObject|undefined} */
124
+ this.shadowCopyVAO = undefined;
125
+ /** @property {WebGLVertexArrayObject|undefined} - Vertex array object for the stretch shader
126
+ * @type {WebGLVertexArrayObject|undefined} */
127
+ this.shadowStretchVAO = undefined;
128
+ /** @property {OffscreenCanvasRenderingContext2D|undefined} - Where Canvas2D draws go during the shadow and emissive passes, a 1x1 canvas, so text in a render() is not drawn twice
129
+ * @type {OffscreenCanvasRenderingContext2D|undefined} */
130
+ this.shadowContext = undefined;
131
+ /** @property {Vector2} - World position of the shadow map's bottom left corner, set each shadow pass */
132
+ this.shadowMapOrigin = vec2();
133
+ /** @property {number} - World size the shadow map covers, set each shadow pass */
134
+ this.shadowMapWorldSize = 0;
135
+ this.shadowMapSizeAllocated = 0; // sizes the textures were made at, to remake them on a change
136
+ this.shadowTextureSizeAllocated = 0;
137
+
138
+ initLightSystem();
139
+ engineAddPlugin(undefined, lightSystemRender,
140
+ lightSystemContextLost, lightSystemContextRestored);
141
+
142
+ function initLightSystem()
143
+ {
144
+ if (headlessMode) return;
145
+ if (!glEnable)
146
+ {
147
+ console.warn('LightSystemPlugin: WebGL not enabled!');
148
+ return;
149
+ }
150
+
151
+ // where Canvas2D draws go during the shadow and emissive passes
152
+ lightSystem.shadowContext ||= new OffscreenCanvas(1, 1).getContext('2d');
153
+
154
+ // resolve texture size default at init time (mainCanvasSize may
155
+ // not be set yet at the moment the constructor first ran), and
156
+ // again on a context restore, the canvas may have changed since
157
+ if (lightSystem.textureSizeAuto)
158
+ lightSystem.textureSize = mainCanvasSize.copy();
159
+
160
+ // allocate the lightmap texture with null data at textureSize
161
+ lightSystem.texture = glContext.createTexture();
162
+ glContext.bindTexture(glContext.TEXTURE_2D, lightSystem.texture);
163
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA,
164
+ lightSystem.textureSize.x, lightSystem.textureSize.y, 0,
165
+ glContext.RGBA, glContext.UNSIGNED_BYTE, null);
166
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MAG_FILTER, glContext.LINEAR);
167
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, glContext.LINEAR);
168
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_S, glContext.CLAMP_TO_EDGE);
169
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_T, glContext.CLAMP_TO_EDGE);
170
+
171
+ // light falloff shader: one quad per Light, fragment computes radial falloff
172
+ lightSystem.lightShader = glCreateProgram(
173
+ '#version 300 es\n' +
174
+ 'precision highp float;'+
175
+ 'uniform mat4 m;'+
176
+ 'uniform vec2 lightPos;'+
177
+ 'uniform float radius;'+
178
+ 'in vec2 g;'+ // unit quad geometry [0..1]
179
+ 'out vec2 vWorldPos;'+
180
+ 'out vec2 vUV;'+ // across the light's quad, the shadow texture's uv
181
+ 'void main(){'+
182
+ 'vec2 worldP=lightPos+(g-.5)*2.*radius;'+
183
+ 'gl_Position=m*vec4(worldP,1,1);'+
184
+ 'vWorldPos=worldP;'+
185
+ 'vUV=g;'+
186
+ '}'
187
+ ,
188
+ '#version 300 es\n' +
189
+ 'precision highp float;'+
190
+ 'uniform vec2 lightPos;'+
191
+ 'uniform float radius;'+
192
+ 'uniform float fadeRange;'+
193
+ 'uniform vec4 color;'+
194
+ 'uniform sampler2D shadowTexture;'+ // this light's shadow, white where its rays reach
195
+ 'uniform bool useShadow;'+
196
+ 'in vec2 vWorldPos;'+
197
+ 'in vec2 vUV;'+
198
+ 'out vec4 c;'+
199
+ 'void main(){'+
200
+ 'float dist=distance(vWorldPos,lightPos);'+
201
+ 'float t=clamp((radius-dist)/max(fadeRange,1e-6),0.,1.);'+
202
+ 'c=vec4(color.rgb*t*color.a,1.);'+
203
+ 'if(useShadow)c.rgb*=texture(shadowTexture,vUV).rgb;'+
204
+ '}'
205
+ );
206
+ // the shadow texture is on unit 1, the engine's tracked texture stays on unit 0
207
+ glContext.useProgram(lightSystem.lightShader);
208
+ glContext.uniform1i(glUniformLocation(lightSystem.lightShader, 'shadowTexture'), 1);
209
+
210
+ // composite shader: fullscreen quad, samples the lightmap
211
+ lightSystem.compositeShader = glCreateProgram(
212
+ '#version 300 es\n' +
213
+ 'precision highp float;'+
214
+ 'in vec2 p;'+
215
+ 'void main(){'+
216
+ 'gl_Position=vec4(p+p-1.,1,1);'+
217
+ '}'
218
+ ,
219
+ '#version 300 es\n' +
220
+ 'precision highp float;'+
221
+ 'uniform sampler2D s;'+
222
+ 'uniform vec3 iResolution;'+
223
+ 'out vec4 c;'+
224
+ 'void main(){'+
225
+ 'vec2 uv=gl_FragCoord.xy/iResolution.xy;'+
226
+ 'c=vec4(texture(s,uv).rgb,1.);'+
227
+ '}'
228
+ );
229
+
230
+ // one quad VAO per program, the engine's unit triangle strip through the named attribute
231
+ lightSystem.lightVAO = createQuadVAO(lightSystem.lightShader, 'g');
232
+ lightSystem.compositeVAO = createQuadVAO(lightSystem.compositeShader, 'p');
233
+ }
234
+ function createQuadVAO(program, attribute)
235
+ {
236
+ const gl = glContext, vao = gl.createVertexArray();
237
+ gl.bindVertexArray(vao);
238
+ gl.bindBuffer(gl.ARRAY_BUFFER, glGeometryBuffer);
239
+ const location = gl.getAttribLocation(program, attribute);
240
+ gl.enableVertexAttribArray(location);
241
+ gl.vertexAttribPointer(location, 2, gl.FLOAT, false, 8, 0);
242
+ return vao;
243
+ }
244
+ function createTexture(size)
245
+ {
246
+ const gl = glContext, texture = gl.createTexture();
247
+ gl.bindTexture(gl.TEXTURE_2D, texture);
248
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, size, size, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
249
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
250
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
251
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
252
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
253
+ return texture;
254
+ }
255
+ function initShadows()
256
+ {
257
+ const gl = glContext, ls = lightSystem;
258
+ ls.shadowMap = createTexture(ls.shadowMapSize);
259
+ ls.shadowTextureA = createTexture(ls.shadowTextureSize);
260
+ ls.shadowTextureB = createTexture(ls.shadowTextureSize);
261
+ ls.shadowMapSizeAllocated = ls.shadowMapSize;
262
+ ls.shadowTextureSizeAllocated = ls.shadowTextureSize;
263
+ // put back the texture the engine tracks
264
+ if (glActiveTexture)
265
+ gl.bindTexture(gl.TEXTURE_2D, glActiveTexture);
266
+
267
+ const quadVertex =
268
+ '#version 300 es\n' +
269
+ 'precision highp float;'+
270
+ 'in vec2 p;'+ // unit quad [0..1]
271
+ 'out vec2 uv;'+
272
+ 'void main(){gl_Position=vec4(p+p-1.,1,1);uv=p;}';
273
+
274
+ // copy: the shadow map around the light into the light's texture, light at the center
275
+ ls.shadowCopyShader = glCreateProgram(quadVertex,
276
+ '#version 300 es\n' +
277
+ 'precision highp float;'+
278
+ 'uniform sampler2D s;'+ // the shadow map
279
+ 'uniform vec2 lightPos;'+
280
+ 'uniform float radius;'+
281
+ 'uniform vec2 mapOrigin;'+ // world bottom left of the map
282
+ 'uniform float mapInvSize;'+ // 1 over the world size it covers
283
+ 'uniform float tap;'+ // half a texel of the light's texture, in world units
284
+ 'uniform float core;'+ // radius around the light where casters are left out
285
+ 'in vec2 uv;'+
286
+ 'out vec4 c;'+
287
+ 'void main(){'+
288
+ 'vec2 w=lightPos+(uv-.5)*2.*radius;'+ // world position of this texel
289
+ 'vec3 o=vec3(1);'+
290
+ // the darkest of four taps a half texel out, so a caster a texel thin still covers two whole
291
+ // texels, the stretch's filtering between two would leak through one
292
+ 'for(int i=0;i<4;i++){'+
293
+ 'vec2 m=(w+tap*(vec2(i%2,i/2)*2.-1.)-mapOrigin)*mapInvSize;'+ // in the map, world up at v=0
294
+ // past the map's edge is open, clamping would stretch its edge texels across the light
295
+ 'o=min(o,m==clamp(m,0.,1.)?texture(s,vec2(m.x,1.-m.y)).rgb:vec3(1));'+
296
+ '}'+
297
+ 'c=vec4(length(w-lightPos)<core?vec3(1):o,1);'+
298
+ '}');
299
+
300
+ // stretch: each texel keeps the darkest of itself and the texel toward the light, so a caster's
301
+ // alpha and color carry through as they are; the bleed lights a caster's near side only where
302
+ // the ray from the light still reaches, so it never lifts a texel already in shadow
303
+ ls.shadowStretchShader = glCreateProgram(quadVertex,
304
+ '#version 300 es\n' +
305
+ 'precision highp float;'+
306
+ 'uniform sampler2D s;'+ // the previous pass
307
+ 'uniform float scale;'+ // how much further out this pass pushes the casters
308
+ 'uniform float brightness;'+ // light bled into the near side of casters this pass
309
+ 'in vec2 uv;'+
310
+ 'out vec4 c;'+
311
+ 'void main(){'+
312
+ 'float mask=1.-smoothstep(.35,.525,length(uv-.5));'+ // full out to 70% of the radius, FrankEngine's light mask
313
+ 'vec3 b=texture(s,(uv-.5)/scale+.5).rgb;'+
314
+ 'c=vec4(min(texture(s,uv).rgb+brightness*mask*b,b),1);'+
315
+ '}');
316
+
317
+ ls.shadowCopyVAO = createQuadVAO(ls.shadowCopyShader, 'p');
318
+ ls.shadowStretchVAO = createQuadVAO(ls.shadowStretchShader, 'p');
319
+
320
+ // the plugin samples on unit 1 so the engine's tracked texture on unit 0 is untouched
321
+ gl.useProgram(ls.shadowCopyShader);
322
+ gl.uniform1i(glUniformLocation(ls.shadowCopyShader, 's'), 1);
323
+ gl.useProgram(ls.shadowStretchShader);
324
+ gl.uniform1i(glUniformLocation(ls.shadowStretchShader, 's'), 1);
325
+ }
326
+ function freeShadows()
327
+ {
328
+ const gl = glContext, ls = lightSystem;
329
+ gl.deleteTexture(ls.shadowMap);
330
+ gl.deleteTexture(ls.shadowTextureA);
331
+ gl.deleteTexture(ls.shadowTextureB);
332
+ gl.deleteProgram(ls.shadowCopyShader);
333
+ gl.deleteProgram(ls.shadowStretchShader);
334
+ gl.deleteVertexArray(ls.shadowCopyVAO);
335
+ gl.deleteVertexArray(ls.shadowStretchVAO);
336
+ clearShadows();
337
+ }
338
+ function clearShadows()
339
+ {
340
+ const ls = lightSystem;
341
+ ls.shadowMap = ls.shadowTextureA = ls.shadowTextureB = undefined;
342
+ ls.shadowCopyShader = ls.shadowStretchShader = undefined;
343
+ ls.shadowCopyVAO = ls.shadowStretchVAO = undefined;
344
+ }
345
+ function lightSystemShadowPass()
346
+ {
347
+ const ls = lightSystem;
348
+
349
+ // make the resources the first time, and again when a size changed
350
+ if (!ls.shadowMap || ls.shadowMapSize !== ls.shadowMapSizeAllocated
351
+ || ls.shadowTextureSize !== ls.shadowTextureSizeAllocated)
352
+ {
353
+ ls.shadowMap && freeShadows();
354
+ initShadows();
355
+ }
356
+
357
+ // a square of world space around the camera, rounded to its own texels so the
358
+ // grid stays put in the world as the camera moves, or the shadows would shimmer
359
+ const size = ls.shadowMapSize;
360
+ const view = mainCanvasSize.scale(1/cameraScale);
361
+ const worldSize = ls.shadowMapScale * max(view.x, view.y);
362
+ const texel = worldSize / size;
363
+ const center = vec2(floor(cameraPos.x/texel)*texel, floor(cameraPos.y/texel)*texel);
364
+ ls.shadowMapOrigin = center.subtract(vec2(worldSize/2));
365
+ ls.shadowMapWorldSize = worldSize;
366
+
367
+ // draw with the map's camera, the way a tile layer redraw does; Canvas2D draws
368
+ // go to a 1x1 canvas so text in a render() does not reach the screen twice
369
+ const saved = [drawContext, mainCanvasSize, cameraPos, cameraScale, cameraAngle, canvasClearColor, glCustomShader];
370
+ drawContext = ls.shadowContext;
371
+ mainCanvasSize = vec2(size);
372
+ cameraPos = center;
373
+ cameraScale = size / worldSize;
374
+ cameraAngle = 0;
375
+ canvasClearColor = WHITE;
376
+ glSetRenderTarget(ls.shadowMap, true);
377
+ glSkipScreenSpace = true; // the map's camera would put them anywhere
378
+ ls.shadowPass = true;
379
+ try
380
+ {
381
+ for (const o of engineObjects)
382
+ {
383
+ if (o.destroyed || !o.castShadow) continue;
384
+ // every color black, its alpha kept, set for each object since a render that left
385
+ // setShadowTransparent on ends with it
386
+ glColorMask = 0xff000000;
387
+ setShader(o.shader); // its own Shader as in the main pass, so a snippet that cuts holes casts the same shape
388
+ o.renderShadow();
389
+ }
390
+ }
391
+ finally
392
+ {
393
+ // hand everything back even when a render threw, or every frame after it draws black with no text
394
+ ls.shadowPass = false;
395
+ glColorMask = -1;
396
+ glSkipScreenSpace = false;
397
+ glSetRenderTarget();
398
+ [drawContext, mainCanvasSize, cameraPos, cameraScale, cameraAngle, canvasClearColor, glCustomShader] = saved;
399
+ }
400
+ }
401
+ function lightSystemRender()
402
+ {
403
+ if (headlessMode || !glEnable) return;
404
+ if (!lightSystem.enabled) return;
405
+ if (!lightSystem.texture)
406
+ {
407
+ // made now if WebGL was off when the plugin was made, or when a lost context came back
408
+ if (glContext.isContextLost()) return;
409
+ initLightSystem();
410
+ if (!lightSystem.texture) return;
411
+ }
412
+
413
+ // 1. flush any in-flight sprite batch from earlier render passes
414
+ glFlush();
415
+ const prevAdditive = glAdditive;
416
+
417
+ // 1b. the shadow pass draws every caster black into the shadow map
418
+ if (lightSystem.shadows)
419
+ lightSystemShadowPass();
420
+
421
+ // an automatic size follows the canvas, so reallocate the lightmap when
422
+ // the canvas changed size, after the flush so the batch keeps its texture
423
+ const size = lightSystem.textureSize;
424
+ if (lightSystem.textureSizeAuto &&
425
+ (size.x !== mainCanvasSize.x || size.y !== mainCanvasSize.y))
426
+ {
427
+ lightSystem.textureSize = mainCanvasSize.copy();
428
+ glContext.bindTexture(glContext.TEXTURE_2D, lightSystem.texture);
429
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA,
430
+ mainCanvasSize.x, mainCanvasSize.y, 0,
431
+ glContext.RGBA, glContext.UNSIGNED_BYTE, null);
432
+ // put back the texture the engine tracks, a draw in renderLight must not sample the lightmap
433
+ if (glActiveTexture)
434
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
435
+ }
436
+
437
+ // 2. bind lightmap as render target, clear to ambientColor
438
+ const ac = lightSystem.ambientColor;
439
+ glContext.bindFramebuffer(glContext.FRAMEBUFFER, glFramebuffer);
440
+ glContext.framebufferTexture2D(glContext.FRAMEBUFFER,
441
+ glContext.COLOR_ATTACHMENT0, glContext.TEXTURE_2D, lightSystem.texture, 0);
442
+ glContext.viewport(0, 0, lightSystem.textureSize.x, lightSystem.textureSize.y);
443
+ glContext.clearColor(ac.r, ac.g, ac.b, ac.a);
444
+ glContext.clear(glContext.COLOR_BUFFER_BIT);
445
+
446
+ // 3. walk engineObjects calling renderLight() — additive blend
447
+ // (lightmap accumulates raw additive color contributions)
448
+ setAdditiveBlendMode();
449
+ glContext.enable(glContext.BLEND);
450
+ glContext.blendFunc(glContext.ONE, glContext.ONE);
451
+
452
+ // the camera transform is the same for every light, and a uniform
453
+ // keeps its value per program, so set it once for the whole pass.
454
+ // It is the canvas transform glPreRender built: world→NDC over
455
+ // mainCanvasSize (not textureSize), the viewport handles the
456
+ // lightmap's actual resolution. No y-flip: the composite samples
457
+ // this FBO with gl_FragCoord/iResolution (origin bottom-left), so
458
+ // storing world +Y at the top of the texture lines up with the canvas.
459
+ const ls = lightSystem.lightShader;
460
+ glContext.useProgram(ls);
461
+ glContext.uniformMatrix4fv(glUniformLocation(ls, 'm'), false, glTransform);
462
+ glSetInstancedMode(true);
463
+
464
+ // a target set and ended while the lights draw, a tile layer redrawn in a renderLight, comes back here
465
+ glRenderTargetBase = [lightSystem.texture, lightSystem.textureSize];
466
+ try
467
+ {
468
+ for (const o of engineObjects)
469
+ {
470
+ if (o.destroyed) continue;
471
+ glAdditive || setAdditiveBlendMode(); // added again, an emitter ends its render with it off
472
+ o.renderLight();
473
+ }
474
+
475
+ // 3b. emissive objects draw their shape in grey at their emissive level, white at 1, adding that much
476
+ // light where they are so they show their own colors; text goes to the 1x1 canvas as in the
477
+ // shadow pass, so it is not drawn twice
478
+ const saved = [drawContext, glCustomShader];
479
+ drawContext = lightSystem.shadowContext;
480
+ lightSystem.emissivePass = true;
481
+ try
482
+ {
483
+ for (const o of engineObjects)
484
+ {
485
+ if (o.destroyed || !(o.emissive > 0)) continue;
486
+ const level = clamp(o.emissive)*255+.5|0; // packed like rgbaInt, red in the low byte
487
+ glColorMask = 0xff000000; // its own alpha, and the grey from the additive color
488
+ glColorAdditive = level | level<<8 | level<<16;
489
+ glAdditive || setAdditiveBlendMode(); // added, an emitter ends its render with it off
490
+ setShader(o.shader);
491
+ o.renderEmissive();
492
+ }
493
+ glFlush();
494
+ }
495
+ finally
496
+ {
497
+ lightSystem.emissivePass = false;
498
+ glColorMask = -1;
499
+ glColorAdditive = 0;
500
+ [drawContext, glCustomShader] = saved;
501
+ }
502
+ }
503
+ finally
504
+ {
505
+ glRenderTargetBase = undefined;
506
+ }
507
+
508
+ // 4. drain any sprite-batched draws (e.g. drawTile inside a
509
+ // custom renderLight override) so they hit the FBO, not the
510
+ // canvas after we unbind
511
+ glFlush();
512
+ glContext.bindFramebuffer(glContext.FRAMEBUFFER, null);
513
+
514
+ // backing store size, mainCanvasSize is css pixels
515
+ glContext.viewport(0, 0, glCanvas.width, glCanvas.height);
516
+
517
+ // 5. composite: fullscreen quad, multiplicative blend onto glCanvas
518
+ // (scene * lightmap — unlit areas go to black, lit areas are
519
+ // the scene tinted by the accumulated light color)
520
+ glContext.useProgram(lightSystem.compositeShader);
521
+ glContext.bindVertexArray(lightSystem.compositeVAO);
522
+ glContext.activeTexture(glContext.TEXTURE0);
523
+ glContext.bindTexture(glContext.TEXTURE_2D, lightSystem.texture);
524
+ const cs = lightSystem.compositeShader;
525
+ glContext.uniform1i(glUniformLocation(cs, 's'), 0);
526
+ glContext.uniform3f(glUniformLocation(cs, 'iResolution'),
527
+ mainCanvas.width, mainCanvas.height, 1);
528
+ glContext.blendFunc(glContext.DST_COLOR, glContext.ZERO);
529
+ glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, 4);
530
+
531
+ // 6. restore engine state so subsequent draws use the engine's
532
+ // tracked texture binding (otherwise glSetTexture would think
533
+ // the prior texture was still bound when actually the lightmap
534
+ // is, and any debug text / future draw could sample the lightmap)
535
+ if (glActiveTexture)
536
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
537
+ setAdditiveBlendMode(prevAdditive);
538
+ glSetInstancedMode(true);
539
+ }
540
+ function lightSystemContextLost()
541
+ {
542
+ lightSystem.texture = undefined;
543
+ lightSystem.lightShader = undefined;
544
+ lightSystem.compositeShader = undefined;
545
+ lightSystem.lightVAO = undefined;
546
+ lightSystem.compositeVAO = undefined;
547
+ clearShadows();
548
+ LOG('LightSystemPlugin: WebGL context lost');
549
+ }
550
+ function lightSystemContextRestored()
551
+ {
552
+ initLightSystem();
553
+ LOG('LightSystemPlugin: WebGL context restored');
554
+ }
555
+ }
556
+
557
+ /** Draw a single Light's falloff blob into the currently bound lightmap.
558
+ * Called by Light.renderLight() during the plugin's render pass.
559
+ * @param {Light} light */
560
+ drawLight(light)
561
+ {
562
+ if (headlessMode || !glEnable || !this.lightShader) return;
563
+
564
+ // skip a light that can not touch the screen, its quad is the full
565
+ // radius out from its center on every side
566
+ if (!isOnScreen(light.pos, light.radius*2)) return;
567
+
568
+ // drain any sprite-batched draws queued by a previous custom
569
+ // renderLight() override (e.g. drawRect inside a LavaTile). They were
570
+ // queued in the engine's instanced-vertex format and must flush with
571
+ // the engine's shader+VAO bound — NOT this plugin's light shader.
572
+ glFlush();
573
+
574
+ // a shadow casting light builds its shadow texture first, leaving it on unit 1 and the lightmap bound
575
+ const useShadow = this.shadows && light.castShadow && !!this.shadowMap;
576
+ useShadow && this.renderLightShadow(light);
577
+
578
+ glContext.useProgram(this.lightShader);
579
+ glContext.bindVertexArray(this.lightVAO);
580
+ glContext.enable(glContext.BLEND);
581
+ glContext.blendFunc(glContext.ONE, glContext.ONE); // added, whatever a tile layer redrawn before it left set
582
+
583
+ // the camera transform 'm' was set once for the pass by the plugin
584
+ const ls = this.lightShader;
585
+ glContext.uniform2f(glUniformLocation(ls, 'lightPos'), light.pos.x, light.pos.y);
586
+ glContext.uniform1f(glUniformLocation(ls, 'radius'), light.radius);
587
+ glContext.uniform1f(glUniformLocation(ls, 'fadeRange'), light.fadeRange);
588
+ glContext.uniform1i(glUniformLocation(ls, 'useShadow'), useShadow ? 1 : 0);
589
+ const c = light.color;
590
+ glContext.uniform4f(glUniformLocation(ls, 'color'), c.r, c.g, c.b, c.a);
591
+
592
+ glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, 4);
593
+
594
+ // restore engine's instanced shader+VAO so subsequent renderLight()
595
+ // overrides that batch through drawRect/drawTile work correctly
596
+ glSetInstancedMode(true);
597
+ }
598
+
599
+ /** Build a light's shadow texture from the shadow map: the map around the light, its casters stretched
600
+ * away from the light a little further each pass with light bled into their near sides. Leaves the
601
+ * result on texture unit 1 and the lightmap bound again. Called by drawLight.
602
+ * @param {Light} light */
603
+ renderLightShadow(light)
604
+ {
605
+ const gl = glContext, size = this.shadowTextureSize;
606
+ gl.disable(gl.BLEND);
607
+ gl.viewport(0, 0, size, size);
608
+ gl.activeTexture(gl.TEXTURE1);
609
+
610
+ // the shadow map around the light, light at the center of the texture
611
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.shadowTextureA, 0);
612
+ const cs = this.shadowCopyShader;
613
+ gl.useProgram(cs);
614
+ gl.bindVertexArray(this.shadowCopyVAO);
615
+ gl.bindTexture(gl.TEXTURE_2D, this.shadowMap);
616
+ gl.uniform2f(glUniformLocation(cs, 'lightPos'), light.pos.x, light.pos.y);
617
+ gl.uniform1f(glUniformLocation(cs, 'radius'), light.radius);
618
+ gl.uniform2f(glUniformLocation(cs, 'mapOrigin'), this.shadowMapOrigin.x, this.shadowMapOrigin.y);
619
+ gl.uniform1f(glUniformLocation(cs, 'mapInvSize'), 1/this.shadowMapWorldSize);
620
+ gl.uniform1f(glUniformLocation(cs, 'tap'), light.radius/size);
621
+ gl.uniform1f(glUniformLocation(cs, 'core'), light.shadowCore);
622
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
623
+
624
+ // stretch the casters out from the light, starting a 128th of the texture and growing 1.5x a pass, which
625
+ // reaches the edge in about 16 passes with no gaps since each pass scales by less than the shadow already
626
+ // extends; small steps, so the bleed does not show as stairs along the shadow edges; a fraction of the
627
+ // texture rather than a count of texels, so a larger texture only makes the shadows sharper, not shorter;
628
+ // the bleed fades out over the first 10 passes, the distances FrankEngine's soften covered at 1.8x
629
+ const ss = this.shadowStretchShader;
630
+ gl.useProgram(ss);
631
+ gl.bindVertexArray(this.shadowStretchVAO);
632
+ let src = this.shadowTextureA, dst = this.shadowTextureB;
633
+ for (let k = 0; k < this.shadowPassCount; ++k)
634
+ {
635
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, dst, 0);
636
+ gl.bindTexture(gl.TEXTURE_2D, src);
637
+ gl.uniform1f(glUniformLocation(ss, 'scale'), 1 + 2*1.5**k/256);
638
+ gl.uniform1f(glUniformLocation(ss, 'brightness'), clamp((10.1-k)/7.2)**2 * this.shadowSoftness);
639
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
640
+ [src, dst] = [dst, src];
641
+ }
642
+
643
+ // leave the result on unit 1 for the light shader and go back to the lightmap
644
+ gl.bindTexture(gl.TEXTURE_2D, src);
645
+ gl.activeTexture(gl.TEXTURE0);
646
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texture, 0);
647
+ gl.viewport(0, 0, this.textureSize.x, this.textureSize.y);
648
+ gl.enable(gl.BLEND);
649
+ gl.blendFunc(gl.ONE, gl.ONE);
650
+ }
651
+
652
+ /** In the shadow pass, let the draws that follow keep their color in the shadow map, so light passing
653
+ * through them is tinted instead of blocked: a stained glass window, colored smoke. Any draw blocks light
654
+ * by its alpha, so a fading sprite casts a fading shadow; this keeps the color as well. It covers what was drawn
655
+ * under it in the map as any draw does, so glass drawn after a wall cuts a tinted window in the wall's shadow, and
656
+ * a wall drawn after the glass covers it. Does nothing outside
657
+ * the pass, so a render() can call it around those draws unconditionally; set it back to false after them.
658
+ * @param {boolean} [transparent] */
659
+ setShadowTransparent(transparent=true)
660
+ {
661
+ // the mask applies as each draw is queued, so nothing needs flushing
662
+ if (this.shadowPass)
663
+ glColorMask = transparent ? -1 : 0xff000000;
664
+ }
665
+ }
666
+
667
+ ///////////////////////////////////////////////////////////////////////////////
668
+
669
+ /**
670
+ * A Light is an EngineObject that contributes a soft additive blob of color
671
+ * to the LightSystem plugin's lightmap.
672
+ * - castShadow on a Light means its rays stop at casters when lightSystem.shadows is on, three.js's meaning
673
+ * for a light; a Light's own render() draws nothing so the object meaning never applies to it
674
+ * - A light inside a caster is blocked entirely, so the object that holds it, its lamp, a torch, the player
675
+ * carrying it, needs a shadowCore that reaches past it, castShadow = false, or a renderShadow that leaves
676
+ * the light's spot out
677
+ * @extends EngineObject
678
+ * @memberof LightSystem
679
+ * @example
680
+ * new Light(vec2(5, 5), 4, rgb(1, 0.5, 0)); // orange light, full soft blob
681
+ * new Light(vec2(0, 0), 8, rgb(1, 1, 1), 2); // white core with 2-unit soft halo
682
+ */
683
+ class Light extends EngineObject
684
+ {
685
+ /** Create a light object and add it to the engine object list
686
+ * @param {Vector2} pos - World space position
687
+ * @param {number} radius - Total extent of the light in world units
688
+ * @param {Color} [color] - Color of the light; alpha modulates intensity
689
+ * @param {number} [fadeRange] - Width of the soft edge in world units (defaults to radius) */
690
+ constructor(pos, radius, color, fadeRange)
691
+ {
692
+ super(pos, vec2(1), undefined, 0, color);
693
+ this.mass = 0; // static, a light stays where it is put in a game with gravity
694
+ ASSERT(isNumber(radius) && radius >= 0, 'Light radius must be a non-negative number');
695
+ ASSERT(fadeRange === undefined || (isNumber(fadeRange) && fadeRange >= 0),
696
+ 'Light fadeRange must be a non-negative number when provided');
697
+
698
+ /** @property {number} - Total extent of the light in world units */
699
+ this.radius = radius;
700
+ /** @property {number} - Width of the soft edge in world units */
701
+ this.fadeRange = fadeRange === undefined ? radius : fadeRange;
702
+ /** @property {number} - Radius around the light where casters are left out of its shadow, so the lamp
703
+ * or torch that holds it, or the player carrying it, does not block it; it has to reach past that object's
704
+ * corners, about half its diagonal and a little more, or dark rays run out from them */
705
+ this.shadowCore = 0;
706
+ }
707
+
708
+ /** Lights are invisible in the main render pass — they only contribute
709
+ * to the lightmap via renderLight(). */
710
+ render() {}
711
+
712
+ /** Draw this light's falloff blob into the lightmap.
713
+ * Called by LightSystemPlugin during its render pass. No-op when the
714
+ * plugin or WebGL is unavailable. */
715
+ renderLight()
716
+ {
717
+ lightSystem && lightSystem.drawLight(this);
718
+ }
719
+ }