littlejsengine 1.24.2 → 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.
- package/CHANGELOG.md +55 -0
- package/EDITOR.md +2 -2
- package/FAQ.md +6 -0
- package/REFERENCE.md +86 -41
- package/dist/littlejs.d.ts +600 -323
- package/dist/littlejs.esm.js +2412 -1797
- package/dist/littlejs.esm.min.js +38 -24
- package/dist/littlejs.js +2410 -1797
- package/dist/littlejs.min.js +37 -23
- package/dist/littlejs.release.js +2393 -1826
- package/package.json +1 -1
- package/plugins/box2d.js +41 -28
- package/plugins/drawUtilities.js +374 -380
- package/plugins/gltf.js +331 -55
- package/plugins/lightSystem.js +2 -2
- package/plugins/math3d.js +1013 -1014
- package/plugins/newgrounds.js +1 -4
- package/plugins/particleEffects.js +3 -4
- package/plugins/pathFinder.js +52 -28
- package/plugins/pluginExport.js +2 -0
- package/plugins/postProcess.js +196 -65
- package/plugins/render3d.js +8 -6
- package/plugins/render3dEditor.js +3 -3
- package/plugins/render3dExtras.js +31 -10
- package/plugins/render3dLevel.js +15 -4
- package/plugins/textureSheet.js +15 -7
- package/plugins/tweenSystem.js +43 -38
- package/plugins/uiSystem.js +25 -20
- package/src/engine.js +39 -21
- package/src/engineAudio.js +13 -9
- package/src/engineBuild.mjs +49 -2
- package/src/engineDebug.js +3 -2
- package/src/engineDraw.js +20 -18
- package/src/engineEditor.js +75 -23
- package/src/engineInput.js +10 -10
- package/src/engineMath.js +3 -6
- package/src/engineObject.js +8 -9
- package/src/engineParticles.js +8 -7
- package/src/engineRelease.js +17 -10
- package/src/engineTileLayer.js +33 -14
- package/src/engineUtilities.js +15 -2
- package/src/engineWebGL.js +34 -12
package/plugins/postProcess.js
CHANGED
|
@@ -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
|
-
//
|
|
66
|
-
|
|
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,
|
|
134
|
-
//
|
|
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
|
-
|
|
199
|
-
camera.far == Infinity ? 0 : camera.far, camera
|
|
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
|
-
|
|
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;'+
|
|
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
|
-
|
|
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(
|
|
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
|
|
433
|
+
for (int _k = 0; _k < ${count}; ++_k)
|
|
369
434
|
{
|
|
370
|
-
float
|
|
371
|
-
|
|
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
|
|
376
|
-
c.rgb +=
|
|
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
|
|
403
|
-
c.rgb += ${postProcessNumber(strength)} * fract(1. + sin(51. *
|
|
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
|
|
417
|
-
c.rgb *= 1. - ${postProcessNumber(strength)} * min(1., pow(dot(
|
|
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
|
|
430
|
-
|
|
431
|
-
uv =
|
|
432
|
-
c = all(lessThan(abs(
|
|
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
|
|
445
|
-
c.r = texture(iChannel0, uv +
|
|
446
|
-
c.b = texture(iChannel0, uv -
|
|
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
|
|
463
|
-
float
|
|
464
|
-
float
|
|
465
|
-
float
|
|
466
|
-
vec4
|
|
467
|
-
|
|
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})));
|
package/plugins/render3d.js
CHANGED
|
@@ -706,7 +706,8 @@ class Render3DPlugin
|
|
|
706
706
|
this.streamCount = 0;
|
|
707
707
|
/** @type {TextureInfo|undefined} */
|
|
708
708
|
this.streamTileInfo = undefined;
|
|
709
|
-
|
|
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.-
|
|
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
|
-
|
|
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
|
|
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
|
|
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()
|
|
1492
|
+
const frame = editor3DPrefabStack.pop();
|
|
1493
1493
|
if (!frame) return false;
|
|
1494
1494
|
editor3DStrokeEnd();
|
|
1495
1495
|
editor3DDrag = editor3DHover = undefined;
|
|
@@ -631,9 +631,9 @@ class HeightMap extends EngineObject3D
|
|
|
631
631
|
levelSegment3D(from, to)
|
|
632
632
|
{
|
|
633
633
|
const m = this.pos3D, size = this.mapSize;
|
|
634
|
-
if (abs(to.x - m.x) > size.x / 2 || abs(to.z - m.z) > size.y / 2) return; // off the map
|
|
634
|
+
if (abs(to.x - m.x) > size.x / 2 || abs(to.z - m.z) > size.y / 2) return undefined; // off the map
|
|
635
635
|
const a = from.y - this.getHeight(from.x, from.z), b = to.y - this.getHeight(to.x, to.z);
|
|
636
|
-
if (a < 0 || b >= 0) return; // above all the way, or under from the start
|
|
636
|
+
if (a < 0 || b >= 0) return undefined; // above all the way, or under from the start
|
|
637
637
|
const distance = a / (a - b);
|
|
638
638
|
return {distance, normal: this.getNormal(from.x + (to.x - from.x) * distance, from.z + (to.z - from.z) * distance)};
|
|
639
639
|
}
|
|
@@ -1671,9 +1671,11 @@ class LensFlare3D extends EngineObject3D
|
|
|
1671
1671
|
return this.made;
|
|
1672
1672
|
}
|
|
1673
1673
|
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1674
|
+
/** What the flare is of, seen from the camera: the way to it, how far it is, Infinity for the sun and for a
|
|
1675
|
+
* directional light, which shines from its place toward the origin, and a point to find it on the screen by;
|
|
1676
|
+
* undefined with no direction, or a light that is gone or at the camera
|
|
1677
|
+
* @return {{direction: Vector3, distance: number, pos: Vector3}|undefined}
|
|
1678
|
+
* @ignore */
|
|
1677
1679
|
flareSource()
|
|
1678
1680
|
{
|
|
1679
1681
|
const camera = render3D.camera.pos, light = this.light;
|
|
@@ -1689,9 +1691,11 @@ class LensFlare3D extends EngineObject3D
|
|
|
1689
1691
|
return distance ? {direction: offset.scale(1 / distance), distance, pos} : undefined;
|
|
1690
1692
|
}
|
|
1691
1693
|
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1694
|
+
/** How the flare would show with nothing in the way: where its source is on the screen, how strong it is there,
|
|
1695
|
+
* fading as it leaves the screen, its tint and the height its sizes are parts of; undefined when it would not
|
|
1696
|
+
* show at all, behind the camera, off the screen, or of a light that is off or seen from outside its cone
|
|
1697
|
+
* @return {{sun: Vector2, center: Vector2, strength: number, tint: Color, height: number}|undefined}
|
|
1698
|
+
* @ignore */
|
|
1695
1699
|
flareLook()
|
|
1696
1700
|
{
|
|
1697
1701
|
const source = this.flareSource(), center = mainCanvasSize.scale(.5);
|
|
@@ -1917,7 +1921,11 @@ async function loadOBJ(url, smooth=render3D?.smoothShading)
|
|
|
1917
1921
|
const response = await fetch(url);
|
|
1918
1922
|
if (!response.ok)
|
|
1919
1923
|
throw new Error('loadOBJ failed: ' + url);
|
|
1920
|
-
|
|
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;
|
|
1921
1929
|
}
|
|
1922
1930
|
|
|
1923
1931
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -2088,8 +2096,22 @@ function render3DCSGNear(polygons, polygonsOther)
|
|
|
2088
2096
|
const near = [], far = [];
|
|
2089
2097
|
const faces = [[vec3(1, 0, 0), hi.x], [vec3(-1, 0, 0), -lo.x], [vec3(0, 1, 0), hi.y],
|
|
2090
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;
|
|
2091
2100
|
for (const polygon of polygons)
|
|
2092
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
|
+
}
|
|
2093
2115
|
let inside = [polygon];
|
|
2094
2116
|
for (const [normal, w] of faces)
|
|
2095
2117
|
{
|
|
@@ -2245,7 +2267,6 @@ function render3DCSGMesh(polygons)
|
|
|
2245
2267
|
};
|
|
2246
2268
|
|
|
2247
2269
|
// each loop with the points along its edges put in, as a fan
|
|
2248
|
-
const flat = (a, b, c)=> b.subtract(a).cross(c.subtract(a)).lengthSquared() < 1e-18;
|
|
2249
2270
|
for (const loop of loops)
|
|
2250
2271
|
{
|
|
2251
2272
|
if (loop.length < 3) continue;
|