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.
- package/CHANGELOG.md +123 -0
- package/EDITOR.md +2 -2
- package/FAQ.md +55 -11
- package/README.md +6 -2
- package/REFERENCE.md +203 -57
- package/dist/littlejs.d.ts +933 -313
- package/dist/littlejs.esm.js +5143 -2419
- package/dist/littlejs.esm.min.js +38 -24
- package/dist/littlejs.js +5132 -2416
- package/dist/littlejs.min.js +37 -23
- package/dist/littlejs.release.js +4905 -2232
- package/package.json +4 -4
- package/plugins/box2d.js +141 -56
- package/plugins/drawUtilities.js +374 -380
- package/plugins/gltf.js +383 -60
- package/plugins/lightSystem.js +412 -30
- package/plugins/math3d.js +1015 -1014
- package/plugins/medalSystem.js +19 -7
- package/plugins/newgrounds.js +5 -6
- package/plugins/parallax.js +1 -0
- package/plugins/particleEffects.js +47 -14
- package/plugins/pathFinder.js +60 -38
- package/plugins/pluginExport.js +11 -0
- package/plugins/postProcess.js +200 -67
- package/plugins/render3d.js +493 -2391
- package/plugins/render3dDebug.js +5 -8
- package/plugins/render3dEditor.js +84 -9
- package/plugins/render3dExtras.js +122 -33
- package/plugins/render3dLevel.js +67 -8
- package/plugins/render3dMesh.js +1158 -0
- package/plugins/render3dObject.js +1451 -0
- package/plugins/render3dVoxels.js +15 -3
- package/plugins/textureSheet.js +9 -8
- package/plugins/threejs.js +1 -1
- package/plugins/tweenSystem.js +117 -87
- package/plugins/uiSystem.js +62 -28
- package/plugins/wavedash.js +229 -0
- package/src/engine.js +192 -40
- package/src/engineAudio.js +47 -27
- package/src/engineBuild.mjs +6 -1
- package/src/engineDebug.js +23 -162
- package/src/engineDraw.js +148 -43
- package/src/engineEditor.js +153 -40
- package/src/engineExport.js +0 -3
- package/src/engineInput.js +31 -25
- package/src/engineMath.js +38 -20
- package/src/engineObject.js +68 -50
- package/src/engineParticles.js +66 -18
- package/src/engineRelease.js +18 -15
- package/src/engineSettings.js +48 -9
- package/src/engineTileLayer.js +44 -18
- package/src/engineUtilities.js +56 -11
- 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
|
|
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,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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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));
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
|
460
|
+
const response = await loadFetch(url, 'loadGLTF');
|
|
362
461
|
if (!response.ok)
|
|
363
|
-
throw new Error(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
{
|