littlejsengine 1.25.0 → 1.26.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/FAQ.md +49 -11
  3. package/README.md +6 -2
  4. package/REFERENCE.md +120 -18
  5. package/dist/littlejs.d.ts +464 -99
  6. package/dist/littlejs.esm.js +2853 -704
  7. package/dist/littlejs.esm.min.js +3 -3
  8. package/dist/littlejs.js +2844 -701
  9. package/dist/littlejs.min.js +2 -2
  10. package/dist/littlejs.release.js +2658 -512
  11. package/package.json +4 -4
  12. package/plugins/box2d.js +110 -34
  13. package/plugins/gltf.js +74 -22
  14. package/plugins/lightSystem.js +410 -28
  15. package/plugins/math3d.js +4 -2
  16. package/plugins/medalSystem.js +19 -7
  17. package/plugins/newgrounds.js +4 -2
  18. package/plugins/parallax.js +1 -0
  19. package/plugins/particleEffects.js +44 -10
  20. package/plugins/pathFinder.js +9 -11
  21. package/plugins/pluginExport.js +9 -0
  22. package/plugins/postProcess.js +4 -2
  23. package/plugins/render3d.js +485 -2385
  24. package/plugins/render3dDebug.js +5 -8
  25. package/plugins/render3dEditor.js +81 -6
  26. package/plugins/render3dExtras.js +103 -31
  27. package/plugins/render3dLevel.js +53 -5
  28. package/plugins/render3dMesh.js +1158 -0
  29. package/plugins/render3dObject.js +1451 -0
  30. package/plugins/render3dVoxels.js +15 -3
  31. package/plugins/textureSheet.js +9 -8
  32. package/plugins/threejs.js +1 -1
  33. package/plugins/tweenSystem.js +76 -51
  34. package/plugins/uiSystem.js +37 -8
  35. package/plugins/wavedash.js +229 -0
  36. package/src/engine.js +184 -36
  37. package/src/engineAudio.js +37 -21
  38. package/src/engineBuild.mjs +6 -1
  39. package/src/engineDebug.js +22 -162
  40. package/src/engineDraw.js +139 -34
  41. package/src/engineEditor.js +79 -18
  42. package/src/engineExport.js +0 -3
  43. package/src/engineInput.js +27 -18
  44. package/src/engineMath.js +35 -14
  45. package/src/engineObject.js +60 -41
  46. package/src/engineParticles.js +58 -11
  47. package/src/engineRelease.js +1 -5
  48. package/src/engineSettings.js +48 -9
  49. package/src/engineTileLayer.js +11 -4
  50. package/src/engineUtilities.js +41 -9
  51. package/src/engineWebGL.js +37 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "littlejsengine",
3
- "version": "1.25.0",
3
+ "version": "1.26.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",
@@ -11,6 +11,8 @@
11
11
  "default": "./dist/littlejs.esm.js"
12
12
  },
13
13
  "./dist/*": "./dist/*",
14
+ "./plugins/*": "./plugins/*",
15
+ "./src/*": "./src/*",
14
16
  "./package.json": "./package.json"
15
17
  },
16
18
  "repository": {
@@ -52,12 +54,10 @@
52
54
  "test:browser": "node test/browser/run.mjs"
53
55
  },
54
56
  "engines": {
55
- "node": "^20.19.0 || >=22.12.0"
57
+ "node": ">=22.12.0"
56
58
  },
