littlejsengine 1.24.3 → 1.25.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.
@@ -49,6 +49,18 @@ class PostProcessPlugin
49
49
  /** @property {string} - The shadertoy style mainImage code it shades with, see setShaderCode */
50
50
  this.shaderCode = shaderCode || postProcessEffects(); // no code passes the frame through
51
51
 
52
+ /** @property {Object<string, number|Array<number>>} - The game's own values for the shader, a uniform each
53
+ * by its name, a number a float and a list of 2 to 4 numbers a vector, set every frame as they are; an
54
+ * effect setting can be one of these names, so it changes every frame without making the shader again, all
55
+ * but glow's size, which sets how many samples it takes; adding or removing a name makes the shader again,
56
+ * and the shader is first made at the first render, so values set right after the plugin are in it; a name
57
+ * is a GLSL name not starting with an underscore, which the effects keep for their own, nor i and a capital
58
+ * or gl_, and not c, uv or p, the names mainImage works on
59
+ * @type {Object<string, number|Array<number>>} */
60
+ this.values = {};
61
+ // the names of the values the shader was made with
62
+ this.valueNames = '';
63
+
52
64
  /** @property {WebGLProgram|undefined} - Shader for post processing
53
65
  * @type {WebGLProgram|undefined} */
54
66
  this.shader = undefined;
@@ -62,18 +74,14 @@ class PostProcessPlugin
62
74
  * @type {WebGLVertexArrayObject|undefined} */
63
75
  this.vao = undefined;
64
76
 
65
- // setup the post processing plugin
66
- initPostProcess();
77
+ // the shader is made at the first render, so values the game sets after this, which its code may name, are
78
+ // declared in it
79
+ !headlessMode && !glEnable && console.warn('PostProcessPlugin: WebGL not enabled!');
67
80
  engineAddPlugin(undefined, postProcessRender, postProcessContextLost, postProcessContextRestored);
68
81
 
69
82
  function initPostProcess()
70
83
  {
71
- if (headlessMode) return;
72
- if (!glEnable)
73
- {
74
- console.warn('PostProcessPlugin: WebGL not enabled!');
75
- return;
76
- }
84
+ if (headlessMode || !glEnable) return;
77
85
 
78
86
  // create resources, the feedback starting black, as if the frame before the first were empty
79
87
  if (feedbackTexture)
@@ -93,8 +101,9 @@ class PostProcessPlugin
93
101
  'gl_Position=vec4(p+p-1.,1,1);'+ // set position
94
102
  '}' // end of shader
95
103
  ,
96
- postProcessFragmentSource(postProcess.shaderCode)
104
+ postProcessFragmentSource(postProcess.shaderCode, postProcess.values)
97
105
  );
106
+ postProcess.valueNames = postProcessValueKey(postProcess.values);
98
107
 
99
108
  // setup VAO for post processing
100
109
  postProcess.vao = glContext.createVertexArray();
@@ -130,13 +139,17 @@ class PostProcessPlugin
130
139
  // clear out the buffer, before anything here binds its own
131
140
  glFlush();
132
141
 
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
142
+ // made now if WebGL was off when the plugin was made, when a lost context came back, when setShaderCode
143
+ // gave it new code, or when the game added or took away a value, which the shader declares
144
+ if (postProcess.shader && postProcessValueKey(postProcess.values) !== postProcess.valueNames)
145
+ postProcess.setShaderCode(postProcess.shaderCode);
135
146
  if (!postProcess.shader)
136
147
  {
137
148
  if (glContext.isContextLost()) return;
138
149
  initPostProcess();
139
150
  }
151
+ if (glFailedPrograms.has(postProcess.shader))
152
+ return; // a shader that did not build in a release build, the frame shows as it is
140
153
 
141
154
  // ensure we render to the default framebuffer (in case any earlier
142
155
  // caller this frame left a render target bound)
@@ -194,9 +207,10 @@ class PostProcessPlugin
194
207
  glContext.activeTexture(glContext.TEXTURE2);
195
208
  glContext.bindTexture(glContext.TEXTURE_2D, depth?.cameraDepthTexture || null);
196
209
  glContext.activeTexture(glContext.TEXTURE0);
210
+ // all 0 without one, which an effect reads as no depth
197
211
  const camera = depth?.camera;
198
- camera && glContext.uniform3f(glUniformLocation(postProcess.shader, 'iDepthRange'), camera.near,
199
- camera.far == Infinity ? 0 : camera.far, camera.orthographic ? 1 : 0);
212
+ glContext.uniform3f(glUniformLocation(postProcess.shader, 'iDepthRange'), camera?.near || 0,
213
+ !camera || camera.far == Infinity ? 0 : camera.far, camera?.orthographic ? 1 : 0);
200
214
 
201
215
  // set uniforms and draw
202
216
  const uniformLocation = (name)=>glUniformLocation(postProcess.shader, name);
@@ -205,6 +219,14 @@ class PostProcessPlugin
205
219
  glContext.uniform1i(uniformLocation('iChannel2'), 2);
206
220
  glContext.uniform1f(uniformLocation('iTime'), time);
207
221
  glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
222
+ for (const name in postProcess.values)
223
+ {
224
+ const value = postProcess.values[name], location = uniformLocation(name);
225
+ if (isArray(value))
226
+ [, , glContext.uniform2fv, glContext.uniform3fv, glContext.uniform4fv][value.length]?.call(glContext, location, value);
227
+ else
228
+ glContext.uniform1f(location, value);
229
+ }
208
230
  glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, 4);
