littlejsengine 1.24.3 → 1.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,29 @@ 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 {GLTFSkin|undefined} - For a skinned mesh, what bends it: mesh is its resting pose, and the
77
+ * object createObject makes bends a copy of its own to each pose
78
+ * @type {GLTFSkin|undefined} */
79
+ this.skin = undefined;
80
+ // which of the file's vertices each of the mesh's is, when flat normals split them, for a skin
81
+ /** @type {Array<number>|undefined} */
82
+ this.vertexSource = undefined;
72
83
  }
73
84
  }
74
85
 
86
+ /**
87
+ * What bends a skinned part: its joints, the nodes that move it, their inverse bind matrices, and for each vertex
88
+ * of its mesh four joints and four weights, with the place and normal each pose bends from
89
+ * @typedef {Object} GLTFSkin
90
+ * @property {Array<number>} joints - The joints, as node numbers
91
+ * @property {Array<Matrix4>} inverseBind - Each joint's inverse bind matrix
92
+ * @property {Uint16Array} vertexJoints - Four joints a vertex, as places in joints
93
+ * @property {Float32Array} vertexWeights - Four weights a vertex, summing to 1
94
+ * @property {Float32Array} bindPoints - Each vertex's place as stored, x y z
95
+ * @property {Float32Array} bindNormals - Each vertex's normal as stored, x y z
96
+ * @memberof GLTF
97
+ */
98
+
75
99
  /**
76
100
  * GLTFAnimation - One animation of a model: keys that move, turn and scale its nodes over time
77
101
  * - Play it through the GLTFObject that model.createObject makes
@@ -79,7 +103,8 @@ class GLTFPart
79
103
  */
80
104
  class GLTFAnimation
81
105
  {
82
- /** @param {string} name @param {Array<Object>} channels */
106
+ /** Make an animation from its channels, as the loader does
107
+ * @param {string} name @param {Array<Object>} channels */
83
108
  constructor(name, channels)
84
109
  {
85
110
  /** @property {string} - Its name in the file, or 'animation' and its number when it has none */
@@ -100,7 +125,8 @@ class GLTFAnimation
100
125
  */
101
126
  class GLTFModel
102
127
  {
103
- /** @param {Array<GLTFPart>} parts @param {Array<GLTFAnimation>} [animations] @param {Object} [nodeTree] */
128
+ /** Make a model from its parts, as the loader does
129
+ * @param {Array<GLTFPart>} parts @param {Array<GLTFAnimation>} [animations] @param {Object} [nodeTree] */
104
130
  constructor(parts, animations=[], nodeTree)
105
131
  {
106
132
  /** @property {Array<GLTFPart>} - One per primitive of every node that has a mesh */
@@ -114,7 +140,7 @@ class GLTFModel
114
140
  * and blending and unlit stay with the parts, which createObject draws */
115
141
  this.mesh = new Mesh;
116
142
  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);
143
+ this.mesh.combine(part.mesh, !part.skin && nodeTree?.restPose?.[part.node] || RENDER3D_IDENTITY, part.color);
118
144
  // the one texture every part uses, when they all do; a part without one has no uvs into it, so a model
119
145
  // that mixes plain and textured parts, or uses several textures, is drawn through createObject instead
120
146
  const textures = new Set(parts.map(p=> p.textureInfo));
@@ -180,29 +206,18 @@ class GLTFModel
180
206
  * @return {Array<Matrix4>} */
181
207
  getPose(animation, time)
182
208
  {
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
- }
209
+ if (!this.nodeTree) return this.parts.map(()=> new Matrix4);
210
+ return this.partPoses(gltfNodeWorlds(this.nodeTree, gltfPoseNodes(this.nodeTree, animation, time)));
211
+ }
194
212
 
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));
213
+ // each part's move from where it rests, with each node's place in the model from worldOf, through what center
214
+ // and fit did: modelMatrix * now * rest inverse * modelMatrix inverse; a skinned part is posed by its joints
215
+ // and stays where it is
216
+ partPoses(worldOf)
217
+ {
218
+ const tree = this.nodeTree, model = this.modelMatrix, modelInverse = model.copy().invert();
219
+ return this.parts.map(part=> part.skin ? new Matrix4 :
220
+ model.copy().multiply(worldOf(part.node)).multiply(tree.restInverse[part.node]).multiply(modelInverse));
206
221
  }
