littlejsengine 1.26.0 → 1.26.1

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 CHANGED
@@ -7,6 +7,18 @@ What changed in each release, newest first. Releases before 1.20.0 are on the
7
7
 
8
8
  Nothing yet.
9
9
 
10
+ ## 1.26.1 - 2026-10-08
11
+
12
+ ### New Changes
13
+ - A 3D Shader may define `void mainNormal(inout vec3 n)`, given the world normal after the normal map, to bend it per pixel for waves, ripples or a flag; the lighting, shadows, specular, Fresnel, environment reflection and fog all use it
14
+ - `postProcess.enabled`: false skips the post process pass, so an effect turns off and on without taking the plugin apart
15
+ - `averageFPS` is public and kept in release builds, where it read 0 unless debug or the watermark was on; it starts at the first frame's rate rather than climbing from 0
16
+ - `camera.align2DZ`: the z of the plane an align2D camera lines up with the 2D view, 0 as before
17
+ - The right click menu is left on links, as it is on text fields
18
+
19
+ ### Fixes
20
+ - A tap starts the audio with touch input off too
21
+
10
22
  ## 1.26.0 - 2026-10-07
11
23
 
12
24
  ### New Changes
package/REFERENCE.md CHANGED
@@ -25,8 +25,9 @@ engineName // Name of the engine: 'LittleJS'
25
25
  engineVersion // Version of the engine
26
26
  frameRate // Fixed frame rate for updates (60)
27
27
  frame // Current update frame
28
- time // Game time since start in seconds (stops when paused)
28
+ time // Game time since start in seconds (stops when paused, falls behind the clock below 20 FPS)
29
29
  timeReal // Real time since start in seconds (keeps running when paused; the debug speed keys scale it)
30
+ averageFPS // Frames drawn per second, smoothed
30
31
  timeDelta // Seconds the update covers: 1/60, or the display frame's time with engineVariableStep
31
32
  timeScale = 1 // Game speed, more or fewer fixed updates per second; with engineVariableStep it scales timeDelta
32
33
  paused // Is the game paused? (set with setPaused)
@@ -325,7 +326,9 @@ toggleFullscreen()
325
326
  // Shader Object - a custom fragment shader for objects and draws, 2D or 3D
326
327
  new Shader(fragmentCode) // fragmentCode defines void mainImage(out vec4 c, vec2 uv) in the post processing
327
328
  // style; it gives the surface color, then the object's color and additive color
328
- // apply in 2D, and the lighting, shadows and fog in 3D
329
+ // apply in 2D, and the lighting, shadows and fog in 3D; in 3D it may also define
330
+ // void mainNormal(inout vec3 n), the world normal after the normal map, to bend
331
+ // it per pixel (waves, ripples) for the lighting, specular and reflection
329
332
  obj.shader = shader // any EngineObject or EngineObject3D; draws that share a Shader share a batch
330
333
  shader.dispose() // frees its programs and takes it off the engine's list, which keeps every Shader
331
334
  // for a lost context; for one made each time a scene starts; drawn again it
@@ -1179,6 +1182,7 @@ postProcessBloom(threshold=.6, strength=1, size=6, includeMainCanvas=false) // s
1179
1182
  // and past a size of 32 it would take hundreds, so that is as wide as it goes
1180
1183
  postProcessBloomShader(threshold, strength, size) // its shader code, to pass to PostProcessPlugin or build on
1181
1184
  postProcess.setShaderCode(shaderCode) // shade with new code from the next frame, to switch effects while running
1185
+ postProcess.enabled = true // false skips the pass, the frame showing as drawn, to turn an effect off and back on
1182
1186
 
1183
1187
  // Built in effects: each is a piece of shader code with its settings written in, working on c, the pixel's color,
1184
1188
  // and uv, where it is on the screen from 0 to 1; postProcessEffects joins pieces in order into one shader, and your
@@ -1333,6 +1337,7 @@ render3D.camera.follow(target, offset, percent=1) // chase camera: ease toward t
1333
1337
  // is how far it moves each call, so call it every frame, from
1334
1338
  // gameUpdatePost once the target has moved
1335
1339
  render3D.camera.align2D = true // lock to the 2D camera so the z=0 plane matches world space, false by default
