littlejsengine 1.20.0 → 1.22.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.
@@ -29,6 +29,15 @@ export
29
29
  PostProcessPlugin,
30
30
  postProcessBloom,
31
31
  postProcessBloomShader,
32
+ postProcessEffects,
33
+ postProcessGlow,
34
+ postProcessScanlines,
35
+ postProcessNoise,
36
+ postProcessVignette,
37
+ postProcessCurve,
38
+ postProcessChromatic,
39
+ postProcessOutline,
40
+ postProcessTV,
32
41
 
33
42
  // Light System
34
43
  lightSystem,
@@ -103,6 +112,10 @@ export
103
112
  tweenUpdate,
104
113
  Ease,
105
114
 
115
+ // Scene System
116
+ setScene,
117
+ getScene,
118
+
106
119
  // Path Finding
107
120
  PathFinder,
108
121
  PathFinderNode,
@@ -151,6 +164,7 @@ export
151
164
  buildRibbon,
152
165
  buildLoft,
153
166
  buildSky,
167
+ normalMapFromHeight,
154
168
  buildExtrude,
155
169
  buildText3D,
156
170
  HeightMap,
@@ -161,6 +175,7 @@ export
161
175
  FirstPersonCamera3D,
162
176
  ParticleEmitter3D,
163
177
  Trail3D,
178
+ LensFlare3D,
164
179
  engineObjectsCollect3D,
165
180
  engineObjectsCallback3D,
166
181
  engineObjectsRaycast3D,
@@ -177,6 +192,12 @@ export
177
192
  debugLine3D,
178
193
  debugPoint3D,
179
194
 
195
+ // 3D Level
196
+ level3DAddType,
197
+ level3DAddMesh,
198
+ level3DLoad,
199
+ level3DVoxelSetup,
200
+
180
201
  // Three.js
181
202
  threeJS,
182
203
  ThreeJSPlugin,
@@ -193,4 +214,25 @@ export
193
214
  loadAtlas,
194
215
  parseAtlas,
195
216
  spritesReady,
217
+
218
+ // Particle Effects
219
+ particleEffectGroups,
220
+ particleEffectSettings,
221
+ particleEffectBehaviors,
222
+ particleEffectShapes,
223
+ particleEffectSanitize,
224
+ particleEffectRecolor,
225
+ particleEffectsAdd,
226
+ particleEffectsGet,
227
+ particleEffectsParse,
228
+ particleEffectsText,
229
+ particleEffectsLoad,
230
+ particleEffectShapeTile,
231
+ particleEffect,
232
+ particleEffect3D,
233
+ particleEffectsBuiltIn,
234
+ particleEffectApply,
235
+ particleEffectApply3D,
236
+ particleEffectFromEmitter,
237
+ particleEffectsAddBehavior,
196
238
  }
@@ -46,8 +46,8 @@ class PostProcessPlugin
46
46
  ASSERT(!(includeMainCanvas && feedbackTexture), 'Post process cannot both include main canvas and use feedback texture');
47
47
  postProcess = this;
48
48
 
49
- if (!shaderCode) // default shader pass through
50
- shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
49
+ /** @property {string} - The shadertoy style mainImage code it shades with, see setShaderCode */
50
+ this.shaderCode = shaderCode || postProcessEffects(); // no code passes the frame through
51
51
 
52
52
  /** @property {WebGLProgram|undefined} - Shader for post processing
53
53
  * @type {WebGLProgram|undefined} */
@@ -93,18 +93,7 @@ class PostProcessPlugin
93
93
  'gl_Position=vec4(p+p-1.,1,1);'+ // set position
94
94
  '}' // end of shader
95
95
  ,
96
- '#version 300 es\n' + // specify GLSL ES version
97
- 'precision highp float;'+ // use highp for accuracy
98
- 'uniform sampler2D iChannel0;'+ // input texture
99
- 'uniform sampler2D iChannel1;'+ // the previous frame's output, when feedbackTexture is set
100
- 'uniform vec3 iResolution;'+ // size of output texture
101
- 'uniform float iTime;'+ // time
102
- 'out vec4 c;'+ // out color
103
- '\n' + shaderCode + '\n'+ // insert custom shader code
104
- 'void main(){'+ // shader entry point
105
- 'mainImage(c,gl_FragCoord.xy);'+ // call post process function
106
- 'c.a=1.;'+ // always use full alpha
107
- '}' // end of shader
96
+ postProcessFragmentSource(postProcess.shaderCode)
108
97
  );