57
59
  "devDependencies": {
58
60
  "bestzip": "^2.2.1",
59
- "electron": "^38.2.0",
60
- "electron-packager": "^17.1.2",
61
61
  "google-closure-compiler": "~20230502.0.0",
62
62
  "jsdoc": "~4.0.2",
63
63
  "roadroller": "~2.1.0",
package/plugins/box2d.js CHANGED
@@ -35,7 +35,7 @@ let box2dDebug = false;
35
35
  let box2dTempVectors;
36
36
  function box2dTemp(v, slot=0)
37
37
  {
38
- ASSERT(isVector2(v));
38
+ ASSERT(isVector2(v), 'Box2D: expected a vec2', v);
39
39
  const temp = (box2dTempVectors ||= [new box2d.instance.b2Vec2(), new box2d.instance.b2Vec2()])[slot];
40
40
  temp.Set(v.x, v.y);
41
41
  return temp;
@@ -88,6 +88,9 @@ function box2dWakeWithContacts(body)
88
88
  edge.get_other().SetAwake(true);
89
89
  }
90
90
 
91
+ // a number Box2D can take: it stops for good on NaN or Infinity, and a release build has no asserts to catch one first
92
+ const box2dFinite = (value, otherwise)=> isFinite(value) ? value : otherwise;
93
+
91
94
  // wake both bodies of a joint whose length, spring, ratio or strength changed, a sleeping body would stay where it was
92
95
  function box2dWakeJoint(joint)
93
96
  {
@@ -147,6 +150,64 @@ function box2dDestroyGears(joint)
147
150
  * @memberof Box2D */
148
151
  function box2dSetDebug(enable) { box2dDebug = enable; }
149
152
 
153
+ // the EngineObject physics fields a Box2D body does not read, each with what moves the body instead; a debug build
154
+ // warns once a field when a game sets one, which did nothing and said nothing
155
+ const box2dUnusedFields = {
156
+ velocity: 'setLinearVelocity', angleVelocity: 'setAngularVelocity', damping: 'setLinearDamping',
157
+ angleDamping: 'setAngularDamping', mass: 'setMass or the density of its shapes',
158
+ friction: 'the friction of its shapes, given to addBox and the rest',
159
+ restitution: 'the restitution of its shapes, given to addBox and the rest'};
160
+ const box2dUnusedWarned = new Set, box2dUnusedStarts = new WeakMap;
161
+
162
+ // the values of those fields as numbers, a vector as its two, then gravityScale, kept for the object
163
+ function box2dUnusedFieldValues(o)
164
+ {
165
+ const values = [];
166
+ for (const name in box2dUnusedFields)
167
+ {
168
+ const value = o[name];
169
+ value instanceof Vector2 ? values.push(value.x, value.y) : values.push(value);
170
+ }
171
+ values.push(o.gravityScale);
172
+ return values;
173
+ }
174
+
175
+ // warn once a field
176
+ function box2dUnusedWarn(name, use)
177
+ {
178
+ if (box2dUnusedWarned.has(name)) return;
179
+ box2dUnusedWarned.add(name);
180
+ console.warn('Box2dObject.' + name + ' does nothing on a Box2D body, use ' + use);
181
+ }
182
+
183
+ // warn once a field of one set on a Box2dObject, and of gravityScale set on the field, not with setGravityScale;
184
+ // compared in place, as it runs for every body every step of a debug build
185
+ function box2dCheckUnusedFields(o)
186
+ {
187
+ const start = box2dUnusedStarts.get(o);
188
+ if (!start) return;
189
+ let i = 0;
190
+ for (const name in box2dUnusedFields)
191
+ {
192
+ const value = o[name];
193
+ let changed;
194
+ if (value instanceof Vector2)
195
+ {
196
+ changed = value.x !== start[i] || value.y !== start[i + 1];
197
+ i += 2;
198
+ }
199
+ else
200
+ changed = value !== start[i++];
201
+ changed && box2dUnusedWarn(name, box2dUnusedFields[name]);
202
+ }
203
+
204
+ // the body's own scale is a call into Box2D, asked only when the field changed since it was last asked,
205
+ // since setGravityScale sets both
206
+ if (o.gravityScale === start[i]) return;
207
+ start[i] = o.gravityScale;
208
+ Math.fround(o.gravityScale) === o.body.GetGravityScale() || box2dUnusedWarn('gravityScale', 'setGravityScale');
209
+ }
210
+
150
211
  ///////////////////////////////////////////////////////////////////////////////
151
212
  /**
152
213
  * Box2D Object - extend with your own custom physics objects
@@ -154,6 +215,10 @@ function box2dSetDebug(enable) { box2dDebug = enable; }
154
215
  * - Provides interface for Box2D body and fixture functions
155
216
  * - Each object can have multiple fixtures and joints
156
217
  * - Angular values are clockwise like angle: angular velocity, torque, joint angles, limits and motor speeds
218
+ * - Box2D moves the body, so the EngineObject physics fields do nothing on it: velocity, angleVelocity, damping,
219
+ * angleDamping, mass, friction, restitution and gravityScale; use setLinearVelocity, setAngularVelocity,
220
+ * setLinearDamping, setAngularDamping, setMass, the friction and restitution of its shapes and setGravityScale.
221
+ * A debug build warns once when one of them is set
157
222
  * @extends EngineObject
158
223
  * @memberof Box2D
159
224
  */
@@ -196,6 +261,9 @@ class Box2dObject extends EngineObject
196
261
 
197
262
  this.body.object = this; // link body to this object
198
263
  box2d.objects.push(this); // keep track of all box2d objects
264
+
265
+ // a debug build notes the engine physics fields Box2D does not read, to warn when one is set
266
+ debug && box2dUnusedStarts.set(this, box2dUnusedFieldValues(this));
199
267
  }
200
268
 
201
269
  /** Destroy this object and its physics body
@@ -244,7 +312,7 @@ class Box2dObject extends EngineObject
244
312
  this.drawFixtures(this.color, this.lineColor, this.lineWidth);
245
313
  }
246
314
 
247
- /** Render debug info */
315
+ /** Draw its fixtures for the debug view: yellow asleep, blue static, both when both */
248
316
  renderDebugInfo()
249
317
  {
250
318
  const isAsleep = !this.getIsAwake();
@@ -253,7 +321,7 @@ class Box2dObject extends EngineObject
253
321
  this.drawFixtures(color);
254
322
  }
255
323
 
256
- /** Draws all this object's fixtures
324
+ /** Draw all this object's fixtures
257
325
  * @param {Color} [color]
258
326
  * @param {Color} [lineColor]
259
327
  * @param {number} [lineWidth]
@@ -343,8 +411,8 @@ class Box2dObject extends EngineObject
343
411
  ASSERT(isNumber(angle), 'angle must be a number');
344
412
 
345
413
  // Box2D stops for good on a box with almost no area, like addPoly no fixture is made from one
346
- ASSERT(size.x * size.y > 1e-6, 'box is too small for Box2D');
347
- if (!(size.x * size.y > 1e-6)) return;
414
+ ASSERT(size.x * size.y > 1e-6 && isFinite(size.x * size.y), 'box is too small or too big for Box2D');
415
+ if (!(size.x * size.y > 1e-6) || !isFinite(size.x * size.y)) return;
348
416
 
349
417
  const shape = new box2d.instance.b2PolygonShape();
350
418
  shape.SetAsBox(size.x/2, size.y/2, box2dTemp(offset), -angle);
@@ -449,12 +517,13 @@ class Box2dObject extends EngineObject
449
517
  * @param {number} [friction]
450
518
  * @param {number} [restitution]
451
519
  * @param {boolean} [isSensor]
452
- * @return {Object} - The fixture made, Box2D's own */
520
+ * @return {Object|undefined} - The fixture made, Box2D's own, undefined for a circle too big to make */
453
521
  addCircle(diameter=1, offset=vec2(), density, friction, restitution, isSensor)
454
522
  {
455
- ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
523
+ ASSERT(isNumber(diameter) && diameter>0 && isFinite(diameter), 'diameter must be a positive number');
456
524
  ASSERT(isVector2(offset), 'offset must be a Vector2');
457
-
525
+ if (!isFinite(diameter)) return; // Box2D stops for good on it, no fixture is made
526
+
458
527
  const shape = new box2d.instance.b2CircleShape();
459
528
  shape.set_m_p(box2dTemp(offset));
460
529
  shape.set_m_radius(diameter/2);
@@ -685,12 +754,14 @@ class Box2dObject extends EngineObject
685
754
  setAngularVelocity(angularVelocity)
686
755
  { this.body.SetAngularVelocity(-angularVelocity); }
687
756
 
688
- /** Sets the linear damping
757
+ /** Sets the linear damping, Box2D's: a rate, 0 none and larger slows it faster, with no upper limit, where the
758
+ * damping of an EngineObject is the fraction it keeps each frame and does nothing on a Box2dObject
689
759
  * @param {number} damping */
690
760
  setLinearDamping(damping)
691
761
  { this.body.SetLinearDamping(damping); }
692
762
 
693
- /** Sets the angular damping
763
+ /** Sets the angular damping, Box2D's: a rate, 0 none and larger slows its turning faster, with no upper limit;
764
+ * angleDamping does nothing on a Box2dObject
694
765
  * @param {number} damping */
695
766
  setAngularDamping(damping)
696
767
  { this.body.SetAngularDamping(damping); }
@@ -767,7 +838,7 @@ class Box2dObject extends EngineObject
767
838
  const f = Math.fround;
768
839
  const oldInertia = f(data.get_I() - box2dCenterInertia(oldMass, center.get_x(), center.get_y()));
769
840
  const inertia = momentOfInertia ?? oldInertia;
770
- mass ??= oldMass;
841
+ mass = box2dFinite(mass ?? oldMass, oldMass);
771
842
  const offset = box2dCenterInertia(mass > 0 ? mass : 1, cx, cy); // a mass of 0 or less is 1 to Box2D
772
843
  const I = f(inertia + offset);
773
844
  data.set_mass(mass);
@@ -1088,13 +1159,15 @@ class Box2dJoint
1088
1159
  const bodyA = jointDef.get_bodyA(), bodyB = jointDef.get_bodyB();
1089
1160
  const bothLive = !box2d.isNull(bodyA) && !box2d.isNull(bodyB);
1090
1161
  ASSERT(bothLive, 'a joint needs two objects that are not destroyed');
1091
- ASSERT(box2d.instance.getPointer(bodyA) !== box2d.instance.getPointer(bodyB), 'a joint needs two different objects');
1162
+ const twoObjects = !bothLive || box2d.instance.getPointer(bodyA) !== box2d.instance.getPointer(bodyB);
1163
+ ASSERT(twoObjects, 'a joint needs two different objects');
1092
1164
 
1093
1165
  /** @property {Object} - The Box2d joint, 0 once it is destroyed, as it is when either object is */
1094
1166
  this.box2dJoint = 0;
1095
- if (!bothLive)
1167
+ if (!bothLive || !twoObjects)
1096
1168
  {
1097
- // one of its objects is gone, so it is made destroyed, as it would be had it gone after
1169
+ // one of its objects is gone, so it is made destroyed, as it would be had it gone after; a joint of an
1170
+ // object to itself, which Box2D stops for good on, is made destroyed too in a release build
1098
1171
  box2d.instance.destroy(jointDef);
1099
1172
  return;
1100
1173
  }
@@ -1206,7 +1279,7 @@ class Box2dTargetJoint extends Box2dJoint
1206
1279
 
1207
1280
  /** Sets the joint frequency in Hertz, above 0, Box2D stops for good on 0
1208
1281
  * @param {number} hz */
1209
- setFrequency(hz) { this.box2dJoint.SetFrequency(max(hz, 1e-3)); box2dWakeJoint(this.box2dJoint); }
1282
+ setFrequency(hz) { this.box2dJoint.SetFrequency(max(box2dFinite(hz, 0), 1e-3)); box2dWakeJoint(this.box2dJoint); }
1210
1283
 
1211
1284
  /** Gets the joint frequency in Hertz
1212
1285
  * @return {number} */
@@ -1269,11 +1342,11 @@ class Box2dDistanceJoint extends Box2dJoint
1269
1342
  * @return {number} */
1270
1343
  getFrequency() { return this.box2dJoint.GetFrequency(); }
1271
1344
 
1272
- /** Set the damping ratio
1345
+ /** Set how much the spring is damped, 0 for none and 1 to stop it bouncing
1273
1346
  * @param {number} ratio */
1274
1347
  setDampingRatio(ratio) { this.box2dJoint.SetDampingRatio(ratio); box2dWakeJoint(this.box2dJoint); }
1275
1348
 
1276
- /** Get the damping ratio
1349
+ /** Get how much the spring is damped, 0 for none and 1 to stop it bouncing
1277
1350
  * @return {number} */
1278
1351
  getDampingRatio() { return this.box2dJoint.GetDampingRatio(); }
1279
1352
  }
@@ -1402,6 +1475,7 @@ class Box2dRevoluteJoint extends Box2dJoint
1402
1475
  setLimits(min, max)
1403
1476
  {
1404
1477
  ASSERT(min <= max, 'the lower limit must not be above the upper one');
1478
+ min = box2dFinite(min, 0), max = box2dFinite(max, 0); // or on NaN, which passes the order check
1405
1479
  if (min > max) [min, max] = [max, min]; // Box2D stops on them reversed
1406
1480
  this.box2dJoint.SetLimits(-max, -min);
1407
1481
  }
@@ -1426,7 +1500,7 @@ class Box2dRevoluteJoint extends Box2dJoint
1426
1500
  * @param {number} torque */
1427
1501
  setMaxMotorTorque(torque) { this.box2dJoint.SetMaxMotorTorque(torque); }
1428
1502
 
1429
- /** Get the max motor torque
1503
+ /** Get the most torque the motor can apply, a magnitude
1430
1504
  * @return {number} */
1431
1505
  getMaxMotorTorque() { return this.box2dJoint.GetMaxMotorTorque(); }
1432
1506
 
@@ -1604,6 +1678,7 @@ class Box2dPrismaticJoint extends Box2dJoint
1604
1678
  setLimits(min, max)
1605
1679
  {
1606
1680
  ASSERT(min <= max, 'the lower limit must not be above the upper one');
1681
+ min = box2dFinite(min, 0), max = box2dFinite(max, 0); // or on NaN, which passes the order check
1607
1682
  if (min > max) [min, max] = [max, min]; // Box2D stops on them reversed
1608
1683
  this.box2dJoint.SetLimits(min, max);
1609
1684
  }
@@ -1616,11 +1691,11 @@ class Box2dPrismaticJoint extends Box2dJoint
1616
1691
  * @param {boolean} [enable] */
1617
1692
  enableMotor(enable=true) { this.box2dJoint.EnableMotor(enable); }
1618
1693
 
1619
- /** Set the motor speed
1694
+ /** Set the speed the motor drives the bodies apart along the axis, in meters per second
1620
1695
  * @param {number} speed */
1621
1696
  setMotorSpeed(speed) { this.box2dJoint.SetMotorSpeed(speed); }
1622
1697
 
1623
- /** Get the motor speed
1698
+ /** Get the speed the motor drives the bodies apart along the axis, in meters per second
1624
1699
  * @return {number} */
1625
1700
  getMotorSpeed() { return this.box2dJoint.GetMotorSpeed(); }
1626
1701
 
@@ -1712,7 +1787,7 @@ class Box2dWheelJoint extends Box2dJoint
1712
1787
  * @param {number} torque */
1713
1788
  setMaxMotorTorque(torque) { this.box2dJoint.SetMaxMotorTorque(torque); }
1714
1789
 
1715
- /** Get the max motor torque
1790
+ /** Get the most torque the wheel's motor can apply, a magnitude
1716
1791
  * @return {number} */
1717
1792
  getMaxMotorTorque() { return this.box2dJoint.GetMaxMotorTorque(); }
1718
1793
 
@@ -1729,11 +1804,11 @@ class Box2dWheelJoint extends Box2dJoint
1729
1804
  * @return {number} */
1730
1805
  getSpringFrequencyHz() { return this.box2dJoint.GetSpringFrequencyHz(); }
1731
1806
 
1732
- /** Set the spring damping ratio
1807
+ /** Set how much the suspension spring is damped, 0 for none and 1 to stop it bouncing
1733
1808
  * @param {number} ratio */
1734
1809
  setSpringDampingRatio(ratio) { this.box2dJoint.SetSpringDampingRatio(ratio); box2dWakeJoint(this.box2dJoint); }
1735
1810
 
1736
- /** Get the spring damping ratio
1811
+ /** Get how much the suspension spring is damped, 0 for none and 1 to stop it bouncing
1737
1812
  * @return {number} */
1738
1813
  getSpringDampingRatio() { return this.box2dJoint.GetSpringDampingRatio(); }
1739
1814
  }