207
222
 
208
223
  /** Free the GPU buffers of every part's mesh and of the combined mesh, and the textures, for a model that is
@@ -263,19 +278,32 @@ class GLTFObject extends EngineObject3D
263
278
  this.animationLoop = true;
264
279
  /** @property {boolean} - Whether it is moving through the animation now */
265
280
  this.animationPlaying = false;
281
+ // what a cross-fade comes from: an animation going on as it was, or a pose held, and how far it is
282
+ /** @type {{animation?: GLTFAnimation, time?: number, speed?: number, loop?: boolean, nodes?: Map<number, Object>}|undefined} */
283
+ this.blendFrom = undefined;
284
+ this.blendTime = 0;
285
+ this.blendElapsed = 0;
286
+ // each node's values and place in the model as last posed, for a fade from here and for getJointMatrix, and
287
+ // whether that pose was a mix, as a fade stopped part way holds it
288
+ /** @type {Map<number, Object>|undefined} */
289
+ this.poseNodes = undefined;
290
+ this.poseMixed = false;
291
+ /** @type {(function(number): Matrix4)|undefined} */
292
+ this.poseWorldOf = undefined;
266
293
  /** @property {Array<EngineObject3D>} - The child that draws each of the model's parts, in the order of
267
294
  * model.parts, which an animation poses; one destroyed or taken off the object is left alone
268
295
  * @type {Array<EngineObject3D>} */
269
296
  this.parts = [];
270
297
  for (const part of model.parts)
271
298
  {
272
- const o = new EngineObject3D(vec3(), part.mesh, part.textureInfo, part.color);
299
+ // a skinned part bends a mesh of its own, so objects of one model each hold their own pose
300
+ const o = new EngineObject3D(vec3(), part.skin ? gltfSkinMeshCopy(part.mesh) : part.mesh, part.textureInfo, part.color);
273
301
  o.transparent = part.transparent;
274
302
  o.pixelated = part.pixelated;
275
303
  o.emissive = part.unlit ? 1 : 0;
276
304
  o.normalMap = part.normalMap, o.normalScale = part.normalScale;
277
305
  o.emissiveMap = part.emissiveMap, o.emissiveMapColor = part.emissiveMapColor.copy(); // its own, as color is
278
- const rest = model.nodeTree?.restPose?.[part.node];
306
+ const rest = !part.skin && model.nodeTree?.restPose?.[part.node];
279
307
  if (rest)
280
308
  {
281
309
  // baked at scale 1 from a node resting at 0, it starts as it rests, where a pose would put it
@@ -290,15 +318,44 @@ class GLTFObject extends EngineObject3D
290
318
  }
291
319
  }
292
320
 
293
- /** Play an animation from its start
321
+ /** Destroy the object and its parts, and free the meshes its skinned parts bend, which are its own
322
+ * @param {boolean} [immediate] */
323
+ destroy(immediate)
324
+ {
325
+ if (this.destroyed) return;
326
+ this.parts.forEach((o, i)=> this.model.parts[i]?.skin && o.mesh?.dispose());
327
+ super.destroy(immediate);
328
+ }
329
+
330
+ /** Play an animation from its start, at once or cross-faded from the pose it is in
331
+ * - A play with a blend of the animation already playing goes on with it, so state code may call it each
332
+ * frame; without a blend it starts the animation again
294
333
  * @param {string|number|GLTFAnimation} [animation] - Its name, its number in model.animations, or itself
295
334
  * @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)
335
+ * @param {number} [speed] - 1 is as made, negative plays it backward from its end
336
+ * @param {number} [blend] - Seconds to cross-fade from the pose it is in, 0 to switch at once; the animation
337
+ * it comes from goes on through the fade, and a fade started during another fades from the mix there */
338
+ play(animation=0, loop=true, speed=1, blend=0)
298
339
  {
299
340
  const found = this.model.getAnimation(animation);
300
341
  ASSERT(found, 'the model has no animation ' + animation, this.model.animations.map(a=> a.name));
301
342
  if (!found) return;
343
+ if (blend > 0 && found === this.animation && this.animationPlaying)
344
+ {
345
+ this.animationLoop = loop, this.animationSpeed = speed;
346
+ return;
347
+ }
348
+ const fading = this.blendFrom;
349
+ this.blendFrom = undefined;
350
+ if (blend > 0)
351
+ {
352
+ // from the animation playing, going on as it was, or from the pose held, a fade's mix included
353
+ const from = this.animation && !fading && !this.poseMixed ? {animation: this.animation, time: this.animationTime,
354
+ speed: this.animationPlaying ? this.animationSpeed : 0, loop: this.animationLoop} : undefined;
355
+ this.blendFrom = from || {nodes: this.poseNodes || new Map};
356
+ this.blendTime = blend;
357
+ this.blendElapsed = 0;
358
+ }
302
359
  this.animation = found;
303
360
  this.animationLoop = loop;
304
361
  this.animationSpeed = speed;
@@ -306,21 +363,39 @@ class GLTFObject extends EngineObject3D
306
363
  this.setAnimationTime(speed < 0 ? found.duration : 0);
307
364
  }
