littlejsengine 1.24.3 → 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 (53) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/EDITOR.md +2 -2
  3. package/FAQ.md +55 -11
  4. package/README.md +6 -2
  5. package/REFERENCE.md +203 -57
  6. package/dist/littlejs.d.ts +933 -313
  7. package/dist/littlejs.esm.js +5143 -2419
  8. package/dist/littlejs.esm.min.js +38 -24
  9. package/dist/littlejs.js +5132 -2416
  10. package/dist/littlejs.min.js +37 -23
  11. package/dist/littlejs.release.js +4905 -2232
  12. package/package.json +4 -4
  13. package/plugins/box2d.js +141 -56
  14. package/plugins/drawUtilities.js +374 -380
  15. package/plugins/gltf.js +383 -60
  16. package/plugins/lightSystem.js +412 -30
  17. package/plugins/math3d.js +1015 -1014
  18. package/plugins/medalSystem.js +19 -7
  19. package/plugins/newgrounds.js +5 -6
  20. package/plugins/parallax.js +1 -0
  21. package/plugins/particleEffects.js +47 -14
  22. package/plugins/pathFinder.js +60 -38
  23. package/plugins/pluginExport.js +11 -0
  24. package/plugins/postProcess.js +200 -67
  25. package/plugins/render3d.js +493 -2391
  26. package/plugins/render3dDebug.js +5 -8
  27. package/plugins/render3dEditor.js +84 -9
  28. package/plugins/render3dExtras.js +122 -33
  29. package/plugins/render3dLevel.js +67 -8
  30. package/plugins/render3dMesh.js +1158 -0
  31. package/plugins/render3dObject.js +1451 -0
  32. package/plugins/render3dVoxels.js +15 -3
  33. package/plugins/textureSheet.js +9 -8
  34. package/plugins/threejs.js +1 -1
  35. package/plugins/tweenSystem.js +117 -87
  36. package/plugins/uiSystem.js +62 -28
  37. package/plugins/wavedash.js +229 -0
  38. package/src/engine.js +192 -40
  39. package/src/engineAudio.js +47 -27
  40. package/src/engineBuild.mjs +6 -1
  41. package/src/engineDebug.js +23 -162
  42. package/src/engineDraw.js +148 -43
  43. package/src/engineEditor.js +153 -40
  44. package/src/engineExport.js +0 -3
  45. package/src/engineInput.js +31 -25
  46. package/src/engineMath.js +38 -20
  47. package/src/engineObject.js +68 -50
  48. package/src/engineParticles.js +66 -18
  49. package/src/engineRelease.js +18 -15
  50. package/src/engineSettings.js +48 -9
  51. package/src/engineTileLayer.js +44 -18
  52. package/src/engineUtilities.js +56 -11
  53. package/src/engineWebGL.js +68 -22
package/plugins/gltf.js CHANGED
@@ -3,9 +3,11 @@
3
3
  * - Loads glTF 2.0 models: a .gltf with its .bin and images beside it, or a .glb with everything in one file
4
4
  * - A model comes back as parts, one Mesh per primitive of every node placed by the node tree, each with its
5
5
  * material's color and base color texture, plus everything combined into one Mesh
6
- * - Geometry: positions, normals, uvs, vertex colors and indices; skins and morph targets are not read
7
- * - Node animations play: parts that move, turn and scale, like doors, wheels and propellers, through the
8
- * GLTFObject that createObject makes; a skinned character's walk is not read
6
+ * - Geometry: positions, normals, uvs, vertex colors and indices, and skins, the four strongest joints of a vertex
7
+ * of up to eight; morph targets are not read
8
+ * - Animations play through the GLTFObject that createObject makes: parts that move, turn and scale, like doors,
9
+ * wheels and propellers, and skinned characters, their meshes bent by their joints each frame; play takes a
10
+ * blend time to cross-fade from one animation to the next
9
11
  * - Materials give a base color and texture and whether they blend; glass made with KHR_materials_transmission blends too,
10
12
  * and a KHR_materials_unlit material comes in emissive, its own color with no shading
11
13
  * - A material's normal map and emissive map load too, with its normal scale and emissive factor, read at the base
@@ -23,6 +25,7 @@
23
25
  * const model = await loadGLTF('ship.glb'); // in an async gameInit
24
26
  * const ship = model.createObject(vec3(0, 1, 0)); // an object with a child per part, textures and all
25
27
  * ship.play('fly'); // and its animation, by name or number
28
+ * ship.play('land', false, 1, .3); // then another, cross-faded over .3 seconds
26
29
  * new EngineObject3D(vec3(), model.mesh); // or the whole thing as one mesh, still
27
30
  */
28
31
 
@@ -35,7 +38,8 @@
35
38
  */
36
39
  class GLTFPart