209
231
 
210
232
  if (feedbackTexture)
@@ -254,24 +276,40 @@ class PostProcessPlugin
254
276
 
255
277
  ///////////////////////////////////////////////////////////////////////////////
256
278
 
279
+ // what the shader declares of the game's values, each name and its length, so a value that changes either makes the
280
+ // shader again
281
+ const postProcessValueKey = (values)=> Object.entries(values).map(([name, v])=> name + ':' + (isArray(v) ? v.length : 1)).join();
282
+
257
283
  // the fragment shader of the post process pass around its mainImage snippet: the frame on iChannel0, the last output
258
284
  // on iChannel1 with a feedback texture, and the 3D depth on iChannel2 when render3D.depthTexture is on, read with
259
- // sceneDepth(uv), the distance from the camera along its view in world units, uv 0 to 1 across the screen
260
- function postProcessFragmentSource(shaderCode)
285
+ // sceneDepth(uv), the distance from the camera along its view in world units, uv 0 to 1 across the screen; and a
286
+ // uniform for each of the game's values, a float or a vector of the list's length
287
+ function postProcessFragmentSource(shaderCode, values={})
261
288
  {
289
+ let declared = '';
290
+ for (const name in values)
291
+ {
292
+ const value = values[name], length = isArray(value) ? value.length : 1;
293
+ ASSERT(postProcessNameCheck(name), 'a postProcess value needs a name GLSL takes that the shader does not use', name);
294
+ ASSERT(!isArray(value) || length >= 2 && length <= 4, 'a postProcess value is a number or 2 to 4 of them', name);
295
+ declared += `uniform ${length > 1 ? 'vec' + length : 'float'} ${name};`;
296
+ }
262
297
  return '#version 300 es\n' + // specify GLSL ES version
263
298
  'precision highp float;'+ // use highp for accuracy
264
299
  'uniform sampler2D iChannel0;'+ // input texture
265
300
  'uniform sampler2D iChannel1;'+ // the previous frame's output, when feedbackTexture is set
266
- 'uniform sampler2D iChannel2;'+ // the 3D depth, when render3D.depthTexture is on
301
+ 'uniform highp sampler2D iChannel2;'+ // the 3D depth, when render3D.depthTexture is on, highp or it is 11 bits
267
302
  'uniform vec3 iResolution;'+ // size of output texture
268
303
  'uniform float iTime;'+ // time
269
304
  'uniform vec3 iDepthRange;'+ // the camera's near, its far or 0 for none, and 1 when orthographic
305
+ declared + // the game's own values
270
306
  'out vec4 c;'+ // out color
271
307
  // the depth texture's value back to a distance, as the camera's projection put it there
272
308
  'float sceneDepth(vec2 uv){'+
273
309
  'float d=texture(iChannel2,uv).r*2.-1.,n=iDepthRange.x,f=iDepthRange.y;'+
274
- 'return iDepthRange.z>0.?(d*(f-n)+f+n)/2.:f>0.?2.*n*f/(f+n-d*(f-n)):2.*n/(1.-d);}'+
310
+ 'return iDepthRange.z>0.?(d*(f-n)+f+n)/2.:f>0.?2.*n*f/(f+n-d*(f-n)):2.*n/max(1.-d,1e-7);}'+
311
+ // whether there is depth to read: a range of all 0 is none, an orthographic near plane may be behind
312
+ '\n#define LJS_HAS_DEPTH (iDepthRange != vec3(0))\n'+ // a define needs a line of its own
275
313
  '\n' + shaderCode + '\n'+ // insert custom shader code
276
314
  'void main(){'+ // shader entry point
277
315
  'mainImage(c,gl_FragCoord.xy);'+ // call post process function
@@ -282,9 +320,10 @@ function postProcessFragmentSource(shaderCode)
282
320
  /**
283
321
  * Shader code for a bloom effect, the bright parts of the image blurred back over it
284
322
  * - Pass it to PostProcessPlugin, or edit the string to build an effect on top of it
285
- * @param {number} [threshold] - Brightness where the glow starts, 0 is everything and 1 is only pure white
286
- * @param {number} [strength] - How much glow to add
287
- * @param {number} [size] - How far the glow spreads in pixels, which also sets how many samples it takes
323
+ * @param {number|string} [threshold] - Brightness where the glow starts, 0 is everything and 1 is only pure white
324
+ * @param {number|string} [strength] - How much glow to add
325
+ * @param {number} [size] - How far the glow spreads in pixels, which also sets how many samples it takes, so a
326
+ * number and not a value's name
288
327
  * @return {string}
289
328
  * @memberof PostProcess
290
329
  */
@@ -295,8 +334,8 @@ function postProcessBloomShader(threshold=.6, strength=1, size=6)
295
334
 
296
335
  /**
297
336
  * Set up post processing with a bloom effect, so bright colors and lights glow
298
- * @param {number} [threshold] - Brightness where the glow starts, 0 is everything and 1 is only pure white
299
- * @param {number} [strength] - How much glow to add
337
+ * @param {number|string} [threshold] - Brightness where the glow starts, 0 is everything and 1 is only pure white
338
+ * @param {number|string} [strength] - How much glow to add
300
339
  * @param {number} [size] - How far the glow spreads in pixels
301
340
  * @param {boolean} [includeMainCanvas] - Glow the 2D canvas too, off by default so HUD text stays crisp
302
341
  * (a HUD drawn with WebGL in gameRenderPost glows either way, draw it with useWebGL=false)
@@ -314,7 +353,32 @@ function postProcessBloom(threshold=.6, strength=1, size=6, includeMainCanvas=fa
314
353
  // its pixel; one that bends uv or samples the frame, the curve, chromatic and glow, goes before the ones that shade
315
354
 
316
355
  // a number as GLSL writes it, a float with a point
317
- const postProcessNumber = (n)=> (ASSERT(isNumber(n), 'effect settings must be numbers', n), n.toFixed(4));
356
+ // an effect setting: a number written into the code, or the name of a postProcess value, read from it each frame
357
+ const postProcessNumber = (n)=> typeof n === 'string' ?
358
+ (ASSERT(postProcessNameCheck(n), 'an effect setting is a number or the name of a postProcess value', n), n) :
359
+ (ASSERT(isNumber(n) && isFinite(n), 'an effect setting is a finite number or the name of a postProcess value', n),
360
+ n && abs(n) < .01 ? String(n) : n.toFixed(4)); // a small one keeps its digits, as 1.5e-7 or 0.00015
361
+
362
+ // a GLSL name for a value or a setting, which no part of the shader uses already: not the engine's own, i and a
363
+ // capital, nor those of mainImage or an effect's own locals, which start with an underscore, nor a GLSL word
364
+ const postProcessGLSLWords = new Set(('c uv p sceneDepth mainImage main ' +
365
+ // keywords and reserved words
366
+ 'attribute const uniform varying layout centroid flat smooth noperspective patch sample break continue do for ' +
367
+ 'while switch case default if else subroutine in out inout float double int void bool true false invariant ' +
368
+ 'precise discard return lowp mediump highp precision struct common partition active asm class union enum ' +
369
+ 'typedef template this resource goto inline noinline public static extern external interface long short half ' +
370
+ 'fixed unsigned superp input output filter sizeof cast namespace using coherent volatile restrict readonly ' +
371
+ 'writeonly atomic_uint uint mat2 mat3 mat4 vec2 vec3 vec4 ivec2 ivec3 ivec4 bvec2 bvec3 bvec4 uvec2 uvec3 uvec4 ' +
372
+ // built in functions
373
+ 'radians degrees sin cos tan asin acos atan sinh cosh tanh asinh acosh atanh pow exp log exp2 log2 sqrt ' +
374
+ 'inversesqrt abs sign floor trunc round roundEven ceil fract mod modf min max clamp mix step smoothstep isnan ' +
375
+ 'isinf length distance dot cross normalize faceforward reflect refract matrixCompMult outerProduct transpose ' +
376
+ 'determinant inverse lessThan lessThanEqual greaterThan greaterThanEqual equal notEqual any all not textureSize ' +
377
+ 'texture textureProj textureLod textureOffset texelFetch texelFetchOffset textureProjOffset textureLodOffset ' +
378
+ 'textureProjLod textureProjLodOffset textureGrad textureGradOffset textureProjGrad textureProjGradOffset dFdx ' +
379
+ 'dFdy fwidth').split(' '));
380
+ const postProcessNameCheck = (name)=> /^[A-Za-z][A-Za-z0-9_]*$/.test(name) && !name.includes('__') &&
381
+ !/^(i[A-Z]|gl_|webgl_|GL_|mat[2-4]|[dfh]vec[2-4]|[iu]?(sampler|image)([123]D|Cube|Buffer|External))/.test(name) && !postProcessGLSLWords.has(name);
318
382
 
319
383
  /**
320
384
  * Join effects into one post process shader, in the order given, for PostProcessPlugin or setShaderCode
@@ -339,15 +403,16 @@ function postProcessEffects(...effects)
339
403
 
340
404
  /**
341
405
  * Bright parts glow, the bloom as an effect to join with others; postProcessBloom sets up bloom on its own
342
- * @param {number} [threshold] - Brightness where the glow starts, 0 is everything and 1 is only pure white
343
- * @param {number} [strength] - How much glow to add
344
- * @param {number} [size] - How far the glow spreads in pixels, which also sets how many samples it takes
406
+ * @param {number|string} [threshold] - Brightness where the glow starts, 0 is everything and 1 is only pure white
407
+ * @param {number|string} [strength] - How much glow to add
408
+ * @param {number} [size] - How far the glow spreads in pixels, which also sets how many samples it takes, so a
409
+ * number and not a value's name
345
410
  * @return {string}
346
411
  * @memberof PostProcess
347
412
  */
348
413
  function postProcessGlow(threshold=.6, strength=1, size=6)
349
414
  {
350
- ASSERT(isNumber(threshold) && isNumber(strength) && isNumber(size), 'glow settings must be numbers');
415
+ ASSERT(isNumber(size), 'glow size is a number, it sets how many samples the glow takes');
351
416
  ASSERT(size > 0, 'glow size must be above zero');
352
417
  ASSERT(size <= 32, 'a glow this wide takes a sample every few pixels of every ring, which is hundreds of samples a pixel', size);
353
418
 
@@ -365,21 +430,22 @@ function postProcessGlow(threshold=.6, strength=1, size=6)
365
430
  const count = max(5 + 2 * j, round(2 * radius)) | 1;
366
431
  taps += count;
367
432
  code += `
368
- for (int k = 0; k < ${count}; ++k)
433
+ for (int _k = 0; _k < ${count}; ++_k)
369
434
  {
370
- float a = float(k) * ${(2 * PI / count).toFixed(7)}${j ? ' + ' + (j * 2.3999632).toFixed(7) : ''};
371
- glow += max(vec3(0), texture(iChannel0, uv + vec2(cos(a), sin(a)) * ${radius.toFixed(4)} / iResolution.xy).rgb - ${threshold.toFixed(4)});
435
+ float _a = float(_k) * ${(2 * PI / count).toFixed(7)}${j ? ' + ' + (j * 2.3999632).toFixed(7) : ''};
436
+ _glow += max(vec3(0), texture(iChannel0, uv + vec2(cos(_a), sin(_a)) * ${radius.toFixed(4)} / iResolution.xy).rgb - ${postProcessNumber(threshold)});
372
437
  }`;
373
438
  }
374
439
  return ` // glow
375
- vec3 glow = vec3(0);${code}
376
- c.rgb += glow * ${(strength / taps).toFixed(6)};`;
440
+ vec3 _glow = vec3(0);${code}
441
+ c.rgb += _glow * ${typeof strength === 'string' ? postProcessNumber(strength) + ' / ' + taps + '.' :
442
+ (strength / taps).toFixed(6)};`; // a number as before, a value's name divided in the shader
377
443
  }
378
444
 
379
445
  /**
380
446
  * Scan lines across the screen, like an old TV
381
- * @param {number} [strength] - How dark the lines are, and how bright between them
382
- * @param {number} [spacing] - Pixels from one line to the next
447
+ * @param {number|string} [strength] - How dark the lines are, and how bright between them
448
+ * @param {number|string} [spacing] - Pixels from one line to the next
383
449
  * @return {string}
384
450
  * @memberof PostProcess
385
451
  */
@@ -391,66 +457,130 @@ function postProcessScanlines(strength=.5, spacing=6)
391
457
 
392
458
  /**
393
459
  * Static noise over the picture, changing every frame
394
- * @param {number} [strength] - How bright the static is
395
- * @param {number} [size] - Size of a speck in pixels
460
+ * @param {number|string} [strength] - How bright the static is
461
+ * @param {number|string} [size] - Size of a speck in pixels
396
462
  * @return {string}
397
463
  * @memberof PostProcess
398
464
  */
399
465
  function postProcessNoise(strength=.1, size=2)
400
466
  {
401
467
  return ` // noise
402
- vec2 q = fract((floor(p / ${postProcessNumber(size)}) + mod(iTime * 500., 1e3)) * .3197);
403
- c.rgb += ${postProcessNumber(strength)} * fract(1. + sin(51. * q.x + 73. * q.y) * 13753.3);`;
468
+ vec2 _q = fract((floor(p / ${postProcessNumber(size)}) + mod(iTime * 500., 1e3)) * .3197);
469
+ c.rgb += ${postProcessNumber(strength)} * fract(1. + sin(51. * _q.x + 73. * _q.y) * 13753.3);`;
404
470
  }