@@ -1791,11 +1866,11 @@ class Box2dWeldJoint extends Box2dJoint
1791
1866
  * @return {number} */
1792
1867
  getFrequency() { return this.box2dJoint.GetFrequency(); }
1793
1868
 
1794
- /** Set the damping ratio
1869
+ /** Set how much the weld's spring is damped, 0 for none and 1 to stop it bouncing
1795
1870
  * @param {number} ratio */
1796
1871
  setDampingRatio(ratio) { this.box2dJoint.SetDampingRatio(ratio); box2dWakeJoint(this.box2dJoint); }
1797
1872
 
1798
- /** Get the damping ratio
1873
+ /** Get how much the weld's spring is damped, 0 for none and 1 to stop it bouncing
1799
1874
  * @return {number} */
1800
1875
  getDampingRatio() { return this.box2dJoint.GetDampingRatio(); }
1801
1876
  }
@@ -1839,7 +1914,7 @@ class Box2dFrictionJoint extends Box2dJoint
1839
1914
 
1840
1915
  /** Set the maximum friction force
1841
1916
  * @param {number} force */
1842
- setMaxForce(force) { this.box2dJoint.SetMaxForce(max(force, 0)); } // Box2D stops on a negative one
1917
+ setMaxForce(force) { this.box2dJoint.SetMaxForce(max(box2dFinite(force, 0), 0)); } // Box2D stops on a negative one
1843
1918
 
