littlejsengine 1.21.0 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "littlejsengine",
3
- "version": "1.21.0",
3
+ "version": "1.22.0",
4
4
  "description": "LittleJS - Tiny and Fast HTML5 Game Engine",
5
5
  "main": "dist/littlejs.esm.js",
6
6
  "types": "dist/littlejs.d.ts",
@@ -45,7 +45,8 @@
45
45
  "build": "node src/engineBuild.mjs",
46
46
  "build-docs": "node tools/buildDocs.mjs",
47
47
  "build-release": "node tools/buildRelease.mjs",
48
- "test": "node --test --import ./test/setup.mjs \"test/**/*.test.mjs\""
48
+ "test": "node --test --import ./test/setup.mjs \"test/**/*.test.mjs\"",
49
+ "test:browser": "node test/browser/run.mjs"
49
50
  },
50
51
  "engines": {
51
52
  "node": "^20.19.0 || >=22.12.0"
package/plugins/gltf.js CHANGED
@@ -361,7 +361,13 @@ async function loadGLTF(url)
361
361
  const response = await fetch(url);
362
362
  if (!response.ok)
363
363
  throw new Error('loadGLTF failed: ' + url);
364
- return parseGLTF(await response.arrayBuffer(), url.slice(0, url.lastIndexOf('/') + 1));
364
+ // the files beside it are beside where it came from, after any redirect; a blob or data url has nothing
365
+ // beside it, and a model in one is whole, or parsed with parseGLTF and a base of its own
366
+ const from = response.url || url;
367
+ let base = '';
368
+ if (!/^(blob|data):/i.test(from))
369
+ base = response.url ? new URL('.', from).href : from.slice(0, from.lastIndexOf('/') + 1);
370
+ return parseGLTF(await response.arrayBuffer(), base);
365
371
  }
366
372
 
367
373
  /** Parse a model from GLB bytes or glTF JSON, fetching the buffers and images it refers to
@@ -20,6 +20,8 @@
20
20
  * shadow map through renderShadow(), which calls render() by default; obj.castShadow = false keeps it
21
21
  * out (a floor TileLayer, a background), a draw's alpha sets how much light it blocks, and
22
22
  * setShadowTransparent lets its color tint the light
23
+ * - Set light.glow for a soft hazy glow over a light, like a lamp at night; it is added over the lit scene after the
24
+ * lightmap, so it shows in the dark and sits in front of everything there
23
25
  * - Must be constructed BEFORE PostProcessPlugin so post-process sees lit pixels
24
26
  * @namespace LightSystem
25
27
  */
@@ -156,6 +158,7 @@ class LightSystemPlugin
156
158
  // again on a context restore, the canvas may have changed since
157
159
  if (lightSystem.textureSizeAuto)
158
160
  lightSystem.textureSize = mainCanvasSize.copy();
161
+ lightSystem.clampTextureSizes();
159
162
 
160
163
  // allocate the lightmap texture with null data at textureSize
161
164
  lightSystem.texture = glContext.createTexture();
@@ -347,6 +350,7 @@ class LightSystemPlugin
347
350
  const ls = lightSystem;
348
351
 
349
352
  // make the resources the first time, and again when a size changed
353
+ ls.clampTextureSizes();
350
354
  if (!ls.shadowMap || ls.shadowMapSize !== ls.shadowMapSizeAllocated
351
355
  || ls.shadowTextureSize !== ls.shadowTextureSizeAllocated)
352
356
  {
@@ -534,8 +538,14 @@ class LightSystemPlugin
534
538
  // is, and any debug text / future draw could sample the lightmap)
535
539
  if (glActiveTexture)
536
540
  glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
537
- setAdditiveBlendMode(prevAdditive);
538
541
  glSetInstancedMode(true);
542
+
543
+ // 7. the lights' glows, added over the lit scene so the darkness does not dim them
544
+ setAdditiveBlendMode();
545
+ for (const o of engineObjects)
546
+ o instanceof Light && !o.destroyed && o.renderGlow();
547
+ glFlush();
548
+ setAdditiveBlendMode(prevAdditive);
539
549
  }
540
550
  function lightSystemContextLost()
541
551
  {
@@ -554,6 +564,17 @@ class LightSystemPlugin
554
564
  }
555
565
  }
556
566
 