308
365
 
309
- /** Stop the animation where it is, the parts hold that pose */
310
- stop() { this.animationPlaying = false; }
366
+ /** Stop the animation where it is, the parts hold that pose; a cross-fade going on stops too, holding the mix */
367
+ stop() { this.animationPlaying = false; this.blendFrom = undefined; }
311
368
 
312
369
  /** Put the parts where the animation has them at a time, playing or not
313
370
  * @param {number} time - Seconds into the animation */
314
371
  setAnimationTime(time)
315
372
  {
316
373
  this.animationTime = time;
317
- if (!this.animation) return;
374
+ const model = this.model, tree = model.nodeTree;
375
+ if (!this.animation || !tree) return;
376
+
377
+ // each node's values now, mixed with what a fade comes from by how far it is, eased in and out
378
+ let nodes = gltfPoseNodes(tree, this.animation, time);
379
+ const from = this.blendFrom;
380
+ if (from)
381
+ {
382
+ const fromNodes = from.nodes || gltfPoseNodes(tree, from.animation, from.time);
383
+ nodes = gltfPoseBlend(tree, fromNodes, nodes, smoothStep(clamp(this.blendElapsed / this.blendTime)));
384
+ }
385
+ const worldOf = gltfNodeWorlds(tree, nodes);
386
+ this.poseNodes = nodes, this.poseWorldOf = worldOf, this.poseMixed = !!from;
387
+
318
388
  // 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;
389
+ const pose = model.partPoses(worldOf), parts = this.parts;
320
390
  for (let i = 0; i < pose.length && i < parts.length; ++i)
321
391
  {
322
- const o = parts[i], m = pose[i];
392
+ const o = parts[i], m = pose[i], skin = model.parts[i].skin;
323
393
  if (o.destroyed || o.parent !== this) continue;
394
+ if (skin)
395
+ {
396
+ gltfSkinApply(skin, worldOf, model.modelMatrix, o.mesh); // bent by its joints, where it is
397
+ continue;
398
+ }
324
399
  // drawn with the whole pose, which a parent's uneven scale can shear, the parts kept for what reads them
325
400
  o.localMatrix = m;
326
401
  o.pos3D = m.getTranslation();
@@ -329,23 +404,40 @@ class GLTFObject extends EngineObject3D
329
404
  }
330
405
  }
331
406
 
332
- /** Move through the animation, called automatically each frame */
407
+ /** Move through the animation, and a cross-fade into it, called automatically each frame */
333
408
  update()
334
409
  {
335
410
  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)