1844
1919
  /** Get the maximum friction force
1845
1920
  * @return {number} */
@@ -1847,7 +1922,7 @@ class Box2dFrictionJoint extends Box2dJoint
1847
1922
 
1848
1923
  /** Set the maximum friction torque
1849
1924
  * @param {number} torque */
1850
- setMaxTorque(torque) { this.box2dJoint.SetMaxTorque(max(torque, 0)); } // Box2D stops on a negative one
1925
+ setMaxTorque(torque) { this.box2dJoint.SetMaxTorque(max(box2dFinite(torque, 0), 0)); } // Box2D stops on a negative one
1851
1926
 
1852
1927
  /** Get the maximum friction torque
1853
1928
  * @return {number} */
@@ -1888,7 +1963,7 @@ class Box2dPulleyJoint extends Box2dJoint
1888
1963
  jointDef.set_localAnchorA(box2dTemp(localAnchorA));
1889
1964
  jointDef.set_localAnchorB(box2dTemp(localAnchorB));
1890
1965
  ASSERT(ratio, 'a pulley ratio can not be 0');
1891
- jointDef.set_ratio(ratio);
1966
+ jointDef.set_ratio(box2dFinite(ratio, 1) || 1); // Box2D stops for good on 0
1892
1967
  jointDef.set_lengthA(groundAnchorA.distance(anchorA));