37
40
  {
38
- /** @param {string} name @param {Mesh} mesh @param {Color} color @param {TextureInfo|undefined} textureInfo @param {boolean} transparent */
41
+ /** Make a part, as the loader does for each primitive
42
+ * @param {string} name @param {Mesh} mesh @param {Color} color @param {TextureInfo|undefined} textureInfo @param {boolean} transparent */
39
43
  constructor(name, mesh, color, textureInfo, transparent)
40
44
  {
41
45
  /** @property {string} - The node's name, or its mesh's */
@@ -69,9 +73,32 @@ class GLTFPart
69
73
  this.emissiveMap = undefined;
70
74
  /** @property {Color} - The emissiveFactor, which multiplies the emissive map */
71
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;
79
+ /** @property {GLTFSkin|undefined} - For a skinned mesh, what bends it: mesh is its resting pose, and the
80
+ * object createObject makes bends a copy of its own to each pose
81
+ * @type {GLTFSkin|undefined} */
82
+ this.skin = undefined;
83
+ // which of the file's vertices each of the mesh's is, when flat normals split them, for a skin
84
+ /** @type {Array<number>|undefined} */
85
+ this.vertexSource = undefined;
72
86
  }
73
87
  }
74
88
 
89
+ /**
90
+ * What bends a skinned part: its joints, the nodes that move it, their inverse bind matrices, and for each vertex
91
+ * of its mesh four joints and four weights, with the place and normal each pose bends from
92
+ * @typedef {Object} GLTFSkin
93
+ * @property {Array<number>} joints - The joints, as node numbers
94
+ * @property {Array<Matrix4>} inverseBind - Each joint's inverse bind matrix
95
+ * @property {Uint16Array} vertexJoints - Four joints a vertex, as places in joints
96
+ * @property {Float32Array} vertexWeights - Four weights a vertex, summing to 1
97
+ * @property {Float32Array} bindPoints - Each vertex's place as stored, x y z
98
+ * @property {Float32Array} bindNormals - Each vertex's normal as stored, x y z
99
+ * @memberof GLTF
100
+ */
101
+
75
102
  /**
76
103
  * GLTFAnimation - One animation of a model: keys that move, turn and scale its nodes over time
77
104
  * - Play it through the GLTFObject that model.createObject makes
@@ -79,7 +106,8 @@ class GLTFPart
79
106
  */
80
107
  class GLTFAnimation