411
+ const animation = this.animation, from = this.blendFrom;
412
+ if (!animation || !this.animationPlaying && !from) return;
413
+ let t = this.animationTime;
414
+ if (this.animationPlaying)
343
415
  {
344
- // the end, or the start when playing backward: hold the last pose there
345
- t = clamp(t, 0, duration);
346
- this.animationPlaying = false;
416
+ t = gltfAnimationStep(animation, t, this.animationSpeed, this.animationLoop);
417
+ if (!this.animationLoop && (t >= animation.duration || t <= 0))
418
+ this.animationPlaying = false; // the end, or the start when playing backward: hold the last pose there
419
+ }
420
+ if (from)
421
+ {
422
+ this.blendElapsed += timeDelta;
423
+ if (from.animation) // what it fades from goes on as it was
424
+ from.time = gltfAnimationStep(from.animation, from.time, from.speed, from.loop);
347
425
  }
348
426
  this.setAnimationTime(t);
427
+ if (from && this.blendElapsed >= this.blendTime)
428
+ this.blendFrom = undefined; // faded all the way, the pose just set is the animation's own
429
+ }
430
+
431
+ /** A node's matrix in the world as the model is posed now, by its name in the file, to hang something on a
432
+ * joint, a sword on a hand or a hat on a head; undefined when the model has no node of that name
433
+ * @param {string} name
434
+ * @return {Matrix4|undefined} */
435
+ getJointMatrix(name)
436
+ {
437
+ const tree = this.model.nodeTree, index = tree?.nodes?.findIndex(node=> node.name === name) ?? -1;
438
+ if (index < 0) return;
439
+ const worldOf = this.poseWorldOf || gltfNodeWorlds(tree, new Map);
440
+ return this.getMatrix().multiply(this.model.modelMatrix).multiply(worldOf(index));
349
441
  }
350
442
  }
351
443
 
@@ -367,7 +459,14 @@ async function loadGLTF(url)
367
459
  let base = '';
368
460
  if (!/^(blob|data):/i.test(from))
369
461
  base = response.url ? new URL('.', from).href : from.slice(0, from.lastIndexOf('/') + 1);
370
- return parseGLTF(await response.arrayBuffer(), base);
462
+ const data = await response.arrayBuffer();
463
+ try { return await parseGLTF(data, base); }
464
+ catch (e)
465
+ {
466
+ // which file, and a web page, as a dev server sends for a mistyped path, said as one
467
+ const page = loadIsWebPage(String.fromCharCode(...new Uint8Array(data, 0, min(data.byteLength, 64))));
468
+ throw new Error('loadGLTF ' + url + (page ? ' is a web page, so the path may be wrong' : ': ' + e.message));
469
+ }
371
470
  }
372
471
 