405
471
 
406
472
  /**
407
473
  * Darken toward the edges and corners
408
- * @param {number} [strength] - How dark the corners get, 1 is black
409
- * @param {number} [falloff] - How far in it reaches, low darkens most of the screen, high only the corners
474
+ * @param {number|string} [strength] - How dark the corners get, 1 is black
475
+ * @param {number|string} [falloff] - How far in it reaches, low darkens most of the screen, high only the corners
410
476
  * @return {string}
411
477
  * @memberof PostProcess
412
478
  */
413
479
  function postProcessVignette(strength=1, falloff=3)
414
480
  {
415
481
  return ` // vignette
416
- vec2 d = uv * 2. - 1.;
417
- c.rgb *= 1. - ${postProcessNumber(strength)} * min(1., pow(dot(d, d) / 2., ${postProcessNumber(falloff)}));`;
482
+ vec2 _d = uv * 2. - 1.;
483
+ c.rgb *= 1. - ${postProcessNumber(strength)} * min(1., pow(dot(_d, _d) / 2., ${postProcessNumber(falloff)}));`;
418
484
  }
419
485
 
420
486
  /**
421
487
  * Bend the picture like the bulged glass of an old TV, black past the corners; put it first
422
- * @param {number} [strength] - How much it bends
488
+ * @param {number|string} [strength] - How much it bends
423
489
  * @return {string}
424
490
  * @memberof PostProcess
425
491
  */
426
492
  function postProcessCurve(strength=.1)
427
493
  {
428
494
  return ` // curve
429
- vec2 d = uv * 2. - 1.;
430
- d *= 1. + ${postProcessNumber(strength)} * dot(d, d);
431
- uv = d * .5 + .5;
432
- c = all(lessThan(abs(d), vec2(1))) ? texture(iChannel0, uv) : vec4(0, 0, 0, 1);`;
495
+ vec2 _d = uv * 2. - 1.;
496
+ _d *= 1. + ${postProcessNumber(strength)} * dot(_d, _d);
497
+ uv = _d * .5 + .5;
498
+ c = all(lessThan(abs(_d), vec2(1))) ? texture(iChannel0, uv) : vec4(0, 0, 0, 1);`;
433
499
  }
