littlejsengine 1.24.2 → 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/CHANGELOG.md +55 -0
- package/EDITOR.md +2 -2
- package/FAQ.md +6 -0
- package/REFERENCE.md +86 -41
- package/dist/littlejs.d.ts +600 -323
- package/dist/littlejs.esm.js +2412 -1797
- package/dist/littlejs.esm.min.js +38 -24
- package/dist/littlejs.js +2410 -1797
- package/dist/littlejs.min.js +37 -23
- package/dist/littlejs.release.js +2393 -1826
- package/package.json +1 -1
- package/plugins/box2d.js +41 -28
- package/plugins/drawUtilities.js +374 -380
- package/plugins/gltf.js +331 -55
- package/plugins/lightSystem.js +2 -2
- package/plugins/math3d.js +1013 -1014
- package/plugins/newgrounds.js +1 -4
- package/plugins/particleEffects.js +3 -4
- package/plugins/pathFinder.js +52 -28
- package/plugins/pluginExport.js +2 -0
- package/plugins/postProcess.js +196 -65
- package/plugins/render3d.js +8 -6
- package/plugins/render3dEditor.js +3 -3
- package/plugins/render3dExtras.js +31 -10
- package/plugins/render3dLevel.js +15 -4
- package/plugins/textureSheet.js +15 -7
- package/plugins/tweenSystem.js +43 -38
- package/plugins/uiSystem.js +25 -20
- package/src/engine.js +39 -21
- package/src/engineAudio.js +13 -9
- package/src/engineBuild.mjs +49 -2
- package/src/engineDebug.js +3 -2
- package/src/engineDraw.js +20 -18
- package/src/engineEditor.js +75 -23
- package/src/engineInput.js +10 -10
- package/src/engineMath.js +3 -6
- package/src/engineObject.js +8 -9
- package/src/engineParticles.js +8 -7
- package/src/engineRelease.js +17 -10
- package/src/engineTileLayer.js +33 -14
- package/src/engineUtilities.js +15 -2
- package/src/engineWebGL.js +34 -12
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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
184
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
@@ -682,11 +949,16 @@ function gltfFetch(uri, baseUrl, files)
|
|
|
682
949
|
const path = clean(baseUrl + name);
|
|
683
950
|
if (path === undefined)
|
|
684
951
|
return Promise.reject(new Error('glTF needs ' + name + ', which is above the files given'));
|
|
685
|
-
const lower = path.toLowerCase(), file =
|
|
952
|
+
const lower = path.toLowerCase(), file = lower.slice(lower.lastIndexOf('/') + 1);
|
|
686
953
|
const byPath = new Map([...files].map(([key, value])=> [clean(key)?.toLowerCase(), value]));
|
|
687
954
|
const found = files.get(path) ?? byPath.get(lower), keys = [...byPath.keys()];
|
|
688
955
|
if (found)
|
|
689
956
|
return Promise.resolve(new Response(found));
|
|
957
|
+
// what is in a folder dropped, not the folder, leaves the folder's own name out of the paths: the longest key
|
|
958
|
+
// the path ends with, a folder and more, as a name alone is only taken when one file has it
|
|
959
|
+
const end = keys.filter((k)=> k?.includes('/') && lower.endsWith('/' + k)).sort((a, b)=> b.length - a.length)[0];
|
|
960
|
+
if (end)
|
|
961
|
+
return Promise.resolve(new Response(byPath.get(end)));
|
|
690
962
|
const named = new Set(keys.filter((k)=> k?.slice(k.lastIndexOf('/') + 1) === file).map((k)=> byPath.get(k)));
|
|
691
963
|
if (named.size === 1)
|
|
692
964
|
return Promise.resolve(new Response([...named][0]));
|
|
@@ -811,7 +1083,10 @@ function gltfPart(json, buffers, textures, primitive, matrix, name)
|
|
|
811
1083
|
else if (mode === 6) // a fan around the first entry
|
|
812
1084
|
indices = indices.flatMap((_, i, s)=> i < 2 ? [] : [s[0], s[i-1], s[i]]);
|
|
813
1085
|
const mesh = new Mesh().addTriangles(points, indices, normals, uvs, colors);
|
|
814
|
-
|
|
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);
|
|
815
1090
|
mesh.transform(matrix);
|
|
816
1091
|
const factor = pbr.baseColorFactor || [1, 1, 1, 1];
|
|
817
1092
|
mesh.doubleSided = !!material.doubleSided;
|
|
@@ -824,6 +1099,7 @@ function gltfPart(json, buffers, textures, primitive, matrix, name)
|
|
|
824
1099
|
const part = new GLTFPart(name, mesh, rgb(gltfSRGB(factor[0]), gltfSRGB(factor[1]), gltfSRGB(factor[2]), alpha),
|
|
825
1100
|
textureRef ? textures[textureRef.index] : undefined, blend || transmission > 0);
|
|
826
1101
|
part.pixelated = json.samplers?.[texture?.sampler]?.magFilter === 9728; // NEAREST
|
|
1102
|
+
part.vertexSource = vertexSource;
|
|
827
1103
|
part.unlit = !!material.extensions?.KHR_materials_unlit;
|
|
828
1104
|
|
|
829
1105
|
// the normal and emissive maps, read at the base color texture's uvs; the emissive texture is multiplied by the
|
package/plugins/lightSystem.js
CHANGED
|
@@ -476,7 +476,7 @@ class LightSystemPlugin
|
|
|
476
476
|
o.renderLight();
|
|
477
477
|
}
|
|
478
478
|
|
|
479
|
-
// 3b. emissive objects draw their shape in
|
|
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
|
|
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);
|