1340
+ render3D.camera.align2DZ = 0 // the z of the plane align2D lines up, the camera its distance in front
1336
1341
  render3D.camera.getForward() .getRight() .getUp() // the camera's axes as it is right now; render3D.cameraRight
1337
1342
  // .cameraUp .cameraForward are this frame's, read only
1338
1343
  render3D.viewMatrix .projectionMatrix .viewProjection .shadowMatrix // this frame's, rebuilt by updateMatrices()
@@ -1408,7 +1413,8 @@ render3D.shadowLight = flashlight // a spotlight, a Light3D with a coneAngle
1408
1413
  render3D.shadowRange = 40 // world size the map covers around shadowCenter, smaller is sharper
1409
1414
  // it is a square facing the light, so ~1.5x an area's width covers it
1410
1415
  render3D.shadowCenter = undefined // Vector3 center of the shadowed area, read each frame; undefined follows the camera
1411
- render3D.shadowBias = .003 // raise if lit surfaces get speckled with their own shadow, lower if shadows float away
1416
+ render3D.shadowBias = .003 // raise if lit surfaces get speckled with their own shadow, lower if shadows float away;
1417
+ // a share of the map's depth (2 * shadowRange for the sun), so the gap grows with the range
1412
1418
  // from their casters
1413
1419
  render3D.shadowSoftness = 1 // how far to blur the shadow edge, in shadow map pixels
1414
1420
 