434
500
 
435
501
  /**
436
502
  * Split red and blue apart toward the edges, like a cheap lens; put it before what shades the picture
437
- * @param {number} [strength] - How far apart at the edge, as a part of the screen
503
+ * @param {number|string} [strength] - How far apart at the edge, as a part of the screen
438
504
  * @return {string}
439
505
  * @memberof PostProcess
440
506
  */
441
507
  function postProcessChromatic(strength=.005)
442
508
  {
443
509
  return ` // chromatic
444
- vec2 d = (uv - .5) * ${postProcessNumber(strength)} * 2.;
445
- c.r = texture(iChannel0, uv + d).r;
446
- c.b = texture(iChannel0, uv - d).b;`;
510
+ vec2 _d = (uv - .5) * ${postProcessNumber(strength)} * 2.;
511
+ c.r = texture(iChannel0, uv + _d).r;
512
+ c.b = texture(iChannel0, uv - _d).b;`;
513
+ }
514
+
515
+ // a blur's widest is 32 pixels, past which its 24 taps sit far enough apart to show; a value's is the game's to keep
516
+ const postProcessBlurCheck = (blur)=>
517
+ ASSERT(typeof blur === 'string' || blur <= 32, 'a blur of more than 32 pixels shows its taps as copies', blur);
518
+
519
+ // a blur over a disc of radius _r pixels around uv, of 24 taps spread evenly by the golden angle; weight, when given,
520
+ // is GLSL for how much a tap at _puv, _s of the radius out, counts, from 0 to 1
521
+ function postProcessDiscBlur(weight)
522
+ {
523
+ return `
524
+ vec3 _sum = vec3(0);
525
+ float _total = 0.;
526
+ for (int _k = 0; _k < 24; ++_k)
527
+ {
528
+ float _s = sqrt((float(_k) + .5) / 24.), _a = float(_k) * 2.39996;
529
+ vec2 _puv = uv + vec2(cos(_a), sin(_a)) * _s * _r / iResolution.xy;
530
+ float _w = ${weight || '1.'};
531
+ _sum += texture(iChannel0, _puv).rgb * _w;
532
+ _total += _w;
533
+ }
534
+ c.rgb = _total > 0. ? _sum / _total : c.rgb;`;
535
+ }
536
+
537
+ /**
538
+ * Keep a band across the screen sharp and blur the picture above and below it, as a tilt shift lens does, which
539
+ * makes a scene look like a small model; it reads only the screen, so it works in 2D and 3D; put it first
540
+ * @param {number|string} [focus] - Height of the middle of the sharp band, 0 the bottom of the screen and 1 the top
541
+ * @param {number|string} [size] - Height of the sharp band, as a part of the screen
542
+ * @param {number|string} [blur] - Widest blur in pixels, reached half the screen past the band, at most 32
543
+ * @return {string}
544
+ * @memberof PostProcess
545
+ * @example
546
+ * new PostProcessPlugin(postProcessEffects(postProcessTiltShift(.4, .2, 10), postProcessVignette(.5)));
547
+ */
548
+ function postProcessTiltShift(focus=.5, size=.25, blur=8)
549
+ {
550
+ postProcessBlurCheck(blur);
551
+ const n = postProcessNumber;
552
+ return ` // tilt shift
553
+ float _r = ${n(blur)} * smoothstep(0., .5, abs(uv.y - ${n(focus)}) - ${n(size)} * .5);${postProcessDiscBlur()}`;
554
+ }
555
+
556
+ /**
557
+ * Keep what is a distance from the camera sharp and blur what is nearer or farther, as a camera lens does; needs
558
+ * render3D.depthTexture on, and blurs 3D only, 2D draws having no depth; put it first
559
+ * - What is in focus stays sharp at its edges: a blur in front of or behind it leaves out what is in focus
560
+ * @param {number|string} [focus] - Distance from the camera, along its view, that is sharpest, in world units
561
+ * @param {number|string} [range] - How deep the sharp part is; the blur grows over as far again past it
562
+ * @param {number|string} [blur] - Widest blur in pixels, at most 32
563
+ * @return {string}
564
+ * @memberof PostProcess
565
+ * @example
566
+ * render3D.depthTexture = true;
567
+ * new PostProcessPlugin(postProcessEffects(postProcessDepthOfField(10, 4, 8)));
568
+ */
569
+ function postProcessDepthOfField(focus=10, range=4, blur=8)
570
+ {
571
+ postProcessBlurCheck(blur);
572
+ const n = postProcessNumber;
573
+ const amount = (depth)=> `${n(blur)} * smoothstep(0., ${n(range)}, abs(${depth} - ${n(focus)}) - ${n(range)} * .5)`;
574
+ // a tap counts as far as its own blur reaches back to here, so a sharp thing in front is not smeared over
575
+ return ` // depth of field, none where there is no depth, as with render3D.depthTexture off
576
+ float _r = LJS_HAS_DEPTH ? ${amount('sceneDepth(uv)')} : 0.;${postProcessDiscBlur(`clamp(${amount('sceneDepth(_puv)')} - _s * _r + 1., 0., 1.)`)}`;
447
577
  }
448
578
 
449
579
  /**
450
580
  * Draw lines where the 3D depth jumps, around objects and along their creases; needs render3D.depthTexture on
451
581
  * @param {Color} [color] - The lines' color, its alpha how strong they are
452
- * @param {number} [thickness] - How wide the lines are in pixels
453
- * @param {number} [threshold] - How big a jump makes a line, as a part of the distance, lower draws more
582
+ * @param {number|string} [thickness] - How wide the lines are in pixels
583
+ * @param {number|string} [threshold] - How big a jump makes a line, as a part of the distance, lower draws more
454
584
  * @return {string}
455
585
  * @memberof PostProcess
456
586
  */
@@ -459,24 +589,25 @@ function postProcessOutline(color=BLACK, thickness=1, threshold=.02)
459
589
  ASSERT(isColor(color), 'outline color must be a Color');
460
590
  const n = postProcessNumber;
461
591
  return ` // outline
462
- vec2 o = ${n(thickness)} / iResolution.xy;
463
- float d = sceneDepth(uv);
464
- float dx = abs(sceneDepth(uv + vec2(o.x, 0)) + sceneDepth(uv - vec2(o.x, 0)) - 2. * d);
465
- float dy = abs(sceneDepth(uv + vec2(0, o.y)) + sceneDepth(uv - vec2(0, o.y)) - 2. * d);
466
- vec4 line = vec4(${n(color.r)}, ${n(color.g)}, ${n(color.b)}, ${n(color.a)});
467
- c.rgb = mix(c.rgb, line.rgb, line.a * step(${n(threshold)}, max(dx, dy) / d));`;
592
+ vec2 _o = ${n(thickness)} / iResolution.xy;
593
+ float _d = sceneDepth(uv);
594
+ float _dx = abs(sceneDepth(uv + vec2(_o.x, 0)) + sceneDepth(uv - vec2(_o.x, 0)) - 2. * _d);
595
+ float _dy = abs(sceneDepth(uv + vec2(0, _o.y)) + sceneDepth(uv - vec2(0, _o.y)) - 2. * _d);
596
+ vec4 _line = vec4(${n(color.r)}, ${n(color.g)}, ${n(color.b)}, ${n(color.a)});
597
+ if (LJS_HAS_DEPTH)
598
+ c.rgb = mix(c.rgb, _line.rgb, _line.a * step(${n(threshold)}, max(_dx, _dy) / max(abs(_d), 1e-6)));`;
468
599
  }