567
+ /** Bring the sizes of the textures it makes down to what the device can make: shadowMapSize, shadowTextureSize
568
+ * and a textureSize given by hand; called before they are made, so a size too big falls back instead of failing */
569
+ clampTextureSizes()
570
+ {
571
+ this.shadowMapSize = glClampTextureSize(this.shadowMapSize);
572
+ this.shadowTextureSize = glClampTextureSize(this.shadowTextureSize);
573
+ const size = this.textureSize;
574
+ if (size)
575
+ this.textureSize = vec2(glClampTextureSize(size.x), glClampTextureSize(size.y));
576
+ }
577
+
557
578
  /** Draw a single Light's falloff blob into the currently bound lightmap.
558
579
  * Called by Light.renderLight() during the plugin's render pass.
559
580
  * @param {Light} light */
@@ -703,6 +724,14 @@ class Light extends EngineObject
703
724
  * or torch that holds it, or the player carrying it, does not block it; it has to reach past that object's
704
725
  * corners, about half its diagonal and a little more, or dark rays run out from them */
705
726
  this.shadowCore = 0;
727
+ /** @property {number} - Size across of a soft hazy glow drawn over the light, like a lamp at night, 0 for
728
+ * none; it is added over the lit scene, in front of everything there */
729
+ this.glow = 0;
730
+ /** @property {number} - How fast the glow fades from its middle: 1 by default, .5 a wide haze, 2 a tight
731
+ * bright core */
732
+ this.glowFalloff = 1;
733
+ /** @type {TileInfo|undefined} */
734
+ this.glowTileInfo = undefined; // the whole glow texture, kept for the falloff it was made for
706
735
  }
707
736
 
708
737
  /** Lights are invisible in the main render pass — they only contribute
@@ -716,4 +745,18 @@ class Light extends EngineObject
716
745
  {
717
746
  lightSystem && lightSystem.drawLight(this);
718
747
  }
748
+
749
+ /** Draw this light's glow, soft and round, its glow size across and in its color, added over the lit scene;
750
+ * called by LightSystemPlugin after the lightmap is applied */
751
+ renderGlow()
752
+ {
753
+ if (!(this.glow > 0)) return;
754
+ const size = vec2(this.glow);
755
+ if (!isOnScreen(this.pos, size)) return;
756
+ const texture = engineGlowTexture(this.glowFalloff);
757
+ if (!texture) return;
758
+ if (this.glowTileInfo?.textureInfo !== texture)
759
+ this.glowTileInfo = new TileInfo(vec2(), texture.size, texture);
760
+ drawTile(this.pos, size, this.glowTileInfo, this.color, 0, false, undefined, true, false);
761
+ }
719
762
  }
package/plugins/math3d.js CHANGED
@@ -338,15 +338,7 @@ class Matrix4
338
338
  static rotation(euler, matrix=new Matrix4)
339
339
  {
340
340
  ASSERT_VECTOR3_VALID(euler);
341
- const cx = cos(euler.x), sx = sin(euler.x);
342
- const cy = cos(euler.y), sy = sin(euler.y);
343
- const cz = cos(euler.z), sz = sin(euler.z);
344
- const m = matrix.m;
345
- // R = Ry * Rx * Rz written out, column major, every element set so a reused matrix comes out clean
346
- m[0] = cy*cz + sy*sx*sz; m[1] = cx*sz; m[2] = -sy*cz + cy*sx*sz; m[3] = 0;
347
- m[4] = -cy*sz + sy*sx*cz; m[5] = cx*cz; m[6] = sy*sz + cy*sx*cz; m[7] = 0;
348
- m[8] = sy*cx; m[9] = -sx; m[10] = cy*cx; m[11] = 0;
349
- m[12] = m[13] = m[14] = 0; m[15] = 1;
341
+ matrix4RotationAt(matrix.m, 0, euler);
350
342
  return matrix;
351
343
  }
352
344
 
@@ -594,24 +586,54 @@ class Matrix4
594
586
  * @memberof Math3D
595
587
  */
596
588
  function buildMatrix(pos, rotation, scale, matrix=new Matrix4)