1893
1968
  jointDef.set_lengthB(groundAnchorB.distance(anchorB));
1894
1969
  jointDef.set_collideConnected(collide);
@@ -1949,11 +2024,11 @@ class Box2dMotorJoint extends Box2dJoint
1949
2024
  super(jointDef);
1950
2025
  }
1951
2026
 
1952
- /** Set the target linear offset, in frame A, in meters.
2027
+ /** Set the target linear offset, in frame A, in meters
1953
2028
  * @param {Vector2} offset */
1954
2029
  setLinearOffset(offset) { this.box2dJoint.SetLinearOffset(box2dTemp(offset)); }
1955
2030
 
1956
- /** Get the target linear offset, in frame A, in meters.
2031
+ /** Get the target linear offset, in frame A, in meters
1957
2032
  * @return {Vector2} */
1958
2033
  getLinearOffset() { return box2d.vec2From(this.box2dJoint.GetLinearOffset()); }
1959
2034
 
@@ -1967,7 +2042,7 @@ class Box2dMotorJoint extends Box2dJoint
1967
2042
 
1968
2043
  /** Set the maximum force
1969
2044
  * @param {number} force */
1970
- setMaxForce(force) { this.box2dJoint.SetMaxForce(max(force, 0)); box2dWakeJoint(this.box2dJoint); } // Box2D stops on a negative one
2045
+ setMaxForce(force) { this.box2dJoint.SetMaxForce(max(box2dFinite(force, 0), 0)); box2dWakeJoint(this.box2dJoint); } // Box2D stops on a negative one
1971
2046
 
1972
2047
  /** Get the maximum force
1973
2048
  * @return {number} */
@@ -1975,7 +2050,7 @@ class Box2dMotorJoint extends Box2dJoint
1975
2050
 
1976
2051
  /** Set the maximum torque
1977
2052
  * @param {number} torque */
1978
- setMaxTorque(torque) { this.box2dJoint.SetMaxTorque(max(torque, 0)); box2dWakeJoint(this.box2dJoint); } // Box2D stops on a negative one
2053
+ setMaxTorque(torque) { this.box2dJoint.SetMaxTorque(max(box2dFinite(torque, 0), 0)); box2dWakeJoint(this.box2dJoint); } // Box2D stops on a negative one
1979
2054
 
1980
2055
  /** Get the maximum torque
1981
2056
  * @return {number} */
@@ -2329,7 +2404,7 @@ class Box2dPlugin
2329
2404
  * @return {Vector2} */
2330
2405
  vec2From(v)
2331
2406
  {
2332
- ASSERT(v instanceof box2d.instance.b2Vec2);
2407
+ ASSERT(v instanceof box2d.instance.b2Vec2, 'vec2From: expected a b2Vec2', v);
2333
2408
  return new Vector2(v.get_x(), v.get_y());
2334
2409
  }
2335
2410
 
@@ -2348,7 +2423,7 @@ class Box2dPlugin
2348
2423
  * @return {Object} - A Box2D vector, its b2Vec2 */
2349
2424
  vec2dTo(v)
2350
2425
  {
2351
- ASSERT(isVector2(v));
2426
+ ASSERT(isVector2(v), 'vec2dTo: expected a vec2', v);
2352
2427
  return new box2d.instance.b2Vec2(v.x, v.y);
2353
2428
  }
2354
2429
 
