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.
- package/FAQ.md +841 -820
- package/README.md +29 -25
- package/dist/littlejs.d.ts +2455 -935
- package/dist/littlejs.esm.js +30344 -21110
- package/dist/littlejs.esm.min.js +4 -3
- package/dist/littlejs.js +30259 -21052
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +26567 -20455
- package/package.json +10 -5
- package/plugins/audioEffects.js +28 -24
- package/plugins/box2d.js +2516 -2073
- package/plugins/drawUtilities.js +206 -31
- package/plugins/gltf.js +740 -0
- package/plugins/jsconfig.json +19 -0
- package/plugins/lightSystem.js +719 -332
- package/plugins/math3d.js +58 -17
- package/plugins/medalSystem.js +326 -280
- package/plugins/newgrounds.js +400 -199
- package/plugins/pathFinder.js +208 -175
- package/plugins/pluginExport.js +15 -0
- package/plugins/postProcess.js +56 -13
- package/plugins/render3d.js +1643 -1577
- package/plugins/render3dDebug.js +109 -0
- package/plugins/render3dExtras.js +1754 -0
- package/plugins/render3dVoxels.js +502 -0
- package/plugins/textureSheet.js +496 -458
- package/plugins/threejs.js +183 -163
- package/plugins/tweakables.js +411 -0
- package/plugins/tweenSystem.js +263 -145
- package/plugins/uiSystem.js +2024 -1687
- package/plugins/zzfxm.js +159 -159
- package/src/engine.js +191 -75
- package/src/engineAudio.js +229 -125
- package/src/engineBuild.mjs +289 -256
- package/src/engineDebug.js +912 -742
- package/src/engineDraw.js +295 -193
- package/src/engineEditor.js +2431 -0
- package/src/engineExport.js +12 -0
- package/src/engineInput.js +1462 -1293
- package/src/engineMath.js +1274 -1252
- package/src/engineObject.js +194 -80
- package/src/engineParticles.js +583 -532
- package/src/engineRelease.js +67 -45
- package/src/engineSettings.js +77 -29
- package/src/engineTileLayer.js +995 -741
- package/src/engineUtilities.js +19 -13
- package/src/engineWebGL.js +239 -82
- package/src/jsconfig.json +17 -13
- package/plugins/box2d.wasm.js +0 -630
- package/plugins/box2d.wasm.wasm +0 -0
package/plugins/lightSystem.js
CHANGED
|
@@ -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
|
-
* -
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
/** @property {
|
|
64
|
-
this.
|
|
65
|
-
/** @property {
|
|
66
|
-
this.
|
|
67
|
-
/** @property {
|
|
68
|
-
this.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
{
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
glContext.
|
|
163
|
-
glContext.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
glContext.
|
|
208
|
-
glContext.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
lightSystem.
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
function
|
|
235
|
-
{
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
*
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
+
}
|