589
+ {
590
+ ASSERT(matrix instanceof Matrix4, 'the matrix to write into must be a Matrix4');
591
+ matrix4Compose(matrix.m, 0, pos, rotation, scale);
592
+ return matrix;
593
+ }
594
+
595
+ // the rotation matrix of euler angles written into 16 floats of an array at k
596
+ function matrix4RotationAt(m, k, euler)
597
+ {
598
+ const cx = cos(euler.x), sx = sin(euler.x);
599
+ const cy = cos(euler.y), sy = sin(euler.y);
600
+ const cz = cos(euler.z), sz = sin(euler.z);
601
+ // R = Ry * Rx * Rz written out, column major, every element set so a reused matrix comes out clean
602
+ m[k] = cy*cz + sy*sx*sz; m[k+1] = cx*sz; m[k+2] = -sy*cz + cy*sx*sz; m[k+3] = 0;
603
+ m[k+4] = -cy*sz + sy*sx*cz; m[k+5] = cx*cz; m[k+6] = sy*sz + cy*sx*cz; m[k+7] = 0;
604
+ m[k+8] = sy*cx; m[k+9] = -sx; m[k+10] = cy*cx; m[k+11] = 0;
605
+ m[k+12] = m[k+13] = m[k+14] = 0; m[k+15] = 1;
606
+ }
607
+
608
+ // the transform buildMatrix makes, written into 16 floats of an array at k, so an instance buffer takes it straight in
609
+ // the rotation columns are scaled and the position dropped in, instead of multiplying three matrices, each value
610
+ // worked out whole and stored once; InstancedMesh3D.setTransforms writes the same expressions for many at a time
611
+ function matrix4Compose(m, k, pos, rotation, scale)
597
612
  {
598
613
  ASSERT(!pos || isVector3(pos), 'pos must be a Vector3', pos);
599
614
  ASSERT(!scale || isVector3(scale), 'scale must be a Vector3', scale);
600
- ASSERT(matrix instanceof Matrix4, 'the matrix to write into must be a Matrix4');
601
- // scale the rotation columns and drop the position in, instead of multiplying three matrices
615
+ const scaleX = scale ? scale.x : 1, scaleY = scale ? scale.y : 1, scaleZ = scale ? scale.z : 1;
602
616
  // an object that is not turned at all is most of a big scene, and identity is what the six
603
617
  // trig calls would have worked out to anyway
604
- const turned = rotation && (rotation.x || rotation.y || rotation.z), m = matrix.m;
605
- turned ? Matrix4.rotation(rotation, matrix) : m.set(matrix4Identity);
606
- if (scale)
618
+ if (rotation && (rotation.x || rotation.y || rotation.z))
607
619
  {
608
- m[0] *= scale.x; m[1] *= scale.x; m[2] *= scale.x;
609
- m[4] *= scale.y; m[5] *= scale.y; m[6] *= scale.y;
610
- m[8] *= scale.z; m[9] *= scale.z; m[10] *= scale.z;
620
+ ASSERT_VECTOR3_VALID(rotation);
621
+ const cx = cos(rotation.x), sx = sin(rotation.x);
622
+ const cy = cos(rotation.y), sy = sin(rotation.y);
623
+ const cz = cos(rotation.z), sz = sin(rotation.z);
624
+ // R = Ry * Rx * Rz written out, column major, as matrix4RotationAt, each column times its scale
625
+ m[k] = (cy*cz + sy*sx*sz) * scaleX; m[k+1] = cx*sz * scaleX; m[k+2] = (-sy*cz + cy*sx*sz) * scaleX;
626
+ m[k+4] = (-cy*sz + sy*sx*cz) * scaleY; m[k+5] = cx*cz * scaleY; m[k+6] = (sy*sz + cy*sx*cz) * scaleY;
627
+ m[k+8] = sy*cx * scaleZ; m[k+9] = -sx * scaleZ; m[k+10] = cy*cx * scaleZ;
611
628
  }
612
- if (pos)
613
- m[12] = pos.x, m[13] = pos.y, m[14] = pos.z;
614
- return matrix;
629
+ else
630
+ {
631
+ m[k] = scaleX; m[k+1] = 0; m[k+2] = 0;
632
+ m[k+4] = 0; m[k+5] = scaleY; m[k+6] = 0;
633
+ m[k+8] = 0; m[k+9] = 0; m[k+10] = scaleZ;
634
+ }
635
+ m[k+3] = m[k+7] = m[k+11] = 0; m[k+15] = 1;
636
+ m[k+12] = pos ? pos.x : 0, m[k+13] = pos ? pos.y : 0, m[k+14] = pos ? pos.z : 0;
615
637
  }
616
638
 
617
639
  ///////////////////////////////////////////////////////////////////////////////