469
600
 
470
601
  /**
471
602
  * The look of an old TV, as one effect to use alone or join with others: static noise, scan lines, a soft glow and
472
603
  * a vignette, and a bulged screen when curve is set; any setting at 0 leaves that part out
473
604
  * @param {Object} [settings]
474
- * @param {number} [settings.noise] - Static noise strength
475
- * @param {number} [settings.scanlines] - Scan line strength
476
- * @param {number} [settings.scanlineSpacing] - Pixels from one scan line to the next
477
- * @param {number} [settings.glow] - Soft glow strength
478
- * @param {number} [settings.vignette] - Vignette strength
479
- * @param {number} [settings.curve] - How much the screen bulges, 0 by default for flat
605
+ * @param {number|string} [settings.noise] - Static noise strength
606
+ * @param {number|string} [settings.scanlines] - Scan line strength
607
+ * @param {number|string} [settings.scanlineSpacing] - Pixels from one scan line to the next
608
+ * @param {number|string} [settings.glow] - Soft glow strength
609
+ * @param {number|string} [settings.vignette] - Vignette strength
610
+ * @param {number|string} [settings.curve] - How much the screen bulges, 0 by default for flat
480
611
  * @return {string}
481
612
  * @example
482
613
  * new PostProcessPlugin(postProcessEffects(postProcessTV({scanlines: .4, curve: .1})));
@@ -706,7 +706,8 @@ class Render3DPlugin
706
706
  this.streamCount = 0;
707
707
  /** @type {TextureInfo|undefined} */