81
108
  {
82
- /** @param {string} name @param {Array<Object>} channels */
109
+ /** Make an animation from its channels, as the loader does
110
+ * @param {string} name @param {Array<Object>} channels */
83
111
  constructor(name, channels)
84
112
  {
85
113
  /** @property {string} - Its name in the file, or 'animation' and its number when it has none */
@@ -100,7 +128,8 @@ class GLTFAnimation
100
128
  */
101
129
  class GLTFModel
102
130
  {
103
- /** @param {Array<GLTFPart>} parts @param {Array<GLTFAnimation>} [animations] @param {Object} [nodeTree] */
131
+ /** Make a model from its parts, as the loader does
132
+ * @param {Array<GLTFPart>} parts @param {Array<GLTFAnimation>} [animations] @param {Object} [nodeTree] */
104
133
  constructor(parts, animations=[], nodeTree)
105
134
  {
106
135
  /** @property {Array<GLTFPart>} - One per primitive of every node that has a mesh */
@@ -108,13 +137,15 @@ class GLTFModel
108
137
  /** @property {Array<GLTFAnimation>} - The animations, play one through createObject's GLTFObject
109
138
  * @type {Array<GLTFAnimation>} */
110
139
  this.animations = animations;
140
+ /** @type {{nodes: Array<Object>, parents: Array<number|undefined>, restInverse: Array<Matrix4>,
141
+ * restPose: Array<Matrix4|undefined>}|undefined} */
111
142
  this.nodeTree = nodeTree; // each node's parent and resting place, for animation
112
143
  this.modelMatrix = new Matrix4; // what center, fit and transform did to the parts, animation works through it
113
144
  /** @property {Mesh} - Every part combined, each tinted with its material color; the texture is textureInfo,
114
145
  * and blending and unlit stay with the parts, which createObject draws */
115
146
  this.mesh = new Mesh;
116
147
  for (const part of parts) // a part baked at scale 1 from a node resting at 0 goes in as it rests
117
- this.mesh.combine(part.mesh, nodeTree?.restPose?.[part.node] || RENDER3D_IDENTITY, part.color);
148
+ this.mesh.combine(part.mesh, !part.skin && nodeTree?.restPose?.[part.node] || RENDER3D_IDENTITY, part.color);
118
149
  // the one texture every part uses, when they all do; a part without one has no uvs into it, so a model
119
150
  // that mixes plain and textured parts, or uses several textures, is drawn through createObject instead
120
151
  const textures = new Set(parts.map(p=> p.textureInfo));
@@ -180,29 +211,18 @@ class GLTFModel
180
211
  * @return {Array<Matrix4>} */
181
212
  getPose(animation, time)
182
213
  {
183
- const tree = this.nodeTree;
184
- if (!tree) return this.parts.map(()=> new Matrix4);
185
-
186
- // the nodes the animation moves get its values at this time, every other node keeps its own
187
- const moved = new Map;
188
- for (const channel of animation.channels)
189
- {
190
- let node = moved.get(channel.node);
191
- node || moved.set(channel.node, node = gltfNodeTRS(tree.nodes[channel.node]));
192
- gltfSample(channel, time, node[channel.path]);
193
- }
214
+ if (!this.nodeTree) return this.parts.map(()=> new Matrix4);
215
+ return this.partPoses(gltfNodeWorlds(this.nodeTree, gltfPoseNodes(this.nodeTree, animation, time)));
216
+ }
194
217
 
195
- // then each node's place in the model through its parents, and each part's move from where it rests,
196
- // through what center and fit did: modelMatrix * now * rest inverse * modelMatrix inverse
197
- const world = [];
198
- const worldOf = (i)=>
199
- {
200
- if (world[i]) return world[i];
201
- const local = gltfNodeMatrix(moved.get(i) || tree.nodes[i]), parent = tree.parents[i];
202
- return world[i] = parent === undefined ? local : worldOf(parent).copy().multiply(local);
203
- };
204
- const model = this.modelMatrix, modelInverse = model.copy().invert();
205
- return this.parts.map(part=> model.copy().multiply(worldOf(part.node)).multiply(tree.restInverse[part.node]).multiply(modelInverse));
218
+ // each part's move from where it rests, with each node's place in the model from worldOf, through what center
219
+ // and fit did: modelMatrix * now * rest inverse * modelMatrix inverse; a skinned part is posed by its joints
220
+ // and stays where it is
221
+ partPoses(worldOf)
222
+ {
223
+ const tree = this.nodeTree, model = this.modelMatrix, modelInverse = model.copy().invert();
224
+ return this.parts.map(part=> part.skin ? new Matrix4 :
225
+ model.copy().multiply(worldOf(part.node)).multiply(tree.restInverse[part.node]).multiply(modelInverse));
206
226
  }
207
227
 
208
228
  /** Free the GPU buffers of every part's mesh and of the combined mesh, and the textures, for a model that is
@@ -263,19 +283,33 @@ class GLTFObject extends EngineObject3D
263
283
  this.animationLoop = true;
264
284
  /** @property {boolean} - Whether it is moving through the animation now */
265
285
  this.animationPlaying = false;
286
+ // what a cross-fade comes from: an animation going on as it was, or a pose held, and how far it is
287
+ /** @type {{animation?: GLTFAnimation, time?: number, speed?: number, loop?: boolean, nodes?: Map<number, Object>}|undefined} */
288
+ this.blendFrom = undefined;
289
+ this.blendTime = 0;
290
+ this.blendElapsed = 0;
291
+ // each node's values and place in the model as last posed, for a fade from here and for getJointMatrix, and
292
+ // whether that pose was a mix, as a fade stopped part way holds it
293
+ /** @type {Map<number, Object>|undefined} */
294
+ this.poseNodes = undefined;
295
+ this.poseMixed = false;
296
+ /** @type {(function(number): Matrix4)|undefined} */
297
+ this.poseWorldOf = undefined;
266
298
  /** @property {Array<EngineObject3D>} - The child that draws each of the model's parts, in the order of
267
299
  * model.parts, which an animation poses; one destroyed or taken off the object is left alone
268
300
  * @type {Array<EngineObject3D>} */
269
301
  this.parts = [];
270
302
  for (const part of model.parts)
271
303
  {
272
- const o = new EngineObject3D(vec3(), part.mesh, part.textureInfo, part.color);
304
+ // a skinned part bends a mesh of its own, so objects of one model each hold their own pose
305
+ const o = new EngineObject3D(vec3(), part.skin ? gltfSkinMeshCopy(part.mesh) : part.mesh, part.textureInfo, part.color);
273
306
  o.transparent = part.transparent;
274
307
  o.pixelated = part.pixelated;
275
308
  o.emissive = part.unlit ? 1 : 0;
276
309
  o.normalMap = part.normalMap, o.normalScale = part.normalScale;
277
310
  o.emissiveMap = part.emissiveMap, o.emissiveMapColor = part.emissiveMapColor.copy(); // its own, as color is
278
- const rest = model.nodeTree?.restPose?.[part.node];
311
+ o.roughness = part.roughness;
312
+ const rest = !part.skin && model.nodeTree?.restPose?.[part.node];
279
313
  if (rest)
280
314
  {
281
315
  // baked at scale 1 from a node resting at 0, it starts as it rests, where a pose would put it
@@ -285,20 +319,50 @@ class GLTFObject extends EngineObject3D
285
319
  o.rotation3D = m.getRotation();
286
320
  o.scale3D = m.getScale();
287
321
  }
322
+ render3DShareSettings(o, this); // what is set on the model, the part's own material otherwise
288
323
  this.addChild(o);
289
324
  this.parts.push(o);
290
325
  }
291
326
  }
292
327
 
293
- /** Play an animation from its start
328
+ /** Destroy the object and its parts, and free the meshes its skinned parts bend, which are its own
329
+ * @param {boolean} [immediate] */
330
+ destroy(immediate)
331
+ {
332
+ if (this.destroyed) return;
333
+ this.parts.forEach((o, i)=> this.model.parts[i]?.skin && o.mesh?.dispose());
334
+ super.destroy(immediate);
335
+ }
336
+
337
+ /** Play an animation from its start, at once or cross-faded from the pose it is in
338
+ * - A play with a blend of the animation already playing goes on with it, so state code may call it each
339
+ * frame; without a blend it starts the animation again
294
340
  * @param {string|number|GLTFAnimation} [animation] - Its name, its number in model.animations, or itself
295
341
  * @param {boolean} [loop] - Start again at the end, or stop there
296
- * @param {number} [speed] - 1 is as made, negative plays it backward from its end */
297
- play(animation=0, loop=true, speed=1)
342
+ * @param {number} [speed] - 1 is as made, negative plays it backward from its end
343
+ * @param {number} [blend] - Seconds to cross-fade from the pose it is in, 0 to switch at once; the animation
344
+ * it comes from goes on through the fade, and a fade started during another fades from the mix there */
345
+ play(animation=0, loop=true, speed=1, blend=0)
298
346
  {
299
347
  const found = this.model.getAnimation(animation);
300
348
  ASSERT(found, 'the model has no animation ' + animation, this.model.animations.map(a=> a.name));
301
349
  if (!found) return;
350
+ if (blend > 0 && found === this.animation && this.animationPlaying)
351
+ {
352
+ this.animationLoop = loop, this.animationSpeed = speed;
353
+ return;
354
+ }
355
+ const fading = this.blendFrom;
356
+ this.blendFrom = undefined;
357
+ if (blend > 0)
358
+ {
359
+ // from the animation playing, going on as it was, or from the pose held, a fade's mix included
360
+ const from = this.animation && !fading && !this.poseMixed ? {animation: this.animation, time: this.animationTime,
361
+ speed: this.animationPlaying ? this.animationSpeed : 0, loop: this.animationLoop} : undefined;
362
+ this.blendFrom = from || {nodes: this.poseNodes || new Map};
363
+ this.blendTime = blend;
364
+ this.blendElapsed = 0;
365
+ }
302
366
  this.animation = found;
303
367
  this.animationLoop = loop;
304
368
  this.animationSpeed = speed;
@@ -306,21 +370,39 @@ class GLTFObject extends EngineObject3D
306
370
  this.setAnimationTime(speed < 0 ? found.duration : 0);
307
371
  }
308
372
 
309
- /** Stop the animation where it is, the parts hold that pose */
310
- stop() { this.animationPlaying = false; }
373
+ /** Stop the animation where it is, the parts hold that pose; a cross-fade going on stops too, holding the mix */
374
+ stop() { this.animationPlaying = false; this.blendFrom = undefined; }
311
375
 
312
376
  /** Put the parts where the animation has them at a time, playing or not
313
377
  * @param {number} time - Seconds into the animation */
314
378
  setAnimationTime(time)
315
379
  {
316
380
  this.animationTime = time;
317
- if (!this.animation) return;
381
+ const model = this.model, tree = model.nodeTree;
382
+ if (!this.animation || !tree) return;
383
+
384
+ // each node's values now, mixed with what a fade comes from by how far it is, eased in and out
385
+ let nodes = gltfPoseNodes(tree, this.animation, time);
386
+ const from = this.blendFrom;
387
+ if (from)
388
+ {
389
+ const fromNodes = from.nodes || gltfPoseNodes(tree, from.animation, from.time);
390
+ nodes = gltfPoseBlend(tree, fromNodes, nodes, smoothStep(clamp(this.blendElapsed / this.blendTime)));
391
+ }
392
+ const worldOf = gltfNodeWorlds(tree, nodes);
393
+ this.poseNodes = nodes, this.poseWorldOf = worldOf, this.poseMixed = !!from;
394
+
318
395
  // its own list of the part objects, so a child removed or added does not hand a part another's pose
319
- const pose = this.model.getPose(this.animation, time), parts = this.parts;
396
+ const pose = model.partPoses(worldOf), parts = this.parts;
320
397
  for (let i = 0; i < pose.length && i < parts.length; ++i)
321
398
  {
322
- const o = parts[i], m = pose[i];
399
+ const o = parts[i], m = pose[i], skin = model.parts[i].skin;
323
400
  if (o.destroyed || o.parent !== this) continue;
401
+ if (skin)
402
+ {
403
+ gltfSkinApply(skin, worldOf, model.modelMatrix, o.mesh); // bent by its joints, where it is
404
+ continue;
405
+ }
324
406
  // drawn with the whole pose, which a parent's uneven scale can shear, the parts kept for what reads them
325
407
  o.localMatrix = m;
326
408
  o.pos3D = m.getTranslation();
@@ -329,23 +411,40 @@ class GLTFObject extends EngineObject3D
329
411
  }
330
412
  }
331
413
 
332
- /** Move through the animation, called automatically each frame */
414
+ /** Move through the animation, and a cross-fade into it, called automatically each frame */
333
415
  update()
334
416
  {
335
417
  super.update();
336
- const animation = this.animation;
337
- if (!this.animationPlaying || !animation) return;
338
- const duration = animation.duration;
339
- let t = this.animationTime + timeDelta * this.animationSpeed;
340
- if (this.animationLoop)
341
- t = duration ? mod(t, duration) : 0;
342
- else if (t >= duration || t <= 0)
418
+ const animation = this.animation, from = this.blendFrom;
419
+ if (!animation || !this.animationPlaying && !from) return;
420
+ let t = this.animationTime;
421
+ if (this.animationPlaying)
343
422
  {
344
- // the end, or the start when playing backward: hold the last pose there
345
- t = clamp(t, 0, duration);
346
- this.animationPlaying = false;
423
+ t = gltfAnimationStep(animation, t, this.animationSpeed, this.animationLoop);
424
+ if (!this.animationLoop && (t >= animation.duration || t <= 0))
425
+ this.animationPlaying = false; // the end, or the start when playing backward: hold the last pose there
426
+ }
427
+ if (from)
428
+ {
429
+ this.blendElapsed += timeDelta;
430
+ if (from.animation) // what it fades from goes on as it was
431
+ from.time = gltfAnimationStep(from.animation, from.time, from.speed, from.loop);
347
432
  }
348
433
  this.setAnimationTime(t);
434
+ if (from && this.blendElapsed >= this.blendTime)
435
+ this.blendFrom = undefined; // faded all the way, the pose just set is the animation's own
436
+ }
437
+
438
+ /** A node's matrix in the world as the model is posed now, by its name in the file, to hang something on a
439
+ * joint, a sword on a hand or a hat on a head; undefined when the model has no node of that name
440
+ * @param {string} name
441
+ * @return {Matrix4|undefined} */
442
+ getJointMatrix(name)
443
+ {
444
+ const tree = this.model.nodeTree, index = tree?.nodes?.findIndex(node=> node.name === name) ?? -1;
445
+ if (index < 0) return;
446
+ const worldOf = this.poseWorldOf || gltfNodeWorlds(tree, new Map);
447
+ return this.getMatrix().multiply(this.model.modelMatrix).multiply(worldOf(index));
349
448
  }
350
449
  }
351
450
 
@@ -358,16 +457,23 @@ class GLTFObject extends EngineObject3D
358
457
  * @memberof GLTF */
359
458
  async function loadGLTF(url)
360
459
  {
361
- const response = await fetch(url);
460
+ const response = await loadFetch(url, 'loadGLTF');
362
461
  if (!response.ok)
363
- throw new Error('loadGLTF failed: ' + url);
462
+ throw new Error(`loadGLTF: could not load ${url}, ${response.status} ${response.statusText}`);
364
463
  // the files beside it are beside where it came from, after any redirect; a blob or data url has nothing
365
464
  // beside it, and a model in one is whole, or parsed with parseGLTF and a base of its own
366
465
  const from = response.url || url;
367
466
  let base = '';
368
467
  if (!/^(blob|data):/i.test(from))
369
468
  base = response.url ? new URL('.', from).href : from.slice(0, from.lastIndexOf('/') + 1);
370
- return parseGLTF(await response.arrayBuffer(), base);
469
+ const data = await response.arrayBuffer();
470
+ try { return await parseGLTF(data, base); }
471
+ catch (e)
472
+ {
473
+ // which file, and a web page, as a dev server sends for a mistyped path, said as one
474
+ const page = loadIsWebPage(String.fromCharCode(...new Uint8Array(data, 0, min(data.byteLength, 64))));
475
+ throw new Error('loadGLTF ' + url + (page ? ' is a web page, so the path may be wrong' : ': ' + e.message));
476
+ }
371
477
  }
372
478
 
373
479
  /** Parse a model from GLB bytes or glTF JSON, fetching the buffers and images it refers to
@@ -378,7 +484,7 @@ async function loadGLTF(url)
378
484
  * is fetched then
379
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
380
486
  * a warning and left out
381
- * @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
382
488
  * @param {string} [baseUrl] - Where the .bin and image files are, with its trailing slash; loadGLTF passes the file's folder
383
489
  * @param {Map<string, Blob>} [files] - The files it refers to, by their paths, in place of fetching them
384
490
  * @return {Promise<GLTFModel>}
@@ -390,7 +496,10 @@ async function loadGLTF(url)
390
496
  * @memberof GLTF */
391
497
  async function parseGLTF(data, baseUrl='', files)
392
498
  {
393
- 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
394
503
  if (data instanceof ArrayBuffer)
395
504
  {
396
505
  const view = new DataView(data);
@@ -474,6 +583,9 @@ async function parseGLTF(data, baseUrl='', files)
474
583
  // the parts: the scene's nodes walked with their transforms, every primitive of a node's mesh placed by it;
475
584
  // each node's parent and resting place are kept, so an animation can move a part from where it rests
476
585
  const parts = [], parents = [], restInverse = [], restPose = [], visited = new Set;
586
+ // every node's parent from the children lists, so a joint outside the scene's nodes keeps its own; the walk
587
+ // below sets them again for the nodes it reaches
588
+ (json.nodes || []).forEach((node, i)=> (node.children || []).forEach((child)=> parents[child] ??= i));
477
589
  const visit = (index, parentMatrix, parentIndex, parentRest)=>
478
590
  {
479
591
  // nodes are trees, so one reached again is a cycle or a node with two parents, a file problem
@@ -499,11 +611,18 @@ async function parseGLTF(data, baseUrl='', files)
499
611
  if (node.mesh !== undefined)
500
612
  {
501
613
  const mesh = json.meshes[node.mesh];
614
+ // a skinned mesh is placed by its joints and not by its node, as the format says, so it is read as stored
615
+ const skin = node.skin !== undefined ? json.skins?.[node.skin] : undefined;
502
616
  for (const primitive of mesh.primitives)
503
617
  {
504
- const part = gltfPart(json, buffers, textures, primitive, matrix, node.name || mesh.name || 'part ' + parts.length);
618
+ const skinned = skin?.joints?.length && primitive.attributes.JOINTS_0 !== undefined &&
619
+ primitive.attributes.WEIGHTS_0 !== undefined;
620
+ const part = gltfPart(json, buffers, textures, primitive, skinned ? new Matrix4 : matrix,
621
+ node.name || mesh.name || 'part ' + parts.length);
505
622
  if (!part) continue;
506
623
  part.node = index;
624
+ if (skinned)
625
+ part.skin = gltfSkin(json, buffers, skin, primitive, part);
507
626
  parts.push(part);
508
627
  }
509
628
  }
@@ -548,8 +667,194 @@ async function parseGLTF(data, baseUrl='', files)
548
667
  const used = new Set(parts.flatMap(p=> [p.textureInfo, p.normalMap, p.emissiveMap]));
549
668
  for (const texture of textures)
550
669
  texture && !used.has(texture) && texture.destroyWebGLTexture();
551
- return new GLTFModel(parts, animations, {nodes: json.nodes, parents, restInverse, restPose});
670
+ // a skinned part rests as its joints place it, with nothing moved
671
+ const nodeTree = {nodes: json.nodes, parents, restInverse, restPose}, restWorlds = gltfNodeWorlds(nodeTree, new Map);
672
+ for (const part of parts)
673
+ part.skin && gltfSkinApply(part.skin, restWorlds, RENDER3D_IDENTITY, part.mesh);
674
+ return new GLTFModel(parts, animations, nodeTree);
675
+ }
676
+
677
+ // the values of each node an animation moves at a time, a Map of node to its translation, rotation and scale;
678
+ // every other node keeps its own
679
+ function gltfPoseNodes(tree, animation, time)
680
+ {
681
+ const moved = new Map;
682
+ for (const channel of animation.channels)
683
+ {
684
+ let node = moved.get(channel.node);
685
+ node || moved.set(channel.node, node = gltfNodeTRS(tree.nodes[channel.node]));
686
+ gltfSample(channel, time, node[channel.path]);
687
+ }
688
+ return moved;
689
+ }
690
+
691
+ // two such sets of node values mixed by a weight, 0 all a, 1 all b: places and scales in a line, turns the short
692
+ // way round; a node only one moves has its own values in the other
693
+ function gltfPoseBlend(tree, a, b, weight)
694
+ {
695
+ const mixed = new Map, lerp3 = (p, q)=> p.map((v, i)=> v + (q[i] - v) * weight);
696
+ for (const node of new Set([...a.keys(), ...b.keys()]))
697
+ {
698
+ const p = a.get(node) || gltfNodeTRS(tree.nodes[node]), q = b.get(node) || gltfNodeTRS(tree.nodes[node]);
699
+ mixed.set(node, {translation: lerp3(p.translation, q.translation), scale: lerp3(p.scale, q.scale),
700
+ rotation: gltfSlerp(p.rotation, q.rotation, weight)});
701
+ }
702
+ return mixed;
703
+ }
704
+
705
+ // the turn between two quaternions, x y z w, a part of the way along the short way round
706
+ function gltfSlerp(a, b, t)
707
+ {
708
+ let dot = a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3], sign = 1;
709
+ if (dot < 0)
710
+ dot = -dot, sign = -1; // the same turn the other way round is shorter
711
+ let wa = 1 - t, wb = t * sign;
712
+ if (dot < .9995)
713
+ {
714
+ // along the arc; nearly the same turn is done in a line, where the arc's sine goes to nothing
715
+ const angle = Math.acos(dot), s = sin(angle);
716
+ wa = sin(wa * angle) / s, wb = sin(t * angle) / s * sign;
717
+ }
718
+ const out = a.map((v, i)=> v * wa + b[i] * wb), l = hypot(...out) || 1;
719
+ return out.map(v=> v / l);
720
+ }
721
+
722
+ // each node's place in the model through its parents, with the values in moved for the nodes there, kept as found
723
+ function gltfNodeWorlds(tree, moved)
724
+ {
725
+ const world = [];
726
+ const worldOf = (i)=>
727
+ {
728
+ if (world[i]) return world[i];
729
+ const local = gltfNodeMatrix(moved.get(i) || tree.nodes[i]), parent = tree.parents[i];
730
+ return world[i] = parent === undefined ? local : worldOf(parent).copy().multiply(local);
731
+ };
732
+ return worldOf;
733
+ }
734
+
735
+ // an animation's time a frame on, at a speed, around again when it loops, held at its ends when it does not
736
+ function gltfAnimationStep(animation, time, speed, loop)
737
+ {
738
+ const duration = animation.duration, t = time + timeDelta * speed;
739
+ return loop ? duration ? mod(t, duration) : 0 : clamp(t, 0, duration);
740
+ }
741
+
742
+ // a skinned primitive's skin: its joints, the nodes that bend it, their inverse bind matrices, and for each vertex
743
+ // of the part's mesh four joints, as places in that list, and four weights made to sum to 1, with its place and
744
+ // normal as stored, which each pose bends from; a mesh split for flat normals maps its corners to the file's
745
+ function gltfSkin(json, buffers, skin, primitive, part)
746
+ {
747
+ const joints = skin.joints, mesh = part.mesh, count = mesh.points.length;
748
+ const bind = skin.inverseBindMatrices !== undefined ? gltfAccessor(json, buffers, skin.inverseBindMatrices).data : undefined;
749
+ const inverseBind = joints.map((_, j)=> bind ? new Matrix4(bind.subarray(j * 16, j * 16 + 16)) : new Matrix4);
750
+ // a second set of four, as rigs from many tools have, is read too, and each vertex keeps its four strongest
751
+ const attributes = primitive.attributes, read = (name)=> gltfAccessor(json, buffers, attributes[name]).data;
752
+ const sets = [[read('JOINTS_0'), read('WEIGHTS_0')]];
753
+ attributes.JOINTS_1 !== undefined && attributes.WEIGHTS_1 !== undefined && sets.push([read('JOINTS_1'), read('WEIGHTS_1')]);
754
+ const vertexJoints = new Uint16Array(count * 4), vertexWeights = new Float32Array(count * 4);
755
+ const bindPoints = new Float32Array(count * 3), bindNormals = new Float32Array(count * 3);
756
+ for (let v = 0; v < count; ++v)
757
+ {
758
+ const from = part.vertexSource ? part.vertexSource[v] : v, influences = [];
759
+ for (const [fileJoints, fileWeights] of sets)
760
+ for (let k = 0; k < 4; ++k)
761
+ influences.push([fileJoints[from * 4 + k], fileWeights[from * 4 + k]]);
762
+ sets.length > 1 && influences.sort((a, b)=> b[1] - a[1]); // the strongest four first
763
+ let total = 0;
764
+ for (let k = 0; k < 4; ++k)
765
+ total += influences[k][1];
766
+ for (let k = 0; k < 4; ++k)
767
+ {
768
+ const [joint, weight] = influences[k];
769
+ vertexJoints[v * 4 + k] = joint < joints.length ? joint : 0;
770
+ vertexWeights[v * 4 + k] = total ? weight / total : k ? 0 : 1;
771
+ }
772
+ const p = mesh.points[v], n = mesh.normals[v];
773
+ bindPoints.set([p.x, p.y, p.z], v * 3);
774
+ bindNormals.set([n.x, n.y, n.z], v * 3);
775
+ // vectors of its own, as each pose writes into them, where a flat normal is shared by a face's corners
776
+ mesh.points[v] = p.copy(), mesh.normals[v] = n.copy();
777
+ }
778
+ return {joints, inverseBind, vertexJoints, vertexWeights, bindPoints, bindNormals};
779
+ }
780
+
781
+ // a skinned part's mesh copied with vectors of its own for an object to bend, its upload rewriting only the values
782
+ function gltfSkinMeshCopy(mesh)
783
+ {
784
+ const copy = new Mesh;
785
+ copy.points = mesh.points.map(p=> p.copy());
786
+ copy.normals = mesh.normals.map(n=> n.copy());
787
+ copy.uvs = mesh.uvs.slice();
788
+ copy.colors = mesh.colors.slice();
789
+ copy.indices = mesh.indices && mesh.indices.slice();
790
+ copy.doubleSided = mesh.doubleSided;
791
+ copy.dynamicDraw = true;
792
+ return copy;
793
+ }
794
+
795
+ // bend a skinned mesh to a pose: each vertex its stored place moved by its four joints, weighted, and its normal by
796
+ // their normal matrices, each joint's matrix modelMatrix * its place now * its inverse bind; written into the
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
799
+ function gltfSkinApply(skin, worldOf, modelMatrix, mesh)
800
+ {
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)
805
+ {
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];
815
+ }
816
+ const {vertexJoints, vertexWeights, bindPoints, bindNormals} = skin, points = mesh.points, normals = mesh.normals;
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)
822
+ {
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];
825
+ let x = 0, y = 0, z = 0, nx = 0, ny = 0, nz = 0;
826
+ for (let k = 0; k < 4; ++k)
827
+ {
828
+ const w = vertexWeights[v4+k];
829
+ if (!w) continue;
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);
837
+ }
838
+ const p = points[v], normal = normals[v], l = (nx*nx + ny*ny + nz*nz) ** .5 || 1;
839
+ p.x = x, p.y = y, p.z = z;
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;
854
+ }
855
+ mesh.dirty = true;
552
856
  }