@@ -606,6 +606,8 @@ declare module "littlejsengine" {
606
606
  * @memberof Engine */
607
607
  export let frame: number;
608
608
  /** Current engine time since start in seconds
609
+ * - Below 20 FPS it falls behind the clock, since a frame catches up at most 50 ms of updates, so a timer in
610
+ * real seconds, one that must keep up on a slow device, goes by timeReal
609
611
  * @type {number}
610
612
  * @memberof Engine */
611
613
  export let time: number;
@@ -613,6 +615,10 @@ declare module "littlejsengine" {
613
615
  * @type {number}
614
616
  * @memberof Engine */
615
617
  export let timeReal: number;
618
+ /** Frames drawn per second, smoothed over the last few seconds, in release builds too
619
+ * @type {number}
620
+ * @memberof Engine */
621
+ export let averageFPS: number;
616
622
  /** Is the game paused? Causes time and objects to not be updated
617
623
  * @type {boolean}
618
624
  * @default false
@@ -647,7 +653,9 @@ declare module "littlejsengine" {
647
653
  * @param {GameCallback} [gameRender] - Called before objects are rendered, use for drawing backgrounds/world elements
648
654
  * @param {GameCallback} [gameRenderPost] - Called after objects are rendered, use for drawing UI/overlays
649
655
  * @param {Array<string>|string} [imageSources=[]] - List of image file paths to preload (e.g., ['player.png', 'tiles.png']), or one path
650
- * @param {HTMLElement} [rootElement] - Root DOM element to attach canvas to, defaults to document.body
656
+ * @param {HTMLElement} [rootElement] - Root DOM element to attach canvas to, defaults to document.body; it is
657
+ * styled for a game (no scroll bars or selection, touch-action none),
658
+ * where its own inline style does not say otherwise
651
659
  * It keeps its own inline styles and the canvas centers inside it, but the canvas is still sized from the window,
652
660
  * so set canvasFixedSize or canvasMaxSize to fit a smaller element
653
661
  * @example
@@ -2890,6 +2898,9 @@ declare module "littlejsengine" {
2890
2898
  * across the sprite or the mesh's own uv
2891
2899
  * - Names in 3D only: worldPos, worldNormal, cameraPos, sunDirection, sunColor, ambientColor, ambientGroundColor,
2892
2900
  * lightCount, lights[i], lightColors[i] and shadow()
2901
+ * - In 3D the snippet may also define void mainNormal(inout vec3 n), given the normal facing the camera after the
2902
+ * normal map, in world space, to bend it per pixel for waves or ripples; the lighting, specular, reflection and
2903
+ * fog then all use it, as they use a normal map
2893
2904
  * - In 3D the shadow map is drawn without the Shader, cut only by the texture's alpha, so a snippet that removes
2894
2905
  * parts of a surface still shadows with the whole of it
2895
2906
  * @example
@@ -5350,6 +5361,9 @@ declare module "littlejsengine" {
5350
5361
  constructor(shaderCode?: string, includeMainCanvas?: boolean, feedbackTexture?: boolean);
5351
5362
  /** @property {string} - The shadertoy style mainImage code it shades with, see setShaderCode */
5352
5363
  shaderCode: string;
5364
+ /** @property {boolean} - Is the pass on? Off, the frame shows as the engine drew it, the shader is kept,
5365
+ * and the feedback texture holds the last frame drawn with it on */
5366
+ enabled: boolean;
5353
5367
  /** @property {Object<string, number|Array<number>>} - The game's own values for the shader, a uniform each
5354
5368
  * by its name, a number a float and a list of 2 to 4 numbers a vector, set every frame as they are; an
5355
5369
  * effect setting can be one of these names, so it changes every frame without making the shader again, all
@@ -8788,7 +8802,8 @@ declare module "littlejsengine" {
8788
8802
  * - EngineObject3D is an EngineObject with a 3D position, rotation and mesh
8789
8803
  * - The 3D scene draws under the 2D sprites, so HUD and text land on top
8790
8804
  * - Lighting is the sun plus ambient, with optional extra lights, fog and shadows
8791
- * - Any object or draw can bring its own Shader, a mainImage snippet the lighting then applies to
8805
+ * - Any object or draw can bring its own Shader, a mainImage snippet the lighting then applies to, and a
8806
+ * mainNormal in it bends the normal the lighting uses
8792
8807
  * - Meshes and the basic builders are in render3dMesh.js, EngineObject3D, instancing and lights in
8793
8808
  * render3dObject.js, both right after this one; the other builders, terrain, particles, camera controls and the
8794
8809
  * OBJ loader are in the Render3D Extras plugin, which goes after those
@@ -8873,7 +8888,9 @@ declare module "littlejsengine" {
8873
8888
  /** @property {Vector3|undefined} - Center of the shadowed area, read each frame, undefined follows the camera
8874
8889
  * @type {Vector3|undefined} */
8875
8890
  shadowCenter: Vector3 | undefined;
8876
- /** @property {number} - Stops surfaces shadowing themselves, raise for speckles, lower if shadows drift off */
8891
+ /** @property {number} - Stops surfaces shadowing themselves, raise for speckles, lower if shadows drift off;
8892
+ * a share of the shadow map's depth, twice shadowRange for the sun and a spotlight's radius, so the gap behind
8893
+ * a caster grows with the range as the map's texels do, and the speckles they make stay away */
8877
8894
  shadowBias: number;
8878
8895
  /** @property {number} - How much to blur the shadow edges */
8879
8896
  shadowSoftness: number;
@@ -9417,6 +9434,9 @@ declare module "littlejsengine" {
9417
9434
  /** @property {boolean} - Line the 3D camera up with the 2D camera, so 3D things at z=0 sit on the 2D sprites;
9418
9435
  * it lines up with the 2D view of the main canvas, whatever canvas size a screenToRay is given */
9419
9436
  align2D: boolean;
9437
+ /** @property {number} - The z of the plane align2D lines up with the 2D view, the camera sitting its
9438
+ * distance in front of it */
9439
+ align2DZ: number;
9420
9440
  /** Returns the camera's world transform
9421
9441
  * @return {Matrix4} */
9422
9442
  getMatrix(): Matrix4;
@@ -35,7 +35,7 @@ const engineName = 'LittleJS';
35
35
  * @type {string}
36
36
  * @default
37
37
  * @memberof Engine */
38
- const engineVersion = '1.26.0';
38
+ const engineVersion = '1.26.1';
39
39
 
40
40
  /** Frames per second to update
41
41
  * @type {number}
@@ -69,6 +69,8 @@ let engineObjectsCollideStaticLast = [];
69
69
  let frame = 0;
70
70
 
71
71
  /** Current engine time since start in seconds
72
+ * - Below 20 FPS it falls behind the clock, since a frame catches up at most 50 ms of updates, so a timer in
73
+ * real seconds, one that must keep up on a slow device, goes by timeReal
72
74
  * @type {number}
73
75
  * @memberof Engine */
74
76
  let time = 0;
@@ -94,8 +96,13 @@ function getPaused() { return paused; }
94
96
  * @memberof Engine */
95
97
  function setPaused(isPaused=true) { paused = isPaused; }
96
98
 
99
+ /** Frames drawn per second, smoothed over the last few seconds, in release builds too
100
+ * @type {number}
101
+ * @memberof Engine */
102
+ let averageFPS = 0;
103
+
97
104
  // Engine internal variables
98
- let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
105
+ let frameTimeLastMS = 0, frameTimeBufferMS = 0;
99
106
 
100
107
  // delta smoothing, after Time Delta Smoothing by Frank Force (2013): a frame is on screen for whole display frames,
101
108
  // so each delta is rounded to them and the rest is carried to the next, keeping the total real time; the frame
@@ -436,7 +443,9 @@ function engineLoadingScreenDraw(elapsed)
436
443
  * @param {GameCallback} [gameRender] - Called before objects are rendered, use for drawing backgrounds/world elements
437
444
  * @param {GameCallback} [gameRenderPost] - Called after objects are rendered, use for drawing UI/overlays
438
445
  * @param {Array<string>|string} [imageSources=[]] - List of image file paths to preload (e.g., ['player.png', 'tiles.png']), or one path
439
- * @param {HTMLElement} [rootElement] - Root DOM element to attach canvas to, defaults to document.body
446
+ * @param {HTMLElement} [rootElement] - Root DOM element to attach canvas to, defaults to document.body; it is
447
+ * styled for a game (no scroll bars or selection, touch-action none),
448
+ * where its own inline style does not say otherwise
440
449
  * It keeps its own inline styles and the canvas centers inside it, but the canvas is still sized from the window,
441
450
  * so set canvasFixedSize or canvasMaxSize to fit a smaller element
442
451
  * @example
@@ -499,8 +508,10 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
499
508
  // by ~page-load-time when RAF starts handing real timestamps
500
509
  if (!frameTimeLastMS) frameTimeDeltaMS = 0;
501
510
  frameTimeLastMS = frameTimeMS;
502
- if (debug || debugWatermark)
503
- averageFPS = lerp(averageFPS, 1e3/(frameTimeDeltaMS||1), .05);
511
+ // the first frame's rate seeds the average, so it does not start out climbing from 0; a gap of over a
512
+ // second, a hidden tab coming back, is not a frame
513
+ if (frameTimeDeltaMS && frameTimeDeltaMS < 1e3)
514
+ averageFPS = averageFPS ? lerp(averageFPS, 1e3/frameTimeDeltaMS, .05) : 1e3/frameTimeDeltaMS;
504
515
  // the time the frame will be on screen, in whole display frames; engineStep's steps are exact already
505
516
  if (!manualStepAtStart)
506
517
  frameTimeDeltaMS = engineSmoothDelta(frameTimeDeltaMS);
@@ -5748,6 +5759,9 @@ class SpriteAnimator
5748
5759
  * across the sprite or the mesh's own uv
5749
5760
  * - Names in 3D only: worldPos, worldNormal, cameraPos, sunDirection, sunColor, ambientColor, ambientGroundColor,
5750
5761
  * lightCount, lights[i], lightColors[i] and shadow()
5762
+ * - In 3D the snippet may also define void mainNormal(inout vec3 n), given the normal facing the camera after the
5763
+ * normal map, in world space, to bend it per pixel for waves or ripples; the lighting, specular, reflection and
5764
+ * fog then all use it, as they use a normal map
5751
5765
  * - In 3D the shadow map is drawn without the Shader, cut only by the texture's alpha, so a snippet that removes
5752
5766
  * parts of a surface still shadows with the whole of it
5753
5767
  * @example
@@ -7899,8 +7913,9 @@ function inputInit()
7899
7913
  }
7900
7914
  function onContextMenu(e)
7901
7915
  {
7902
- // prevent right click menu, but a text field keeps its copy and paste menu
7903
- if (inputPreventDefault && !isTextInput(e.target))
7916
+ // prevent right click menu, but a text field keeps its copy and paste menu and a link its open in new tab
7917
+ const target = /** @type {HTMLElement} */ (e.target);
7918
+ if (inputPreventDefault && !isTextInput(target) && !target?.closest?.('a[href]'))
7904
7919
  e.preventDefault();
7905
7920
  }
7906
7921
  function onBlur()
@@ -7921,6 +7936,10 @@ function inputInit()
7921
7936
  // handle all touch events the same way
7922
7937
  function handleTouch(e)
7923
7938
  {
7939
+ // fix stalled audio requiring user interaction, with touch input off too since the page's sound needs it
7940
+ if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
7941
+ audioResume();
7942
+
7924
7943
  if (!touchInputEnable)
7925
7944
  {
7926
7945
  // turned off mid touch, the finger that drove the mouse lets go of it
@@ -7931,10 +7950,6 @@ function inputInit()
7931
7950
  }
7932
7951
  inputLastTouchTime = performance.now();
7933
7952
 
7934
- // fix stalled audio requiring user interaction
7935
- if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
7936
- audioResume();
7937
-
7938
7953
  // when the touch gamepad is enabled it owns touch input: suppress the
7939
7954
  // touch->mouse passthrough entirely unless touchGamepadPassthrough is set
7940
7955
  // (its own zones drive gameplay via pointer events)
@@ -14193,6 +14208,10 @@ class PostProcessPlugin
14193
14208
  /** @property {string} - The shadertoy style mainImage code it shades with, see setShaderCode */
14194
14209
  this.shaderCode = shaderCode || postProcessEffects(); // no code passes the frame through
14195
14210
 
14211
+ /** @property {boolean} - Is the pass on? Off, the frame shows as the engine drew it, the shader is kept,
14212
+ * and the feedback texture holds the last frame drawn with it on */
14213
+ this.enabled = true;
14214
+
14196
14215
  /** @property {Object<string, number|Array<number>>} - The game's own values for the shader, a uniform each
14197
14216
  * by its name, a number a float and a list of 2 to 4 numbers a vector, set every frame as they are; an
14198
14217
  * effect setting can be one of these names, so it changes every frame without making the shader again, all
@@ -14278,7 +14297,7 @@ class PostProcessPlugin
14278
14297
  }
14279
14298
  function postProcessRender()
14280
14299
  {
14281
- if (headlessMode || !glEnable) return;
14300
+ if (headlessMode || !glEnable || !postProcess.enabled) return;
14282
14301
 
14283
14302
  // clear out the buffer, before anything here binds its own
14284
14303
  glFlush();
@@ -24708,7 +24727,8 @@ function raycastBox(ray, pos, size, rotation)
24708
24727
  * - EngineObject3D is an EngineObject with a 3D position, rotation and mesh
24709
24728
  * - The 3D scene draws under the 2D sprites, so HUD and text land on top
24710
24729
  * - Lighting is the sun plus ambient, with optional extra lights, fog and shadows
24711
- * - Any object or draw can bring its own Shader, a mainImage snippet the lighting then applies to
24730
+ * - Any object or draw can bring its own Shader, a mainImage snippet the lighting then applies to, and a
24731
+ * mainNormal in it bends the normal the lighting uses
24712
24732
  * - Meshes and the basic builders are in render3dMesh.js, EngineObject3D, instancing and lights in
24713
24733
  * render3dObject.js, both right after this one; the other builders, terrain, particles, camera controls and the
24714
24734
  * OBJ loader are in the Render3D Extras plugin, which goes after those
@@ -25286,7 +25306,9 @@ class Render3DPlugin
25286
25306
  /** @property {Vector3|undefined} - Center of the shadowed area, read each frame, undefined follows the camera
25287
25307
  * @type {Vector3|undefined} */
25288
25308
  this.shadowCenter = undefined;
25289
- /** @property {number} - Stops surfaces shadowing themselves, raise for speckles, lower if shadows drift off */
25309
+ /** @property {number} - Stops surfaces shadowing themselves, raise for speckles, lower if shadows drift off;
25310
+ * a share of the shadow map's depth, twice shadowRange for the sun and a spotlight's radius, so the gap behind
25311
+ * a caster grows with the range as the map's texels do, and the speckles they make stay away */
25290
25312
  this.shadowBias = .003;
25291
25313
  /** @property {number} - How much to blur the shadow edges */
25292
25314
  this.shadowSoftness = 1;
@@ -26320,6 +26342,9 @@ class Camera3D
26320
26342
  /** @property {boolean} - Line the 3D camera up with the 2D camera, so 3D things at z=0 sit on the 2D sprites;
26321
26343
  * it lines up with the 2D view of the main canvas, whatever canvas size a screenToRay is given */
26322
26344
  this.align2D = false;
26345
+ /** @property {number} - The z of the plane align2D lines up with the 2D view, the camera sitting its
26346
+ * distance in front of it */
26347
+ this.align2DZ = 0;
26323
26348
  }
26324
26349
 
26325
26350
  /** Returns the camera's world transform
@@ -26390,7 +26415,7 @@ class Camera3D
26390
26415
  ASSERT(!canvasHeight || distance < this.far,
26391
26416
  'align2D needs this camera distance to match the 2D view, raise camera.far past it', distance);
26392
26417
  this.orthographic &&= halfHeight * 2; // an orthographic camera stays orthographic and shows the same height
26393
- this.pos = vec3(cameraPos.x, cameraPos.y, distance);
26418
+ this.pos = vec3(cameraPos.x, cameraPos.y, this.align2DZ + distance);
26394
26419
  this.rotation = vec3(0, 0, -cameraAngle); // 2D angles turn the other way
26395
26420
  }
26396
26421
  }
@@ -26442,7 +26467,8 @@ const RENDER3D_SNIPPET_NAMES =
26442
26467
  '#define lights extraLights\n' +
26443
26468
  '#define lightColors extraLightColors\n';
26444
26469
 
26445
- // the fragment shader; given a Shader's snippet, its mainImage replaces the texture sample and all else is the same
26470
+ // the fragment shader; given a Shader's snippet, its mainImage replaces the texture sample and all else is the same,
26471
+ // and a mainNormal in it bends the normal after the normal map, before all the lighting reads it
26446
26472
  // uniforms: lightDir (xyz the way the sunlight travels, w = emissive, 1 or more skips the lighting),
26447
26473
  // lightColor (the sun's rgb, a = specular), ambientFog (rgb, a = fogEnd), fogColor (rgb, a = fogStart),
26448
26474
  // cameraPos, tex, shadowMap, shadowParams (x = shadows on, y = bias, z = blur step in texture space,
@@ -26451,6 +26477,7 @@ const RENDER3D_SNIPPET_NAMES =
26451
26477
  // emissive map is on), skyTop, skyHorizon, skyBottom (what a reflection shows), normalTex, emissiveTex
26452
26478
  function render3DFragmentSource(fragmentCode)
26453
26479
  {
26480
+ const hasNormal = /\bmainNormal\s*\(/.test(fragmentCode || '');
26454
26481
  return '#version 300 es\n' +
26455
26482
  'precision highp float;' +
26456
26483
  'uniform vec4 lightDir,lightColor,ambientFog,ambientGround,fogColor,shadowParams;' +
@@ -26498,6 +26525,7 @@ function render3DFragmentSource(fragmentCode)
26498
26525
  'return normalize((u*m.x-v*m.y)*inversesqrt(k)+n*m.z);}' +
26499
26526
  // a snippet's mainImage is declared here and written after main, so a define in it can not reach main
26500
26527
  (fragmentCode ? 'void mainImage(out vec4,vec2);' : '') +
26528
+ (hasNormal ? 'void mainNormal(inout vec3);' : '') +
26501
26529
  'void main(){' +
26502
26530
  (fragmentCode ? 'vec4 t;mainImage(t,T);' : 'vec4 t=texture(tex,T);') +
26503
26531
  'if(premultipliedTexture&&t.a>0.)t.rgb/=t.a;' + // back to straight color, what the lighting and blend expect
@@ -26507,6 +26535,7 @@ function render3DFragmentSource(fragmentCode)
26507
26535
  'vec3 n=dot(N,N)>0.?normalize(N):vec3(0,1,0);' +
26508
26536
  'if(!gl_FrontFacing)n=-n;' + // only a double sided mesh shows a back face, light it on the side that is seen
26509
26537
  'if(materialParams.x!=0.)n=normalMapNormal(n);' +
26538
+ (hasNormal ? 'mainNormal(n);n=normalize(n);' : '') +
26510
26539
  'float nl=dot(n,-lightDir.xyz);' +
26511
26540
  // the shadow is the sun's, or one spotlight's: shadowParams.x is 1 for the sun, 2 and up for that Light3D
26512
26541
  'float sh=shadow(),s=shadowParams.x<1.5?sh:1.;' +
@@ -43439,6 +43468,7 @@ export
43439
43468
  frame,
43440
43469
  time,
43441
43470
  timeReal,
43471
+ averageFPS,
43442
43472
  paused,
43443
43473
  getPaused,
43444
43474
  setPaused,