708
708
  this.streamTileInfo = undefined;
709
- this.streamState = undefined; // captured state the pending batch was drawn under
709
+ /** @type {Object|undefined} */
710
+ this.streamState = undefined; // captured state the pending batch was drawn under, render3DCaptureBatchState's
710
711
  this.streamUnlit = false; // the pending batch is drawn unlit whatever that state says, billboards are
711
712
  /** @type {Mesh|undefined} */
712
713
  this.capture = undefined; // the mesh a bake is filling
@@ -1671,7 +1672,7 @@ const RENDER3D_VERTEX_SOURCE =
1671
1672
  'gl_Position=viewProj*w;' +
1672
1673
  'P=w.xyz;' +
1673
1674
  'vec3 c0=m0.xyz,c1=m1.xyz,c2=m2.xyz;' +
1674
- 'N=mat3(c0/dot(c0,c0),c1/dot(c1,c1),c2/dot(c2,c2))*n;' +
1675
+ 'N=mat3(c0/max(dot(c0,c0),1e-20),c1/max(dot(c1,c1),1e-20),c2/max(dot(c2,c2),1e-20))*n;' +
1675
1676
  'T=uvRect.xy+t*uvRect.zw;' +
1676
1677
  'L=t;' +
1677
1678
  'C=c*tint;' +
@@ -1793,7 +1794,7 @@ function render3DFragmentSource(fragmentCode)
1793
1794
  // shadow does not dim it
1794
1795
  'if(materialParams.z>0.){' +
1795
1796
  'vec3 w=normalize(P-cameraPos),q=reflect(w,n);' +
1796
- 'float f=materialParams.z+(1.-materialParams.z)*pow(1.-max(dot(n,-w),0.),5.);' +
1797
+ 'float f=materialParams.z+(1.-materialParams.z)*pow(clamp(1.-dot(n,-w),0.,1.),5.);' +
1797
1798
  'c.rgb=mix(c.rgb,q.y>0.?mix(skyHorizon.rgb,skyTop.rgb,q.y):mix(skyHorizon.rgb,skyBottom.rgb,-q.y),f);' +
1798
1799
  '}}else c.rgb*=e;' + // fully emissive: its own color, or brighter, with no lighting to work out
1799
1800
  // the emissive map adds its light on top, lit or not
@@ -1810,7 +1811,8 @@ function render3DFragmentSource(fragmentCode)
1810
1811
  function render3DShaderProgram(shader)