857
+ const gltfSkinScratch = [new Matrix4, new Matrix4]; // a joint's matrix and its normal matrix, as it is worked out
553
858
 
554
859
  // a node's translation, rotation and scale as arrays to animate, copies so the file's stay as they rest
555
860
  function gltfNodeTRS(node)
@@ -710,7 +1015,7 @@ function gltfFetch(uri, baseUrl, files)
710
1015
  const page = typeof location !== 'undefined' && location.href;
711
1016
  const base = page ? new URL(baseUrl, page) : /^[a-z][a-z0-9+.-]*:/i.test(baseUrl) ? baseUrl : undefined;
712
1017
  const url = base ? new URL(uri, base).href : baseUrl + uri;
713
- return fetch(url).then(r=>
1018
+ return loadFetch(url, 'glTF').then(r=>
714
1019
  {
715
1020
  if (!r.ok) throw new Error('glTF needs ' + uri + ', not found at ' + url);
716
1021
  return r;
@@ -755,6 +1060,9 @@ function gltfAccessor(json, buffers, index)
755
1060
  if (view)
756
1061
  {
757
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');
758
1066
  if (stride === components * size)
759
1067
  out.set(new Type(buffer, offset, a.count * components)); // packed, one view over all of it
760
1068
  else
@@ -769,6 +1077,13 @@ function gltfAccessor(json, buffers, index)
769
1077
  const indexView = json.bufferViews?.[indices.bufferView], valueView = json.bufferViews?.[values.bufferView];
770
1078
  if (!IndexType || !indexView || !valueView)
771
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');
772
1087
  const at = new IndexType(buffers[indexView.buffer], (indexView.byteOffset || 0) + (indices.byteOffset || 0), sparse.count);
773
1088
  const data = new Type(buffers[valueView.buffer], (valueView.byteOffset || 0) + (values.byteOffset || 0), sparse.count * components);
774
1089
  for (let i = 0; i < sparse.count; ++i)
@@ -800,7 +1115,10 @@ function gltfPart(json, buffers, textures, primitive, matrix, name)
800
1115
  };
801
1116
  const points = read(attributes.POSITION, (d, k)=> vec3(d[k], d[k+1], d[k+2]));
802
1117
  const normals = attributes.NORMAL !== undefined ? read(attributes.NORMAL, (d, k)=> vec3(d[k], d[k+1], d[k+2])) : undefined;
803
- // 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
804
1122
  const material = json.materials?.[primitive.material] || {}, pbr = material.pbrMetallicRoughness || {};
805
1123
  const textureRef = pbr.baseColorTexture, uvTransform = textureRef?.extensions?.KHR_texture_transform;
806
1124
  const uvAccessor = attributes['TEXCOORD_' + (uvTransform?.texCoord ?? textureRef?.texCoord ?? 0)];
@@ -816,7 +1134,10 @@ function gltfPart(json, buffers, textures, primitive, matrix, name)
816
1134
  else if (mode === 6) // a fan around the first entry
817
1135
  indices = indices.flatMap((_, i, s)=> i < 2 ? [] : [s[0], s[i-1], s[i]]);
818
1136
  const mesh = new Mesh().addTriangles(points, indices, normals, uvs, colors);
819
- normals || mesh.computeNormals(false); // flat when the file gives none, as the format says
1137
+ // flat when the file gives none, as the format says, which gives each corner a vertex of its own: which of the
1138
+ // file's each one is, kept for a skin
1139
+ const vertexSource = normals ? undefined : mesh.indices.slice();
1140
+ normals || mesh.computeNormals(false);
820
1141
  mesh.transform(matrix);
821
1142
  const factor = pbr.baseColorFactor || [1, 1, 1, 1];
822
1143
  mesh.doubleSided = !!material.doubleSided;
@@ -829,6 +1150,7 @@ function gltfPart(json, buffers, textures, primitive, matrix, name)
829
1150
  const part = new GLTFPart(name, mesh, rgb(gltfSRGB(factor[0]), gltfSRGB(factor[1]), gltfSRGB(factor[2]), alpha),
830
1151
  textureRef ? textures[textureRef.index] : undefined, blend || transmission > 0);
831
1152
  part.pixelated = json.samplers?.[texture?.sampler]?.magFilter === 9728; // NEAREST
1153
+ part.vertexSource = vertexSource;
832
1154
  part.unlit = !!material.extensions?.KHR_materials_unlit;
833
1155
 
834
1156
  // the normal and emissive maps, read at the base color texture's uvs; the emissive texture is multiplied by the
@@ -837,6 +1159,7 @@ function gltfPart(json, buffers, textures, primitive, matrix, name)
837
1159
  const normalRef = material.normalTexture, emissiveRef = material.emissiveTexture;
838
1160
  part.normalMap = normalRef && textures[normalRef.index];
839
1161
  part.normalScale = normalRef?.scale ?? 1;
1162
+ part.roughness = pbr.roughnessFactor ?? 1; // a roughness map is not read
840
1163
  const [er, eg, eb] = material.emissiveFactor || [0, 0, 0];
841
1164
  if (er || eg || eb)
842
1165
  {