373
472
  /** Parse a model from GLB bytes or glTF JSON, fetching the buffers and images it refers to
@@ -474,6 +573,9 @@ async function parseGLTF(data, baseUrl='', files)
474
573
  // the parts: the scene's nodes walked with their transforms, every primitive of a node's mesh placed by it;
475
574
  // each node's parent and resting place are kept, so an animation can move a part from where it rests
476
575
  const parts = [], parents = [], restInverse = [], restPose = [], visited = new Set;
576
+ // every node's parent from the children lists, so a joint outside the scene's nodes keeps its own; the walk
577
+ // below sets them again for the nodes it reaches
578
+ (json.nodes || []).forEach((node, i)=> (node.children || []).forEach((child)=> parents[child] ??= i));
477
579
  const visit = (index, parentMatrix, parentIndex, parentRest)=>
478
580
  {
479
581
  // nodes are trees, so one reached again is a cycle or a node with two parents, a file problem
@@ -499,11 +601,18 @@ async function parseGLTF(data, baseUrl='', files)
499
601
  if (node.mesh !== undefined)
500
602
  {
501
603
  const mesh = json.meshes[node.mesh];
604
+ // a skinned mesh is placed by its joints and not by its node, as the format says, so it is read as stored
605
+ const skin = node.skin !== undefined ? json.skins?.[node.skin] : undefined;
502
606
  for (const primitive of mesh.primitives)
503
607
  {
504
- const part = gltfPart(json, buffers, textures, primitive, matrix, node.name || mesh.name || 'part ' + parts.length);
608
+ const skinned = skin?.joints?.length && primitive.attributes.JOINTS_0 !== undefined &&
609
+ primitive.attributes.WEIGHTS_0 !== undefined;
610
+ const part = gltfPart(json, buffers, textures, primitive, skinned ? new Matrix4 : matrix,
611
+ node.name || mesh.name || 'part ' + parts.length);
505
612
  if (!part) continue;
506
613
  part.node = index;
614
+ if (skinned)
615
+ part.skin = gltfSkin(json, buffers, skin, primitive, part);
507
616
  parts.push(part);
508
617
  }
509
618
  }
@@ -548,7 +657,165 @@ async function parseGLTF(data, baseUrl='', files)
548
657
  const used = new Set(parts.flatMap(p=> [p.textureInfo, p.normalMap, p.emissiveMap]));
549
658
  for (const texture of textures)
550
659
  texture && !used.has(texture) && texture.destroyWebGLTexture();
551
- return new GLTFModel(parts, animations, {nodes: json.nodes, parents, restInverse, restPose});
660
+ // a skinned part rests as its joints place it, with nothing moved
661
+ const nodeTree = {nodes: json.nodes, parents, restInverse, restPose}, restWorlds = gltfNodeWorlds(nodeTree, new Map);
662
+ for (const part of parts)
663
+ part.skin && gltfSkinApply(part.skin, restWorlds, RENDER3D_IDENTITY, part.mesh);
664
+ return new GLTFModel(parts, animations, nodeTree);
665
+ }
666
+
667
+ // the values of each node an animation moves at a time, a Map of node to its translation, rotation and scale;
668
+ // every other node keeps its own
669
+ function gltfPoseNodes(tree, animation, time)
670
+ {
671
+ const moved = new Map;
672
+ for (const channel of animation.channels)
673
+ {
674
+ let node = moved.get(channel.node);
675
+ node || moved.set(channel.node, node = gltfNodeTRS(tree.nodes[channel.node]));
676
+ gltfSample(channel, time, node[channel.path]);
677
+ }
678
+ return moved;
679
+ }
680
+
681
+ // 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
682
+ // way round; a node only one moves has its own values in the other
683
+ function gltfPoseBlend(tree, a, b, weight)
684
+ {
685
+ const mixed = new Map, lerp3 = (p, q)=> p.map((v, i)=> v + (q[i] - v) * weight);
686
+ for (const node of new Set([...a.keys(), ...b.keys()]))
687
+ {
688
+ const p = a.get(node) || gltfNodeTRS(tree.nodes[node]), q = b.get(node) || gltfNodeTRS(tree.nodes[node]);
689
+ mixed.set(node, {translation: lerp3(p.translation, q.translation), scale: lerp3(p.scale, q.scale),
690
+ rotation: gltfSlerp(p.rotation, q.rotation, weight)});
691
+ }
692
+ return mixed;
693
+ }
694
+
695
+ // the turn between two quaternions, x y z w, a part of the way along the short way round
696
+ function gltfSlerp(a, b, t)
697
+ {
698
+ let dot = a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3], sign = 1;
699
+ if (dot < 0)
700
+ dot = -dot, sign = -1; // the same turn the other way round is shorter
701
+ let wa = 1 - t, wb = t * sign;
702
+ if (dot < .9995)
703
+ {
704
+ // along the arc; nearly the same turn is done in a line, where the arc's sine goes to nothing
705
+ const angle = Math.acos(dot), s = sin(angle);
706
+ wa = sin(wa * angle) / s, wb = sin(t * angle) / s * sign;
707
+ }
708
+ const out = a.map((v, i)=> v * wa + b[i] * wb), l = hypot(...out) || 1;
709
+ return out.map(v=> v / l);
710
+ }
711
+
712
+ // each node's place in the model through its parents, with the values in moved for the nodes there, kept as found
713
+ function gltfNodeWorlds(tree, moved)
714
+ {
715
+ const world = [];
716
+ const worldOf = (i)=>
717
+ {
718
+ if (world[i]) return world[i];
719
+ const local = gltfNodeMatrix(moved.get(i) || tree.nodes[i]), parent = tree.parents[i];
720
+ return world[i] = parent === undefined ? local : worldOf(parent).copy().multiply(local);
721
+ };
722
+ return worldOf;
723
+ }
724
+
725
+ // an animation's time a frame on, at a speed, around again when it loops, held at its ends when it does not
726
+ function gltfAnimationStep(animation, time, speed, loop)
727
+ {
728
+ const duration = animation.duration, t = time + timeDelta * speed;
729
+ return loop ? duration ? mod(t, duration) : 0 : clamp(t, 0, duration);
730
+ }
731
+
732
+ // a skinned primitive's skin: its joints, the nodes that bend it, their inverse bind matrices, and for each vertex
733
+ // of the part's mesh four joints, as places in that list, and four weights made to sum to 1, with its place and
734
+ // normal as stored, which each pose bends from; a mesh split for flat normals maps its corners to the file's
735
+ function gltfSkin(json, buffers, skin, primitive, part)
736
+ {
737
+ const joints = skin.joints, mesh = part.mesh, count = mesh.points.length;
738
+ const bind = skin.inverseBindMatrices !== undefined ? gltfAccessor(json, buffers, skin.inverseBindMatrices).data : undefined;
739
+ const inverseBind = joints.map((_, j)=> bind ? new Matrix4(bind.subarray(j * 16, j * 16 + 16)) : new Matrix4);
740
+ // a second set of four, as rigs from many tools have, is read too, and each vertex keeps its four strongest
741
+ const attributes = primitive.attributes, read = (name)=> gltfAccessor(json, buffers, attributes[name]).data;
742
+ const sets = [[read('JOINTS_0'), read('WEIGHTS_0')]];
743
+ attributes.JOINTS_1 !== undefined && attributes.WEIGHTS_1 !== undefined && sets.push([read('JOINTS_1'), read('WEIGHTS_1')]);
744
+ const vertexJoints = new Uint16Array(count * 4), vertexWeights = new Float32Array(count * 4);
745
+ const bindPoints = new Float32Array(count * 3), bindNormals = new Float32Array(count * 3);
746
+ for (let v = 0; v < count; ++v)
747
+ {
748
+ const from = part.vertexSource ? part.vertexSource[v] : v, influences = [];
749
+ for (const [fileJoints, fileWeights] of sets)
750
+ for (let k = 0; k < 4; ++k)
751
+ influences.push([fileJoints[from * 4 + k], fileWeights[from * 4 + k]]);
752
+ sets.length > 1 && influences.sort((a, b)=> b[1] - a[1]); // the strongest four first
753
+ let total = 0;
754
+ for (let k = 0; k < 4; ++k)
755
+ total += influences[k][1];
756
+ for (let k = 0; k < 4; ++k)
757
+ {
758
+ const [joint, weight] = influences[k];
759
+ vertexJoints[v * 4 + k] = joint < joints.length ? joint : 0;
760
+ vertexWeights[v * 4 + k] = total ? weight / total : k ? 0 : 1;
761
+ }
762
+ const p = mesh.points[v], n = mesh.normals[v];
763
+ bindPoints.set([p.x, p.y, p.z], v * 3);
764
+ bindNormals.set([n.x, n.y, n.z], v * 3);
765
+ // vectors of its own, as each pose writes into them, where a flat normal is shared by a face's corners
766
+ mesh.points[v] = p.copy(), mesh.normals[v] = n.copy();
767
+ }
768
+ return {joints, inverseBind, vertexJoints, vertexWeights, bindPoints, bindNormals};
769
+ }
770
+
771
+ // a skinned part's mesh copied with vectors of its own for an object to bend, its upload rewriting only the values
772
+ function gltfSkinMeshCopy(mesh)
773
+ {
774
+ const copy = new Mesh;
775
+ copy.points = mesh.points.map(p=> p.copy());
776
+ copy.normals = mesh.normals.map(n=> n.copy());
777
+ copy.uvs = mesh.uvs.slice();
778
+ copy.colors = mesh.colors.slice();
779
+ copy.indices = mesh.indices && mesh.indices.slice();
780
+ copy.doubleSided = mesh.doubleSided;
781
+ copy.dynamicDraw = true;
782
+ return copy;
783
+ }
784
+
785
+ // bend a skinned mesh to a pose: each vertex its stored place moved by its four joints, weighted, and its normal by
786
+ // 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
788
+ function gltfSkinApply(skin, worldOf, modelMatrix, mesh)
789
+ {
790
+ const matrices = [], normalMatrices = [];
791
+ for (let j = 0; j < skin.joints.length; ++j)
792
+ {
793
+ const m = modelMatrix.copy().multiply(worldOf(skin.joints[j])).multiply(skin.inverseBind[j]);
794
+ matrices.push(m.m), normalMatrices.push(render3DNormalMatrix(m).m);
795
+ }
796
+ const {vertexJoints, vertexWeights, bindPoints, bindNormals} = skin, points = mesh.points, normals = mesh.normals;
797
+ for (let v = 0; v < points.length; ++v)
798
+ {
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];
801
+ let x = 0, y = 0, z = 0, nx = 0, ny = 0, nz = 0;
802
+ for (let k = 0; k < 4; ++k)
803
+ {
804
+ const w = vertexWeights[v*4+k];
805
+ 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);
813
+ }
814
+ const p = points[v], normal = normals[v], l = (nx*nx + ny*ny + nz*nz) ** .5 || 1;
815
+ p.x = x, p.y = y, p.z = z;
816
+ normal.x = nx / l, normal.y = ny / l, normal.z = nz / l;
817
+ }
818
+ mesh.dirty = true;
552
819
  }
553
820
 
554
821
  // a node's translation, rotation and scale as arrays to animate, copies so the file's stay as they rest
@@ -816,7 +1083,10 @@ function gltfPart(json, buffers, textures, primitive, matrix, name)
816
1083
  else if (mode === 6) // a fan around the first entry
817
1084
  indices = indices.flatMap((_, i, s)=> i < 2 ? [] : [s[0], s[i-1], s[i]]);
818
1085
  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
1086
+ // flat when the file gives none, as the format says, which gives each corner a vertex of its own: which of the
1087
+ // file's each one is, kept for a skin
1088
+ const vertexSource = normals ? undefined : mesh.indices.slice();
1089
+ normals || mesh.computeNormals(false);
820
1090
  mesh.transform(matrix);
821
1091
  const factor = pbr.baseColorFactor || [1, 1, 1, 1];
822
1092
  mesh.doubleSided = !!material.doubleSided;
@@ -829,6 +1099,7 @@ function gltfPart(json, buffers, textures, primitive, matrix, name)
829
1099
  const part = new GLTFPart(name, mesh, rgb(gltfSRGB(factor[0]), gltfSRGB(factor[1]), gltfSRGB(factor[2]), alpha),
830
1100
  textureRef ? textures[textureRef.index] : undefined, blend || transmission > 0);
831
1101
  part.pixelated = json.samplers?.[texture?.sampler]?.magFilter === 9728; // NEAREST
1102
+ part.vertexSource = vertexSource;
832
1103
  part.unlit = !!material.extensions?.KHR_materials_unlit;
833
1104
 
834
1105
  // the normal and emissive maps, read at the base color texture's uvs; the emissive texture is multiplied by the
@@ -476,7 +476,7 @@ class LightSystemPlugin
476
476
  o.renderLight();
477
477
  }
478
478
 
479
- // 3b. emissive objects draw their shape in grey at their emissive level, white at 1, adding that much
479
+ // 3b. emissive objects draw their shape in gray at their emissive level, white at 1, adding that much
480
480
  // light where they are so they show their own colors; text goes to the 1x1 canvas as in the
481
481
  // shadow pass, so it is not drawn twice
482
482
  const saved = [drawContext, glCustomShader];
@@ -488,7 +488,7 @@ class LightSystemPlugin
488
488
  {
489
489
  if (o.destroyed || !(o.emissive > 0)) continue;
490
490
  const level = clamp(o.emissive)*255+.5|0; // packed like rgbaInt, red in the low byte
491
- glColorMask = 0xff000000; // its own alpha, and the grey from the additive color
491
+ glColorMask = 0xff000000; // its own alpha, and the gray from the additive color
492
492
  glColorAdditive = level | level<<8 | level<<16;
493
493
  glAdditive || setAdditiveBlendMode(); // added, an emitter ends its render with it off
494
494
  setShader(o.shader);