1811
1812
  {
1812
1813
  ASSERT(shader instanceof Shader, 'render3D.shader must be a Shader, not the snippet itself');
1813
- return shader.program3D ||= glCreateProgram(RENDER3D_VERTEX_SOURCE, render3DFragmentSource(shader.fragmentCode));
1814
+ const program = shader.program3D ||= glCreateProgram(RENDER3D_VERTEX_SOURCE, render3DFragmentSource(shader.fragmentCode));
1815
+ return glFailedPrograms.has(program) ? render3D.program : program; // one that did not build draws as with none
1814
1816
  }
1815
1817
 
1816
1818
  // make a program current for the pass and send it the pass uniforms: the matrices, the camera and the lights,
@@ -2035,7 +2037,7 @@ function render3DSetMaterialUniforms(state)
2035
2037
  {
2036
2038
  const r = render3D, loaded = (map)=> render3DTextureOf(map)?.glTexture ? map : undefined;
2037
2039
  const normalMap = state.normalScale ? loaded(state.normalMap) : undefined, emissiveMap = loaded(state.emissiveMap);
2038
- render3DUniform4f('materialParams', normalMap ? state.normalScale : 0, state.shininess, state.reflectivity, 0);
2040
+ render3DUniform4f('materialParams', normalMap ? state.normalScale : 0, max(state.shininess, 1e-3), state.reflectivity, 0);
2039
2041
  const ec = state.emissiveMapColor || WHITE;
2040
2042
  emissiveMap ? render3DUniform4f('emissiveTint', ec.r, ec.g, ec.b, 1) : render3DUniform4f('emissiveTint', 0, 0, 0, 0);
2041
2043
  render3DBindMap(2, normalMap, state);
@@ -2191,7 +2193,7 @@ function render3DSetDrawUniforms(matrix, tileInfo, tint, uvRect, state=render3D)
2191
2193
  // its alpha says whether the ground color is on
2192
2194
  gc ? render3DUniform4f('ambientGround', gc.r, gc.g, gc.b, 1) : render3DUniform4f('ambientGround', 0, 0, 0, 0);
2193
2195
 
2194
- render3DUniform4f('fogColor', fc.r, fc.g, fc.b, r.fogStart);
2196
+ render3DUniform4f('fogColor', fc.r, fc.g, fc.b, r.fogEnd ? min(r.fogStart, r.fogEnd - 1e-3) : r.fogStart);
2195
2197
  // how the fragment shader finishes: 1 drops see through texels and keeps the draw opaque,
2196
2198
  // 0 blends them away instead, and -1 is additive, which has to fade into fog differently
2197
2199
  const blendMode = state.blend ? (state.additive ? -1 : 0) : 1;
@@ -1261,7 +1261,7 @@ function editor3DPrefabHolds(type, name, depth=0)
1261
1261
  !!level3DPrefabs.get(type)?.objects.some((o)=> editor3DPrefabHolds(o.type, name, depth + 1));
1262
1262
  }
1263
1263
 
1264
- // make the selected objects a prefab of the level's own, about the bottom centre of their box, and put one
1264
+ // make the selected objects a prefab of the level's own, about the bottom center of their box, and put one
1265
1265
  // instance of it where they were, as one undo; a name the level's prefabs have replaces that prefab, and every
1266
1266
  // instance of it changes; true, or why not, which the panel shows
1267
1267
  function editor3DMakePrefab(name='')
@@ -1280,7 +1280,7 @@ function editor3DMakePrefab(name='')
1280
1280
  if (selected.some((o)=> editor3DPrefabHolds(o.type, name)))
1281
1281
  return refuse(name + ' can not hold itself');
1282
1282
 
1283
- // its origin is the bottom centre of the selection, so an instance stands on the ground
1283
+ // its origin is the bottom center of the selection, so an instance stands on the ground
1284
1284
  const low = vec3(Infinity), high = vec3(-Infinity);
1285
1285
  for (const object of selected)
1286
1286
  editor3DWorldBox(object, low, high);
@@ -1489,7 +1489,7 @@ function editor3DPrefabEnter(id)
1489
1489
  // no prefab is open
1490
1490
  function editor3DPrefabBack()
1491
1491
  {
1492
- const frame = editor3DPrefabStack.pop(), edited = editor3DLevel;
1492
+ const frame = editor3DPrefabStack.pop();
1493
1493
  if (!frame) return false;
1494
1494
  editor3DStrokeEnd();
1495
1495
  editor3DDrag = editor3DHover = undefined;
@@ -1921,7 +1921,11 @@ async function loadOBJ(url, smooth=render3D?.smoothShading)
1921
1921
  const response = await fetch(url);
1922
1922
  if (!response.ok)
1923
1923
  throw new Error('loadOBJ failed: ' + url);
1924
- return parseOBJ(await response.text(), smooth);
1924
+ const text = await response.text(), mesh = parseOBJ(text, smooth);
1925
+ // a mesh of nothing draws nothing, so it says why, a web page being what a dev server sends for a mistyped path
1926
+ mesh.points.length || console.warn('loadOBJ: ' + url + ' has no faces' +
1927
+ (loadIsWebPage(text) ? ', it is a web page, so the path may be wrong' : ''));
1928
+ return mesh;
1925
1929
  }
1926
1930
 
1927
1931
  ///////////////////////////////////////////////////////////////////////////////
@@ -2092,8 +2096,22 @@ function render3DCSGNear(polygons, polygonsOther)
2092
2096
  const near = [], far = [];