@@ -2445,6 +2520,7 @@ async function box2dInit()
2445
2520
  {
2446
2521
  if (o.body)
2447
2522
  {
2523
+ debug && box2dCheckUnusedFields(o);
2448
2524
  // moved in place, as the engine moves an EngineObject's pos, so what holds it follows the body;
2449
2525
  // box2d uses reverse angle
2450
2526
  const p = o.body.GetPosition();
package/plugins/gltf.js CHANGED
@@ -73,6 +73,9 @@ class GLTFPart
73
73
  this.emissiveMap = undefined;
74
74
  /** @property {Color} - The emissiveFactor, which multiplies the emissive map */
75
75
  this.emissiveMapColor = WHITE;
76
+ /** @property {number} - The material's roughnessFactor, 0 a mirror to 1 matte, 1 when it has none as the
77
+ * format says; the object createObject makes takes it as its roughness, which sets its shininess */
78
+ this.roughness = 1;
76
79
  /** @property {GLTFSkin|undefined} - For a skinned mesh, what bends it: mesh is its resting pose, and the
77
80
  * object createObject makes bends a copy of its own to each pose
78
81
  * @type {GLTFSkin|undefined} */
@@ -134,6 +137,8 @@ class GLTFModel
134
137
  /** @property {Array<GLTFAnimation>} - The animations, play one through createObject's GLTFObject
135
138
  * @type {Array<GLTFAnimation>} */
136
139
  this.animations = animations;
140
+ /** @type {{nodes: Array<Object>, parents: Array<number|undefined>, restInverse: Array<Matrix4>,
141
+ * restPose: Array<Matrix4|undefined>}|undefined} */
137
142
  this.nodeTree = nodeTree; // each node's parent and resting place, for animation
138
143
  this.modelMatrix = new Matrix4; // what center, fit and transform did to the parts, animation works through it
139
144
  /** @property {Mesh} - Every part combined, each tinted with its material color; the texture is textureInfo,
@@ -303,6 +308,7 @@ class GLTFObject extends EngineObject3D
303
308
  o.emissive = part.unlit ? 1 : 0;
304
309
  o.normalMap = part.normalMap, o.normalScale = part.normalScale;
305
310
  o.emissiveMap = part.emissiveMap, o.emissiveMapColor = part.emissiveMapColor.copy(); // its own, as color is
311
+ o.roughness = part.roughness;
306
312
  const rest = !part.skin && model.nodeTree?.restPose?.[part.node];
307
313
  if (rest)
308
314
  {
@@ -313,6 +319,7 @@ class GLTFObject extends EngineObject3D
313
319
  o.rotation3D = m.getRotation();
314
320
  o.scale3D = m.getScale();
315
321
  }
322
+ render3DShareSettings(o, this); // what is set on the model, the part's own material otherwise
316
323
  this.addChild(o);
317
324
  this.parts.push(o);
318
325
  }
@@ -450,9 +457,9 @@ class GLTFObject extends EngineObject3D
450
457
  * @memberof GLTF */
451
458
  async function loadGLTF(url)
452
459
  {
453
- const response = await fetch(url);
460
+ const response = await loadFetch(url, 'loadGLTF');
454
461
  if (!response.ok)
455
- throw new Error('loadGLTF failed: ' + url);
462
+ throw new Error(`loadGLTF: could not load ${url}, ${response.status} ${response.statusText}`);
456
463
  // the files beside it are beside where it came from, after any redirect; a blob or data url has nothing
457
464
  // beside it, and a model in one is whole, or parsed with parseGLTF and a base of its own
458
465
  const from = response.url || url;
@@ -477,7 +484,7 @@ async function loadGLTF(url)
477
484
  * is fetched then
478
485
  * - A file the model needs that is not found is named in the error, and an image that can not be read is named in
479
486
  * a warning and left out
480
- * @param {ArrayBuffer|Object|string} data - GLB bytes, or the glTF JSON as bytes, text or an object
487
+ * @param {ArrayBuffer|ArrayBufferView|Object<string, any>|string} data - GLB bytes, or the glTF JSON as bytes, text or an object
481
488
  * @param {string} [baseUrl] - Where the .bin and image files are, with its trailing slash; loadGLTF passes the file's folder
482
489
  * @param {Map<string, Blob>} [files] - The files it refers to, by their paths, in place of fetching them
483
490
  * @return {Promise<GLTFModel>}
@@ -489,7 +496,10 @@ async function loadGLTF(url)
489
496
  * @memberof GLTF */
490
497
  async function parseGLTF(data, baseUrl='', files)
491
498
  {
492
- let json = data, glbBuffer;
499
+ // bytes as a typed array, a Node Buffer or a view into a bigger buffer, read as an ArrayBuffer of just them
500
+ if (ArrayBuffer.isView(data))
501
+ data = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
502
+ let json = /** @type {any} */ (data), glbBuffer; // bytes, text or the parsed glTF, an object once parsed
493
503
  if (data instanceof ArrayBuffer)
494
504
  {
495
505
  const view = new DataView(data);
@@ -784,39 +794,67 @@ function gltfSkinMeshCopy(mesh)
784
794
 
785
795
  // bend a skinned mesh to a pose: each vertex its stored place moved by its four joints, weighted, and its normal by
786
796
  // their normal matrices, each joint's matrix modelMatrix * its place now * its inverse bind; written into the
787
- // mesh's own vectors, so a pose makes nothing for each vertex
797
+ // mesh's own vectors, so a pose makes nothing for each vertex, and once an upload has laid the mesh out for the GPU
798
+ // into that data too, with the radius and box the upload would measure, so it sends the data as it is
788
799
  function gltfSkinApply(skin, worldOf, modelMatrix, mesh)
789
800
  {
790
- const matrices = [], normalMatrices = [];
791
- for (let j = 0; j < skin.joints.length; ++j)
801
+ // each joint's point matrix, 12 numbers, then its normal matrix, 9, in one array the skin keeps
802
+ const jointCount = skin.joints.length, M = skin.jointMatrices ||= new Float32Array(jointCount * 21);
803
+ const m = gltfSkinScratch[0], n = gltfSkinScratch[1];
804
+ for (let j = 0; j < jointCount; ++j)
792
805
  {
793
- const m = modelMatrix.copy().multiply(worldOf(skin.joints[j])).multiply(skin.inverseBind[j]);
794
- matrices.push(m.m), normalMatrices.push(render3DNormalMatrix(m).m);
806
+ m.m.set(modelMatrix.m);
807
+ m.multiply(worldOf(skin.joints[j])).multiply(skin.inverseBind[j]);
808
+ n.m.set(m.m);
809
+ n.invert().transpose();
810
+ const a = m.m, b = n.m, o = j * 21;
811
+ M[o] = a[0], M[o+1] = a[1], M[o+2] = a[2], M[o+3] = a[4], M[o+4] = a[5], M[o+5] = a[6];
812
+ M[o+6] = a[8], M[o+7] = a[9], M[o+8] = a[10], M[o+9] = a[12], M[o+10] = a[13], M[o+11] = a[14];
813
+ M[o+12] = b[0], M[o+13] = b[1], M[o+14] = b[2], M[o+15] = b[4], M[o+16] = b[5], M[o+17] = b[6];
814
+ M[o+18] = b[8], M[o+19] = b[9], M[o+20] = b[10];
795
815
  }
796
816
  const {vertexJoints, vertexWeights, bindPoints, bindNormals} = skin, points = mesh.points, normals = mesh.normals;
797
- for (let v = 0; v < points.length; ++v)
817
+ // an indexed mesh's GPU vertex j is its point j, as upload lays it out
818
+ const layout = mesh.vertexLayout, count = points.length;
819
+ const floats = layout && mesh.indices && layout.pointCount === count ? new Float32Array(layout.data) : undefined;
820
+ let r = 0, x0 = Infinity, y0 = Infinity, z0 = Infinity, x1 = -Infinity, y1 = -Infinity, z1 = -Infinity;
821
+ for (let v = 0; v < count; ++v)
798
822
  {
799
- const px = bindPoints[v*3], py = bindPoints[v*3+1], pz = bindPoints[v*3+2];
800
- const qx = bindNormals[v*3], qy = bindNormals[v*3+1], qz = bindNormals[v*3+2];
823
+ const v3 = v*3, v4 = v*4, px = bindPoints[v3], py = bindPoints[v3+1], pz = bindPoints[v3+2];
824
+ const qx = bindNormals[v3], qy = bindNormals[v3+1], qz = bindNormals[v3+2];
801
825
  let x = 0, y = 0, z = 0, nx = 0, ny = 0, nz = 0;
802
826
  for (let k = 0; k < 4; ++k)
803
827
  {
804
- const w = vertexWeights[v*4+k];
828
+ const w = vertexWeights[v4+k];
805
829
  if (!w) continue;
806
- const m = matrices[vertexJoints[v*4+k]], n = normalMatrices[vertexJoints[v*4+k]];
807
- x += w * (m[0]*px + m[4]*py + m[8]*pz + m[12]);
808
- y += w * (m[1]*px + m[5]*py + m[9]*pz + m[13]);
809
- z += w * (m[2]*px + m[6]*py + m[10]*pz + m[14]);
810
- nx += w * (n[0]*qx + n[4]*qy + n[8]*qz);
811
- ny += w * (n[1]*qx + n[5]*qy + n[9]*qz);
812
- nz += w * (n[2]*qx + n[6]*qy + n[10]*qz);
830
+ const o = vertexJoints[v4+k] * 21;
831
+ x += w * (M[o]*px + M[o+3]*py + M[o+6]*pz + M[o+9]);
832
+ y += w * (M[o+1]*px + M[o+4]*py + M[o+7]*pz + M[o+10]);
833
+ z += w * (M[o+2]*px + M[o+5]*py + M[o+8]*pz + M[o+11]);
834
+ nx += w * (M[o+12]*qx + M[o+15]*qy + M[o+18]*qz);
835
+ ny += w * (M[o+13]*qx + M[o+16]*qy + M[o+19]*qz);
836
+ nz += w * (M[o+14]*qx + M[o+17]*qy + M[o+20]*qz);
813
837
  }
814
838
  const p = points[v], normal = normals[v], l = (nx*nx + ny*ny + nz*nz) ** .5 || 1;
815
839
  p.x = x, p.y = y, p.z = z;
816
840
  normal.x = nx / l, normal.y = ny / l, normal.z = nz / l;
841
+ if (!floats) continue;
842
+ const at = v * RENDER3D_VERTEX_FLOATS;
843
+ floats[at] = x, floats[at+1] = y, floats[at+2] = z;
844
+ floats[at+3] = normal.x, floats[at+4] = normal.y, floats[at+5] = normal.z;
845
+ r = max(r, x*x + y*y + z*z);
846
+ x0 = min(x0, x), y0 = min(y0, y), z0 = min(z0, z);
847
+ x1 = max(x1, x), y1 = max(y1, y), z1 = max(z1, z);
848
+ }
849
+ if (floats)
850
+ {
851
+ mesh.radius = r ** .5;
852
+ mesh.bounds = count ? {min: vec3(x0, y0, z0), max: vec3(x1, y1, z1)} : {min: vec3(), max: vec3()};
853
+ mesh.vertexDataPacked = true;
817
854
  }
818
855
  mesh.dirty = true;
819
856
  }
857
+ const gltfSkinScratch = [new Matrix4, new Matrix4]; // a joint's matrix and its normal matrix, as it is worked out
820
858
 
821
859
  // a node's translation, rotation and scale as arrays to animate, copies so the file's stay as they rest
822
860
  function gltfNodeTRS(node)
@@ -977,7 +1015,7 @@ function gltfFetch(uri, baseUrl, files)
977
1015
  const page = typeof location !== 'undefined' && location.href;
978
1016
  const base = page ? new URL(baseUrl, page) : /^[a-z][a-z0-9+.-]*:/i.test(baseUrl) ? baseUrl : undefined;
979
1017
  const url = base ? new URL(uri, base).href : baseUrl + uri;
980
- return fetch(url).then(r=>
1018
+ return loadFetch(url, 'glTF').then(r=>
981
1019
  {
982
1020
  if (!r.ok) throw new Error('glTF needs ' + uri + ', not found at ' + url);
983
1021
  return r;
@@ -1022,6 +1060,9 @@ function gltfAccessor(json, buffers, index)
1022
1060
  if (view)
1023
1061
  {
1024
1062
  const buffer = buffers[view.buffer], offset = (view.byteOffset || 0) + (a.byteOffset || 0);
1063
+ // the format keeps values on their size, a typed array can not read them off it
1064
+ if (offset % size || stride % size)
1065
+ throw new Error('glTF accessor ' + index + ' is not aligned to its ' + size + ' byte values');
1025
1066
  if (stride === components * size)
1026
1067
  out.set(new Type(buffer, offset, a.count * components)); // packed, one view over all of it
1027
1068
  else
@@ -1036,6 +1077,13 @@ function gltfAccessor(json, buffers, index)
1036
1077
  const indexView = json.bufferViews?.[indices.bufferView], valueView = json.bufferViews?.[values.bufferView];
1037
1078
  if (!IndexType || !indexView || !valueView)
1038
1079
  throw new Error('glTF sparse accessor is missing its indices or values');
1080
+ // its indices and values in their buffers and on their sizes, before typed arrays are made over them
1081
+ const indexAt = (indexView.byteOffset || 0) + (indices.byteOffset || 0);
1082
+ const valueAt = (valueView.byteOffset || 0) + (values.byteOffset || 0);
1083
+ if (indexAt % IndexType.BYTES_PER_ELEMENT || valueAt % size ||
1084
+ !(indexAt + sparse.count * IndexType.BYTES_PER_ELEMENT <= (buffers[indexView.buffer]?.byteLength ?? 0)) ||
1085
+ !(valueAt + sparse.count * components * size <= (buffers[valueView.buffer]?.byteLength ?? 0)))
1086
+ throw new Error('glTF sparse accessor ' + index + ' is not aligned or reaches past its buffer');
1039
1087
  const at = new IndexType(buffers[indexView.buffer], (indexView.byteOffset || 0) + (indices.byteOffset || 0), sparse.count);
1040
1088
  const data = new Type(buffers[valueView.buffer], (valueView.byteOffset || 0) + (values.byteOffset || 0), sparse.count * components);
1041
1089
  for (let i = 0; i < sparse.count; ++i)
@@ -1067,7 +1115,10 @@ function gltfPart(json, buffers, textures, primitive, matrix, name)
1067
1115
  };
1068
1116
  const points = read(attributes.POSITION, (d, k)=> vec3(d[k], d[k+1], d[k+2]));
1069
1117
  const normals = attributes.NORMAL !== undefined ? read(attributes.NORMAL, (d, k)=> vec3(d[k], d[k+1], d[k+2])) : undefined;
1070
- // the uv set the base color texture names, moved by its KHR_texture_transform once here, offset + rotation * scale
1118
+ // the uv set the base color texture names, moved by its KHR_texture_transform once here, offset + rotation * scale,
1119
+ // the rotation turning (u, v) to (cos u + sin v, cos v - sin u), counter-clockwise in glTF's uv space with v down,
1120
+ // as the extension's text says and as Khronos's sample renderer and three.js turn it, which models are made to
1121
+ // look right in; the GLSL sample in the extension's readme, read column major, turns the other way
1071
1122
  const material = json.materials?.[primitive.material] || {}, pbr = material.pbrMetallicRoughness || {};
1072
1123
  const textureRef = pbr.baseColorTexture, uvTransform = textureRef?.extensions?.KHR_texture_transform;
1073
1124
  const uvAccessor = attributes['TEXCOORD_' + (uvTransform?.texCoord ?? textureRef?.texCoord ?? 0)];
@@ -1108,6 +1159,7 @@ function gltfPart(json, buffers, textures, primitive, matrix, name)
1108
1159
  const normalRef = material.normalTexture, emissiveRef = material.emissiveTexture;
1109
1160
  part.normalMap = normalRef && textures[normalRef.index];
1110
1161
  part.normalScale = normalRef?.scale ?? 1;
1162
+ part.roughness = pbr.roughnessFactor ?? 1; // a roughness map is not read
1111
1163
  const [er, eg, eb] = material.emissiveFactor || [0, 0, 0];
1112
1164
  if (er || eg || eb)
1113
1165
  {