109
98
 
110
99
  // setup VAO for post processing
@@ -117,6 +106,10 @@ class PostProcessPlugin
117
106
  const pLocation = glContext.getAttribLocation(postProcess.shader, 'p');
118
107
  glContext.enableVertexAttribArray(pLocation);
119
108
  glContext.vertexAttribPointer(pLocation, 2, glContext.FLOAT, false, vertexByteStride, 0);
109
+
110
+ // the engine's own vertex array and buffer back, its next batch is written into the buffer bound
111
+ glContext.bindVertexArray(glPolyMode ? glPolyVAO : glInstancedVAO);
112
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
120
113
  }
121
114
  function postProcessContextLost()
122
115
  {
@@ -134,16 +127,17 @@ class PostProcessPlugin
134
127
  {
135
128
  if (headlessMode || !glEnable) return;
136
129
 
137
- // made now if WebGL was off when the plugin was made, or when a lost context came back
130
+ // clear out the buffer, before anything here binds its own
131
+ glFlush();
132
+
133
+ // made now if WebGL was off when the plugin was made, when a lost context came back, or when
134
+ // setShaderCode gave it new code
138
135
  if (!postProcess.shader)
139
136
  {
140
137
  if (glContext.isContextLost()) return;
141
138
  initPostProcess();
142
139
  }
143
140
 
144
- // clear out the buffer
145
- glFlush();
146
-
147
141
  // ensure we render to the default framebuffer (in case any earlier
148
142
  // caller this frame left a render target bound)
149
143
  glContext.bindFramebuffer(glContext.FRAMEBUFFER, null);
@@ -192,21 +186,38 @@ class PostProcessPlugin
192
186
  glContext.bindTexture(glContext.TEXTURE_2D, postProcess.feedbackTexture);
193
187
  }
194
188
 
189
+ // the 3D depth, when render3D draws it, on the third texture unit, with what sceneDepth needs to read it
190
+ const depth = typeof render3D != 'undefined' && render3D?.depthTexture ? render3D : undefined;
191
+ glContext.activeTexture(glContext.TEXTURE2);
192
+ glContext.bindTexture(glContext.TEXTURE_2D, depth?.cameraDepthTexture || null);
193
+ glContext.activeTexture(glContext.TEXTURE0);
194
+ const camera = depth?.camera;
195
+ camera && glContext.uniform3f(glUniformLocation(postProcess.shader, 'iDepthRange'), camera.near,
196
+ camera.far == Infinity ? 0 : camera.far, camera.orthographic ? 1 : 0);
197
+
195
198
  // set uniforms and draw
196
199
  const uniformLocation = (name)=>glUniformLocation(postProcess.shader, name);
197
200
  glContext.uniform1i(uniformLocation('iChannel0'), 0);
198
201
  glContext.uniform1i(uniformLocation('iChannel1'), 1);
202
+ glContext.uniform1i(uniformLocation('iChannel2'), 2);
199
203
  glContext.uniform1f(uniformLocation('iTime'), time);
200
204
  glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
201
205
  glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, 4);
202
206
 
203
207
  if (feedbackTexture)
204
208
  {
205
- // keep this frame's output for the next one, then hand the first texture unit back to the engine
209
+ // keep this frame's output for the next one, in the feedback texture still bound to the second
210
+ // unit, then hand the first texture unit back to the engine
211
+ glContext.activeTexture(glContext.TEXTURE1);
206
212
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, glCanvas);
207
213
  glContext.activeTexture(glContext.TEXTURE0);
208
214
  }
209
215
 