2093
2097
  const faces = [[vec3(1, 0, 0), hi.x], [vec3(-1, 0, 0), -lo.x], [vec3(0, 1, 0), hi.y],
2094
2098
  [vec3(0, -1, 0), -lo.y], [vec3(0, 0, 1), hi.z], [vec3(0, 0, -1), -lo.z]];
2099
+ const e = RENDER3D_CSG_EPSILON;
2095
2100
  for (const polygon of polygons)
2096
2101
  {
2102
+ // wholly beyond a face, as most of a mesh cut many times is, it is far as it is: splitting it at the faces
2103
+ // before would only cut it into parts that all end up far too
2104
+ let x0 = Infinity, y0 = Infinity, z0 = Infinity, x1 = -Infinity, y1 = -Infinity, z1 = -Infinity;
2105
+ for (const {pos} of polygon.vertices)
2106
+ {
2107
+ x0 = min(x0, pos.x); y0 = min(y0, pos.y); z0 = min(z0, pos.z);
2108
+ x1 = max(x1, pos.x); y1 = max(y1, pos.y); z1 = max(z1, pos.z);
2109
+ }
2110
+ if (x0 > hi.x + e || x1 < lo.x - e || y0 > hi.y + e || y1 < lo.y - e || z0 > hi.z + e || z1 < lo.z - e)
2111
+ {
2112
+ far.push(polygon);
2113
+ continue;
2114
+ }
2097
2115
  let inside = [polygon];
2098
2116
  for (const [normal, w] of faces)
2099
2117
  {
@@ -2249,7 +2267,6 @@ function render3DCSGMesh(polygons)
2249
2267
  };
2250
2268
 
2251
2269
  // each loop with the points along its edges put in, as a fan
2252
- const flat = (a, b, c)=> b.subtract(a).cross(c.subtract(a)).lengthSquared() < 1e-18;
2253
2270
  for (const loop of loops)
2254
2271
  {
2255
2272
  if (loop.length < 3) continue;
@@ -15,6 +15,16 @@
15
15
 
16
16
  const LEVEL3D_VERSION = 1; // the format a level's littlejs3D names
17
17
 
18
+ /** A 3D level as it is saved, what level3DLoad takes and the 3D level editor edits
19
+ * @typedef {Object} Level3D
20
+ * @property {number} [littlejs3D] - The format's version, 1
21
+ * @property {Array<Object>} objects - Each {id, type, pos, rotation, scale, properties}, all but type optional
22
+ * @property {Object} [scene] - The scene block: sky, ambient, sunDirection, sunColor, fog, fogColor, shadows, lensFlare
23
+ * @property {Object} [voxels] - A block map: {pos, size, blocks}
24
+ * @property {Object} [terrain] - A height map: {pos, size, height, color, heights, paint}
25
+ * @property {Object} [prefabs] - The level's own prefabs by name, each {objects, attached}
26
+ * @memberof Level3D */
27
+
18
28
  // the types a level's objects are made from, by name
19
29
  const level3DTypes = new Map;
20
30
 
@@ -33,7 +43,7 @@ const level3DBaseScale = new WeakMap;
33
43
  * - Adding a name again replaces it, Box, Sphere, Cylinder and Light too
34
44
  * @param {string} name - The type the objects have in the level
35
45
  * @param {Function} make - A class made at each object's position, or a function called with it
36
- * @param {Object} [defaults] - Properties of each one made, the level editor shows inputs for them
46
+ * @param {Object<string, any>} [defaults] - Properties of each one made, the level editor shows inputs for them
37
47
  * @param {TileInfo} [tileInfo] - An icon for the level editor
38
48
  * @memberof Level3D
39
49
  * @example
@@ -87,7 +97,7 @@ function level3DAddMesh(name, mesh, tileInfo, color=WHITE)
87
97
  * sunColor, fog, its start and end, fogColor, the horizon color when not given, shadows, and lensFlare, the
88
98
  * sun's lens flare; what the block leaves out stays as the game set it, and a level with no block changes
89
99
  * nothing
90
- * @param {Object} level - The level, the level editor edits this same object
100
+ * @param {Level3D} level - The level, the level editor edits this same object
91
101
  * @return {Array<any>} - What each object's type made, a function that made nothing is left out
92
102
  * @memberof Level3D */
93
103
  function level3DLoad(level)
@@ -420,7 +430,7 @@ function level3DMakeAt(object, pos, rotation, scale)
420
430
  * @param {Vector3} [pos3D]
421
431
  * @param {Vector3} [rotation3D] - In radians, as an object has it
422
432
  * @param {Vector3} [scale3D] - Times the scale the type makes it with
423
- * @param {Object} [properties] - Over the type's defaults
433
+ * @param {Object<string, any>} [properties] - Over the type's defaults
424
434
  * @return {any} - What the type made, a Prefab3D for a prefab, undefined when there is no such type
425
435
  * @memberof Level3D
426
436
  * @example
@@ -459,7 +469,8 @@ const level3DPrefabMaking = [];
459
469
  * by hand does
460
470
  * - Adding a name again replaces it
461
471
  * @param {string} name - The type its instances have in a level
462
- * @param {Object} prefab - {objects, attached}
472
+ * @param {{objects: Array<Object>, attached?: boolean}} prefab - Its objects, about its own origin, and whether
473
+ * they are attached
463
474
  * @memberof Level3D
464
475
  * @example
465
476
  * level3DAddPrefab('Tower', {objects: [{type: 'Box', pos: [0, 1, 0], scale: [2, 2, 2]},