216
+ // the depth leaves the third unit for whatever draws next
217
+ glContext.activeTexture(glContext.TEXTURE2);
218
+ glContext.bindTexture(glContext.TEXTURE_2D, null);
219
+ glContext.activeTexture(glContext.TEXTURE0);
220
+
210
221
  // restore defaults so subsequent dynamic texture uploads aren't flipped or premultiplied
211
222
  glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, false);
212
223
  glContext.pixelStorei(glContext.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
@@ -219,10 +230,52 @@ class PostProcessPlugin
219
230
  glSetInstancedMode(true);
220
231
  }
221
232
  }
233
+
234
+ /** Shade with new code from the next frame on, to switch effects while the game runs; the shader is made again
235
+ * @param {string} [shaderCode] - Shadertoy style mainImage code, postProcessEffects builds it; none passes the
236
+ * frame through */
237
+ setShaderCode(shaderCode)
238
+ {
239
+ ASSERT(!shaderCode || typeof shaderCode === 'string', 'shader code must be a string');
240
+ this.shaderCode = shaderCode || postProcessEffects();
241
+ if (!this.shader || headlessMode || !glContext) return;
242
+
243
+ // what the new shader replaces, made again by the next render
244
+ glContext.deleteProgram(this.shader);
245
+ glContext.deleteTexture(this.texture);
246
+ this.feedbackTexture && glContext.deleteTexture(this.feedbackTexture);
247
+ glContext.deleteVertexArray(this.vao);
248
+ this.shader = this.texture = this.feedbackTexture = this.vao = undefined;
249
+ }
222
250
  }
223
251
 
224
252
  ///////////////////////////////////////////////////////////////////////////////
225
253
 
254
+ // the fragment shader of the post process pass around its mainImage snippet: the frame on iChannel0, the last output
255
+ // on iChannel1 with a feedback texture, and the 3D depth on iChannel2 when render3D.depthTexture is on, read with
256
+ // sceneDepth(uv), the distance from the camera along its view in world units, uv 0 to 1 across the screen
257
+ function postProcessFragmentSource(shaderCode)
258
+ {
259
+ return '#version 300 es\n' + // specify GLSL ES version
260
+ 'precision highp float;'+ // use highp for accuracy
261
+ 'uniform sampler2D iChannel0;'+ // input texture
262
+ 'uniform sampler2D iChannel1;'+ // the previous frame's output, when feedbackTexture is set
263
+ 'uniform sampler2D iChannel2;'+ // the 3D depth, when render3D.depthTexture is on
264
+ 'uniform vec3 iResolution;'+ // size of output texture
265
+ 'uniform float iTime;'+ // time
266
+ 'uniform vec3 iDepthRange;'+ // the camera's near, its far or 0 for none, and 1 when orthographic
267
+ 'out vec4 c;'+ // out color
268
+ // the depth texture's value back to a distance, as the camera's projection put it there
269
+ 'float sceneDepth(vec2 uv){'+
270
+ 'float d=texture(iChannel2,uv).r*2.-1.,n=iDepthRange.x,f=iDepthRange.y;'+
271
+ 'return iDepthRange.z>0.?(d*(f-n)+f+n)/2.:f>0.?2.*n*f/(f+n-d*(f-n)):2.*n/(1.-d);}'+
272
+ '\n' + shaderCode + '\n'+ // insert custom shader code
273
+ 'void main(){'+ // shader entry point
274
+ 'mainImage(c,gl_FragCoord.xy);'+ // call post process function
275
+ 'c.a=1.;'+ // always use full alpha
276
+ '}'; // end of shader
277
+ }
278
+
226
279
  /**
227
280
  * Shader code for a bloom effect, the bright parts of the image blurred back over it
228
281
  * - Pass it to PostProcessPlugin, or edit the string to build an effect on top of it
@@ -234,9 +287,66 @@ class PostProcessPlugin
234
287
  */
235
288
  function postProcessBloomShader(threshold=.6, strength=1, size=6)
236
289
  {
237
- ASSERT(isNumber(threshold) && isNumber(strength) && isNumber(size), 'bloom settings must be numbers');
238
- ASSERT(size > 0, 'bloom size must be above zero');
239
- ASSERT(size <= 32, 'a bloom this wide takes a sample every few pixels of every ring, which is hundreds of samples a pixel', size);
290
+ return postProcessEffects(postProcessGlow(threshold, strength, size));
291
+ }
292
+
293
+ /**
294
+ * Set up post processing with a bloom effect, so bright colors and lights glow
295
+ * @param {number} [threshold] - Brightness where the glow starts, 0 is everything and 1 is only pure white
296
+ * @param {number} [strength] - How much glow to add
297
+ * @param {number} [size] - How far the glow spreads in pixels
298
+ * @param {boolean} [includeMainCanvas] - Glow the 2D canvas too, off by default so HUD text stays crisp
299
+ * (a HUD drawn with WebGL in gameRenderPost glows either way, draw it with useWebGL=false)
300
+ * @return {PostProcessPlugin}
301
+ * @memberof PostProcess
302
+ * @example
303
+ * postProcessBloom(); // in gameInit, after any Render3DPlugin
304
+ */
305
+ function postProcessBloom(threshold=.6, strength=1, size=6, includeMainCanvas=false)
306
+ { return new PostProcessPlugin(postProcessBloomShader(threshold, strength, size), includeMainCanvas); }
307
+
308
+ ///////////////////////////////////////////////////////////////////////////////
309
+ // Effects: pieces of shader code, each with its settings written in as numbers, that postProcessEffects joins in
310
+ // order into one shader; a piece works on c, the pixel's color, and uv, where it is on the screen from 0 to 1, with p
311
+ // its pixel; one that bends uv or samples the frame, the curve, chromatic and glow, goes before the ones that shade
312
+
313
+ // a number as GLSL writes it, a float with a point
314
+ const postProcessNumber = (n)=> (ASSERT(isNumber(n), 'effect settings must be numbers', n), n.toFixed(4));
315
+
316
+ /**
317
+ * Join effects into one post process shader, in the order given, for PostProcessPlugin or setShaderCode
318
+ * - Each effect is a piece of shader code on c, the pixel's color, and uv, where it is on the screen from 0 to 1;
319
+ * your own code is a piece too, a line of GLSL or many, dropped in where you put it
320
+ * - Put the ones that bend the picture or sample it first: postProcessCurve, postProcessChromatic, postProcessGlow
321
+ * @param {...string} effects - The pieces, from the effect functions or your own code
322
+ * @return {string} - Shadertoy style mainImage code
323
+ * @example
324
+ * new PostProcessPlugin(postProcessEffects(
325
+ * postProcessScanlines(.5), postProcessVignette(), 'c.rgb *= vec3(1, .9, .8);'));
326
+ * @memberof PostProcess
327
+ */
328
+ function postProcessEffects(...effects)
329
+ {
330
+ ASSERT(effects.every((e)=> typeof e === 'string'), 'each effect is a piece of shader code, a string');
331
+ return 'void mainImage(out vec4 c, vec2 p)\n{\n' +
332
+ ' vec2 uv = p / iResolution.xy;\n' +
333
+ ' c = texture(iChannel0, uv);\n' +
334
+ effects.map((e)=> ' {\n' + e + '\n }\n').join('') + '}\n';
335
+ }
336
+
337
+ /**
338
+ * Bright parts glow, the bloom as an effect to join with others; postProcessBloom sets up bloom on its own
339
+ * @param {number} [threshold] - Brightness where the glow starts, 0 is everything and 1 is only pure white
340
+ * @param {number} [strength] - How much glow to add
341
+ * @param {number} [size] - How far the glow spreads in pixels, which also sets how many samples it takes
342
+ * @return {string}
343
+ * @memberof PostProcess
344
+ */
345
+ function postProcessGlow(threshold=.6, strength=1, size=6)
346
+ {
347
+ ASSERT(isNumber(threshold) && isNumber(strength) && isNumber(size), 'glow settings must be numbers');
348
+ ASSERT(size > 0, 'glow size must be above zero');
349
+ ASSERT(size <= 32, 'a glow this wide takes a sample every few pixels of every ring, which is hundreds of samples a pixel', size);
240
350
 
241
351
  // Taps on three rings over a disc of the given size, one every three pixels or so of each ring
242
352
  // so there is no gap wide enough to show. The count follows the ring all the way out: hold it
@@ -258,27 +368,124 @@ function postProcessBloomShader(threshold=.6, strength=1, size=6)
258
368
  glow += max(vec3(0), texture(iChannel0, uv + vec2(cos(a), sin(a)) * ${radius.toFixed(4)} / iResolution.xy).rgb - ${threshold.toFixed(4)});
259
369
  }`;
260
370
  }
261
- return `
262
- void mainImage(out vec4 color, vec2 pixel)
263
- {
264
- vec2 uv = pixel / iResolution.xy;
265
- color = texture(iChannel0, uv);
371
+ return ` // glow
266
372
  vec3 glow = vec3(0);${code}
267
- color.rgb += glow * ${(strength / taps).toFixed(6)};
268
- }`;
373
+ c.rgb += glow * ${(strength / taps).toFixed(6)};`;
269
374
  }
270
375
 
271
376
  /**
272
- * Set up post processing with a bloom effect, so bright colors and lights glow
273
- * @param {number} [threshold] - Brightness where the glow starts, 0 is everything and 1 is only pure white
274
- * @param {number} [strength] - How much glow to add
275
- * @param {number} [size] - How far the glow spreads in pixels
276
- * @param {boolean} [includeMainCanvas] - Glow the 2D canvas too, off by default so HUD text stays crisp
277
- * (a HUD drawn with WebGL in gameRenderPost glows either way, draw it with useWebGL=false)
278
- * @return {PostProcessPlugin}
377
+ * Scan lines across the screen, like an old TV
378
+ * @param {number} [strength] - How dark the lines are, and how bright between them
379
+ * @param {number} [spacing] - Pixels from one line to the next
380
+ * @return {string}
381
+ * @memberof PostProcess
382
+ */
383
+ function postProcessScanlines(strength=.5, spacing=6)
384
+ {
385
+ return ` // scanlines
386
+ c.rgb *= 1. - ${postProcessNumber(strength)} * cos(p.y * 6.2832 / ${postProcessNumber(spacing)});`;
387
+ }
388
+
389
+ /**
390
+ * Static noise over the picture, changing every frame
391
+ * @param {number} [strength] - How bright the static is
392
+ * @param {number} [size] - Size of a speck in pixels
393
+ * @return {string}
394
+ * @memberof PostProcess
395
+ */
396
+ function postProcessNoise(strength=.1, size=2)
397
+ {
398
+ return ` // noise
399
+ vec2 q = fract((floor(p / ${postProcessNumber(size)}) + mod(iTime * 500., 1e3)) * .3197);
400
+ c.rgb += ${postProcessNumber(strength)} * fract(1. + sin(51. * q.x + 73. * q.y) * 13753.3);`;
401
+ }
402
+
403
+ /**
404
+ * Darken toward the edges and corners
405
+ * @param {number} [strength] - How dark the corners get, 1 is black
406
+ * @param {number} [falloff] - How far in it reaches, low darkens most of the screen, high only the corners
407
+ * @return {string}
408
+ * @memberof PostProcess
409
+ */
410
+ function postProcessVignette(strength=1, falloff=3)
411
+ {
412
+ return ` // vignette
413
+ vec2 d = uv * 2. - 1.;
414
+ c.rgb *= 1. - ${postProcessNumber(strength)} * min(1., pow(dot(d, d) / 2., ${postProcessNumber(falloff)}));`;
415
+ }
416
+
417
+ /**
418
+ * Bend the picture like the bulged glass of an old TV, black past the corners; put it first
419
+ * @param {number} [strength] - How much it bends
420
+ * @return {string}
421
+ * @memberof PostProcess
422
+ */
423
+ function postProcessCurve(strength=.1)
424
+ {
425
+ return ` // curve
426
+ vec2 d = uv * 2. - 1.;
427
+ d *= 1. + ${postProcessNumber(strength)} * dot(d, d);
428
+ uv = d * .5 + .5;
429
+ c = all(lessThan(abs(d), vec2(1))) ? texture(iChannel0, uv) : vec4(0, 0, 0, 1);`;
430
+ }
431
+
432
+ /**
433
+ * Split red and blue apart toward the edges, like a cheap lens; put it before what shades the picture
434
+ * @param {number} [strength] - How far apart at the edge, as a part of the screen
435
+ * @return {string}
436
+ * @memberof PostProcess
437
+ */
438
+ function postProcessChromatic(strength=.005)
439
+ {
440
+ return ` // chromatic
441
+ vec2 d = (uv - .5) * ${postProcessNumber(strength)} * 2.;
442
+ c.r = texture(iChannel0, uv + d).r;
443
+ c.b = texture(iChannel0, uv - d).b;`;
444
+ }
445
+
446
+ /**
447
+ * Draw lines where the 3D depth jumps, around objects and along their creases; needs render3D.depthTexture on
448
+ * @param {Color} [color] - The lines' color, its alpha how strong they are
449
+ * @param {number} [thickness] - How wide the lines are in pixels
450
+ * @param {number} [threshold] - How big a jump makes a line, as a part of the distance, lower draws more
451
+ * @return {string}
279
452
  * @memberof PostProcess
453
+ */
454
+ function postProcessOutline(color=BLACK, thickness=1, threshold=.02)
455
+ {
456
+ ASSERT(isColor(color), 'outline color must be a Color');
457
+ const n = postProcessNumber;
458
+ return ` // outline
459
+ vec2 o = ${n(thickness)} / iResolution.xy;
460
+ float d = sceneDepth(uv);
461
+ float dx = abs(sceneDepth(uv + vec2(o.x, 0)) + sceneDepth(uv - vec2(o.x, 0)) - 2. * d);
462
+ float dy = abs(sceneDepth(uv + vec2(0, o.y)) + sceneDepth(uv - vec2(0, o.y)) - 2. * d);
463
+ vec4 line = vec4(${n(color.r)}, ${n(color.g)}, ${n(color.b)}, ${n(color.a)});
464
+ c.rgb = mix(c.rgb, line.rgb, line.a * step(${n(threshold)}, max(dx, dy) / d));`;
465
+ }
466
+
467
+ /**
468
+ * The look of an old TV, as one effect to use alone or join with others: static noise, scan lines, a soft glow and
469
+ * a vignette, and a bulged screen when curve is set; any setting at 0 leaves that part out
470
+ * @param {Object} [settings]
471
+ * @param {number} [settings.noise] - Static noise strength
472
+ * @param {number} [settings.scanlines] - Scan line strength
473
+ * @param {number} [settings.scanlineSpacing] - Pixels from one scan line to the next
474
+ * @param {number} [settings.glow] - Soft glow strength
475
+ * @param {number} [settings.vignette] - Vignette strength
476
+ * @param {number} [settings.curve] - How much the screen bulges, 0 by default for flat
477
+ * @return {string}
280
478
  * @example
281
- * postProcessBloom(); // in gameInit, after any Render3DPlugin
479
+ * new PostProcessPlugin(postProcessEffects(postProcessTV({scanlines: .4, curve: .1})));
480
+ * @memberof PostProcess
282
481
  */
283
- function postProcessBloom(threshold=.6, strength=1, size=6, includeMainCanvas=false)
284
- { return new PostProcessPlugin(postProcessBloomShader(threshold, strength, size), includeMainCanvas); }
482
+ function postProcessTV({noise=.1, scanlines=.5, scanlineSpacing=6, glow=.4, vignette=1, curve=0}={})
483
+ {
484
+ const parts = [];
485
+ curve && parts.push(postProcessCurve(curve));
486
+ glow && parts.push(postProcessGlow(0, glow, 2));
487
+ scanlines && parts.push(postProcessScanlines(scanlines, scanlineSpacing));
488
+ noise && parts.push(postProcessNoise(noise, 1));
489
+ vignette && parts.push(postProcessVignette(vignette, 6));
490
+ return parts.map((part)=> ' {\n' + part + '\n }').join('\n');
491
+ }