littlejsengine 1.19.3 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/FAQ.md +841 -820
  2. package/README.md +29 -25
  3. package/dist/littlejs.d.ts +2455 -935
  4. package/dist/littlejs.esm.js +30344 -21110
  5. package/dist/littlejs.esm.min.js +4 -3
  6. package/dist/littlejs.js +30259 -21052
  7. package/dist/littlejs.min.js +2 -2
  8. package/dist/littlejs.release.js +26567 -20455
  9. package/package.json +10 -5
  10. package/plugins/audioEffects.js +28 -24
  11. package/plugins/box2d.js +2516 -2073
  12. package/plugins/drawUtilities.js +206 -31
  13. package/plugins/gltf.js +740 -0
  14. package/plugins/jsconfig.json +19 -0
  15. package/plugins/lightSystem.js +719 -332
  16. package/plugins/math3d.js +58 -17
  17. package/plugins/medalSystem.js +326 -280
  18. package/plugins/newgrounds.js +400 -199
  19. package/plugins/pathFinder.js +208 -175
  20. package/plugins/pluginExport.js +15 -0
  21. package/plugins/postProcess.js +56 -13
  22. package/plugins/render3d.js +1643 -1577
  23. package/plugins/render3dDebug.js +109 -0
  24. package/plugins/render3dExtras.js +1754 -0
  25. package/plugins/render3dVoxels.js +502 -0
  26. package/plugins/textureSheet.js +496 -458
  27. package/plugins/threejs.js +183 -163
  28. package/plugins/tweakables.js +411 -0
  29. package/plugins/tweenSystem.js +263 -145
  30. package/plugins/uiSystem.js +2024 -1687
  31. package/plugins/zzfxm.js +159 -159
  32. package/src/engine.js +191 -75
  33. package/src/engineAudio.js +229 -125
  34. package/src/engineBuild.mjs +289 -256
  35. package/src/engineDebug.js +912 -742
  36. package/src/engineDraw.js +295 -193
  37. package/src/engineEditor.js +2431 -0
  38. package/src/engineExport.js +12 -0
  39. package/src/engineInput.js +1462 -1293
  40. package/src/engineMath.js +1274 -1252
  41. package/src/engineObject.js +194 -80
  42. package/src/engineParticles.js +583 -532
  43. package/src/engineRelease.js +67 -45
  44. package/src/engineSettings.js +77 -29
  45. package/src/engineTileLayer.js +995 -741
  46. package/src/engineUtilities.js +19 -13
  47. package/src/engineWebGL.js +239 -82
  48. package/src/jsconfig.json +17 -13
  49. package/plugins/box2d.wasm.js +0 -630
  50. package/plugins/box2d.wasm.wasm +0 -0
@@ -0,0 +1,740 @@
1
+ /**
2
+ * LittleJS glTF Plugin
3
+ * - Loads glTF 2.0 models: a .gltf with its .bin and images beside it, or a .glb with everything in one file
4
+ * - A model comes back as parts, one Mesh per primitive of every node placed by the node tree, each with its
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
9
+ * - Materials give a base color and texture and whether they blend; glass made with KHR_materials_transmission blends too,
10
+ * and a KHR_materials_unlit material comes in emissive, its own color with no shading
11
+ * - The base color texture reads the uv set its texCoord names, moved by KHR_texture_transform as gltfpack and
12
+ * Blender write it
13
+ * - An OPAQUE material, the default, ignores its texture's alpha as the format says: a texture only such materials
14
+ * use loads with its alpha set to 1, so the 3D pass cuts no holes in it; MASK always cuts at half, alphaCutoff
15
+ * is not read
16
+ * - Material and vertex colors are linear in glTF and are converted to sRGB at load, the space textures are in
17
+ * - glTF and LittleJS agree on the axes, y up and -z forward, on counter clockwise triangles and on uvs running down
18
+ * - Requires the Render3D plugin
19
+ * @namespace GLTF
20
+ * @example
21
+ * const model = await loadGLTF('ship.glb'); // in an async gameInit
22
+ * const ship = model.createObject(vec3(0, 1, 0)); // an object with a child per part, textures and all
23
+ * ship.play('fly'); // and its animation, by name or number
24
+ * new EngineObject3D(vec3(), model.mesh); // or the whole thing as one mesh, still
25
+ */
26
+
27
+ 'use strict';
28
+
29
+ ///////////////////////////////////////////////////////////////////////////////
30
+ /**
31
+ * GLTFPart - One primitive of a model, placed where its node put it
32
+ * @memberof GLTF
33
+ */
34
+ class GLTFPart
35
+ {
36
+ /** @param {string} name @param {Mesh} mesh @param {Color} color @param {TextureInfo|undefined} textureInfo @param {boolean} transparent */
37
+ constructor(name, mesh, color, textureInfo, transparent)
38
+ {
39
+ /** @property {string} - The node's name, or its mesh's */
40
+ this.name = name;
41
+ /** @property {Mesh} - The geometry in model space, the node transforms applied, with the vertex colors the file had;
42
+ * a node resting at scale 0 is applied at scale 1 there, so an animation can grow it from nothing */
43
+ this.mesh = mesh;
44
+ /** @property {Color} - The material's base color, to draw the mesh tinted with */
45
+ this.color = color;
46
+ /** @property {TextureInfo|undefined} - The material's base color texture, undefined without one or without WebGL
47
+ * @type {TextureInfo|undefined} */
48
+ this.textureInfo = textureInfo;
49
+ /** @property {boolean} - The material blends or is glass, so the part belongs in the transparent stage */
50
+ this.transparent = transparent;
51
+ /** @property {boolean} - Its texture's sampler asks for nearest filtering, hard edged pixels, as pixel art
52
+ * and voxel tools export; the object createObject makes draws it pixelated */
53
+ this.pixelated = false;
54
+ /** @property {number} - The node it came from, which an animation moves it with */
55
+ this.node = 0;
56
+ /** @property {boolean} - The material is unlit (KHR_materials_unlit), its own color with no shading; the object
57
+ * createObject makes draws it with emissive 1 */
58
+ this.unlit = false;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * GLTFAnimation - One animation of a model: keys that move, turn and scale its nodes over time
64
+ * - Play it through the GLTFObject that model.createObject makes
65
+ * @memberof GLTF
66
+ */
67
+ class GLTFAnimation
68
+ {
69
+ /** @param {string} name @param {Array<Object>} channels */
70
+ constructor(name, channels)
71
+ {
72
+ /** @property {string} - Its name in the file, or 'animation' and its number when it has none */
73
+ this.name = name;
74
+ /** @property {Array<Object>} - What it moves: for each, a node, which of its translation, rotation or scale,
75
+ * the key times and values, and how to go between keys, LINEAR, STEP or CUBICSPLINE
76
+ * @type {Array<Object>} */
77
+ this.channels = channels;
78
+ /** @property {number} - Length in seconds, the time of its last key
79
+ * @type {number} */
80
+ this.duration = channels.reduce((d, c)=> max(d, c.times[c.times.length - 1] || 0), 0);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * GLTFModel - A loaded model: its parts, and everything as one mesh
86
+ * @memberof GLTF
87
+ */
88
+ class GLTFModel
89
+ {
90
+ /** @param {Array<GLTFPart>} parts @param {Array<GLTFAnimation>} [animations] @param {Object} [nodeTree] */
91
+ constructor(parts, animations=[], nodeTree)
92
+ {
93
+ /** @property {Array<GLTFPart>} - One per primitive of every node that has a mesh */
94
+ this.parts = parts;
95
+ /** @property {Array<GLTFAnimation>} - The animations, play one through createObject's GLTFObject
96
+ * @type {Array<GLTFAnimation>} */
97
+ this.animations = animations;
98
+ this.nodeTree = nodeTree; // each node's parent and resting place, for animation
99
+ this.modelMatrix = new Matrix4; // what center, fit and transform did to the parts, animation works through it
100
+ /** @property {Mesh} - Every part combined, each tinted with its material color; the texture is textureInfo,
101
+ * and blending and unlit stay with the parts, which createObject draws */
102
+ this.mesh = new Mesh;
103
+ for (const part of parts) // a part baked at scale 1 from a node resting at 0 goes in as it rests
104
+ this.mesh.combine(part.mesh, nodeTree?.restPose?.[part.node] || RENDER3D_IDENTITY, part.color);
105
+ // the one texture every part uses, when they all do; a part without one has no uvs into it, so a model
106
+ // that mixes plain and textured parts, or uses several textures, is drawn through createObject instead
107
+ const textures = new Set(parts.map(p=> p.textureInfo));
108
+ /** @property {TextureInfo|undefined} - The texture to draw mesh with, when every part uses the same one
109
+ * @type {TextureInfo|undefined} */
110
+ this.textureInfo = textures.size === 1 ? textures.values().next().value : undefined;
111
+ /** @type {{min: Vector3, max: Vector3}|undefined} */
112
+ this.bounds = undefined; // the box around every part, kept once measured, see getBounds
113
+ }
114
+
115
+ /** The box around every part, measured once and again after transform, so change the model through that
116
+ * @return {{min: Vector3, max: Vector3}} */
117
+ getBounds()
118
+ {
119
+ const bounds = this.bounds ||= this.mesh.getBounds();
120
+ return { min: bounds.min.copy(), max: bounds.max.copy() }; // a copy, so a change to it leaves the model's
121
+ }
122
+
123
+ /** Move every part so the center of the model's bounds is on the origin, like Mesh.center
124
+ * @return {GLTFModel} */
125
+ center()
126
+ {
127
+ const bounds = this.getBounds();
128
+ return this.transform(bounds.min.add(bounds.max).scale(-.5));
129
+ }
130
+
131
+ /** Scale every part evenly so the model's largest extent is a size, like Mesh.fit, for models of unknown units
132
+ * @param {number} [size]
133
+ * @return {GLTFModel} */
134
+ fit(size=1)
135
+ {
136
+ const bounds = this.getBounds(), extent = bounds.max.subtract(bounds.min);
137
+ return this.transform(Matrix4.scaling(vec3(size / (max(extent.x, extent.y, extent.z) || 1))));
138
+ }
139
+
140
+ /** Move, turn or scale every part and the combined mesh together
141
+ * @param {Matrix4|Vector3} matrix - Transform, or just an offset to move by
142
+ * @return {GLTFModel} */
143
+ transform(matrix)
144
+ {
145
+ matrix = render3DMatrix(matrix);
146
+ for (const part of this.parts)
147
+ part.mesh.transform(matrix);
148
+ this.mesh.transform(matrix);
149
+ this.bounds = undefined; // measured again when next asked for
150
+ this.modelMatrix = matrix.copy().multiply(this.modelMatrix);
151
+ return this;
152
+ }
153
+
154
+ /** Find an animation by name or number
155
+ * @param {string|number|GLTFAnimation} animation
156
+ * @return {GLTFAnimation|undefined} */
157
+ getAnimation(animation)
158
+ {
159
+ return animation instanceof GLTFAnimation ? animation : isNumber(animation) ? this.animations[animation]
160
+ : this.animations.find(a=> a.name === animation);
161
+ }
162
+
163
+ /** How far each part has moved from its resting place at a time in an animation, one matrix per part
164
+ * - createObject's GLTFObject calls this as it plays, a game only needs it to pose something by hand
165
+ * @param {GLTFAnimation} animation
166
+ * @param {number} time - Seconds into it
167
+ * @return {Array<Matrix4>} */
168
+ getPose(animation, time)
169
+ {
170
+ const tree = this.nodeTree;
171
+ if (!tree) return this.parts.map(()=> new Matrix4);
172
+
173
+ // the nodes the animation moves get its values at this time, every other node keeps its own
174
+ const moved = new Map;
175
+ for (const channel of animation.channels)
176
+ {
177
+ let node = moved.get(channel.node);
178
+ node || moved.set(channel.node, node = gltfNodeTRS(tree.nodes[channel.node]));
179
+ gltfSample(channel, time, node[channel.path]);
180
+ }
181
+
182
+ // then each node's place in the model through its parents, and each part's move from where it rests,
183
+ // through what center and fit did: modelMatrix * now * rest inverse * modelMatrix inverse
184
+ const world = [];
185
+ const worldOf = (i)=>
186
+ {
187
+ if (world[i]) return world[i];
188
+ const local = gltfNodeMatrix(moved.get(i) || tree.nodes[i]), parent = tree.parents[i];
189
+ return world[i] = parent === undefined ? local : worldOf(parent).copy().multiply(local);
190
+ };
191
+ const model = this.modelMatrix, modelInverse = model.copy().invert();
192
+ return this.parts.map(part=> model.copy().multiply(worldOf(part.node)).multiply(tree.restInverse[part.node]).multiply(modelInverse));
193
+ }
194
+
195
+ /** Free the GPU buffers of every part's mesh and of the combined mesh, and the textures, for a model that is
196
+ * done with, like a level's models when the next level loads
197
+ * - Destroy the objects createObject made from it first; a mesh drawn again only uploads again, but a freed
198
+ * texture is gone */
199
+ dispose()
200
+ {
201
+ for (const part of this.parts)
202
+ part.mesh.dispose();
203
+ this.mesh.dispose();
204
+ for (const textureInfo of new Set(this.parts.map(p=> p.textureInfo)))
205
+ textureInfo?.destroyWebGLTexture();
206
+ }
207
+
208
+ /** Make an object at a position with a child per part, so each keeps its own texture, color and blending, and
209
+ * the model's animations can play on it; the way to show a model with windows or other see through parts,
210
+ * which the combined mesh draws solid, or one that moves
211
+ * @param {Vector3} [pos3D]
212
+ * @return {GLTFObject} - The root, move and turn it and the parts follow */
213
+ createObject(pos3D=vec3()) { return new GLTFObject(this, pos3D); }
214
+ }
215
+
216
+ ///////////////////////////////////////////////////////////////////////////////
217
+ /**
218
+ * GLTFObject - A model as an object with a child per part, which plays the model's animations
219
+ * - model.createObject makes one; move, turn and scale it like any EngineObject3D and the parts follow
220
+ * - play starts an animation by name or number, and the object moves its parts each frame as it runs
221
+ * - The parts' meshes stay where they rest, an animation moves the child objects that draw them
222
+ * @extends EngineObject3D
223
+ * @memberof GLTF
224
+ * @example
225
+ * const door = model.createObject(vec3(0, 0, 5));
226
+ * door.play('open', false); // once, holding the last pose
227
+ */
228
+ class GLTFObject extends EngineObject3D
229
+ {
230
+ /** Make the object and its parts, model.createObject is the usual way
231
+ * @param {GLTFModel} model
232
+ * @param {Vector3} [pos3D] */
233
+ constructor(model, pos3D=vec3())
234
+ {
235
+ super(pos3D);
236
+ // the size of the whole model, as an object made from model.mesh would have, measured once for the model
237
+ const bounds = model.mesh.points.length ? model.getBounds() : undefined;
238
+ if (bounds)
239
+ this.size3D = bounds.max.subtract(bounds.min);
240
+ /** @property {GLTFModel} - The model it shows */
241
+ this.model = model;
242
+ /** @property {GLTFAnimation|undefined} - The animation playing, or the last one, undefined for none
243
+ * @type {GLTFAnimation|undefined} */
244
+ this.animation = undefined;
245
+ /** @property {number} - Seconds into the animation */
246
+ this.animationTime = 0;
247
+ /** @property {number} - How fast it plays, 1 is as made, negative plays it backward */
248
+ this.animationSpeed = 1;
249
+ /** @property {boolean} - Start again at the end, or stop there and hold the last pose */
250
+ this.animationLoop = true;
251
+ /** @property {boolean} - Whether it is moving through the animation now */
252
+ this.animationPlaying = false;
253
+ /** @property {Array<EngineObject3D>} - The child that draws each of the model's parts, in the order of
254
+ * model.parts, which an animation poses; one destroyed or taken off the object is left alone
255
+ * @type {Array<EngineObject3D>} */
256
+ this.parts = [];
257
+ for (const part of model.parts)
258
+ {
259
+ const o = new EngineObject3D(vec3(), part.mesh, part.textureInfo, part.color);
260
+ o.transparent = part.transparent;
261
+ o.pixelated = part.pixelated;
262
+ o.emissive = part.unlit ? 1 : 0;
263
+ const rest = model.nodeTree?.restPose?.[part.node];
264
+ if (rest)
265
+ {
266
+ // baked at scale 1 from a node resting at 0, it starts as it rests, where a pose would put it
267
+ const modelMatrix = model.modelMatrix, m = modelMatrix.copy().multiply(rest).multiply(modelMatrix.copy().invert());
268
+ o.localMatrix = m; // whole, as a pose is
269
+ o.pos3D = m.getTranslation();
270
+ o.rotation3D = m.getRotation();
271
+ o.scale3D = m.getScale();
272
+ }
273
+ this.addChild(o);
274
+ this.parts.push(o);
275
+ }
276
+ }
277
+
278
+ /** Play an animation from its start
279
+ * @param {string|number|GLTFAnimation} [animation] - Its name, its number in model.animations, or itself
280
+ * @param {boolean} [loop] - Start again at the end, or stop there
281
+ * @param {number} [speed] - 1 is as made, negative plays it backward from its end */
282
+ play(animation=0, loop=true, speed=1)
283
+ {
284
+ const found = this.model.getAnimation(animation);
285
+ ASSERT(found, 'the model has no animation ' + animation, this.model.animations.map(a=> a.name));
286
+ if (!found) return;
287
+ this.animation = found;
288
+ this.animationLoop = loop;
289
+ this.animationSpeed = speed;
290
+ this.animationPlaying = true;
291
+ this.setAnimationTime(speed < 0 ? found.duration : 0);
292
+ }
293
+
294
+ /** Stop the animation where it is, the parts hold that pose */
295
+ stop() { this.animationPlaying = false; }
296
+
297
+ /** Put the parts where the animation has them at a time, playing or not
298
+ * @param {number} time - Seconds into the animation */
299
+ setAnimationTime(time)
300
+ {
301
+ this.animationTime = time;
302
+ if (!this.animation) return;
303
+ // its own list of the part objects, so a child removed or added does not hand a part another's pose
304
+ const pose = this.model.getPose(this.animation, time), parts = this.parts;
305
+ for (let i = 0; i < pose.length && i < parts.length; ++i)
306
+ {
307
+ const o = parts[i], m = pose[i];
308
+ if (o.destroyed || o.parent !== this) continue;
309
+ // drawn with the whole pose, which a parent's uneven scale can shear, the parts kept for what reads them
310
+ o.localMatrix = m;
311
+ o.pos3D = m.getTranslation();
312
+ o.rotation3D = m.getRotation();
313
+ o.scale3D = m.getScale();
314
+ }
315
+ }
316
+
317
+ /** Move through the animation, called automatically each frame */
318
+ update()
319
+ {
320
+ super.update();
321
+ const animation = this.animation;
322
+ if (!this.animationPlaying || !animation) return;
323
+ const duration = animation.duration;
324
+ let t = this.animationTime + timeDelta * this.animationSpeed;
325
+ if (this.animationLoop)
326
+ t = duration ? mod(t, duration) : 0;
327
+ else if (t >= duration || t <= 0)
328
+ {
329
+ // the end, or the start when playing backward: hold the last pose there
330
+ t = clamp(t, 0, duration);
331
+ this.animationPlaying = false;
332
+ }
333
+ this.setAnimationTime(t);
334
+ }
335
+ }
336
+
337
+ ///////////////////////////////////////////////////////////////////////////////
338
+
339
+ /** Load a glTF or GLB model, the .bin and images of a .gltf from beside it
340
+ * - A texture only OPAQUE materials use loads with its alpha set to 1, so the 3D pass cuts no holes in it
341
+ * @param {string} url
342
+ * @return {Promise<GLTFModel>}
343
+ * @memberof GLTF */
344
+ async function loadGLTF(url)
345
+ {
346
+ const response = await fetch(url);
347
+ if (!response.ok)
348
+ throw new Error('loadGLTF failed: ' + url);
349
+ return parseGLTF(await response.arrayBuffer(), url.slice(0, url.lastIndexOf('/') + 1));
350
+ }
351
+
352
+ /** Parse a model from GLB bytes or glTF JSON, fetching the buffers and images it refers to
353
+ * @param {ArrayBuffer|Object|string} data - GLB bytes, or the glTF JSON as bytes, text or an object
354
+ * @param {string} [baseUrl] - Where the .bin and image files are, with its trailing slash; loadGLTF passes the file's folder
355
+ * @return {Promise<GLTFModel>}
356
+ * @memberof GLTF */
357
+ async function parseGLTF(data, baseUrl='')
358
+ {
359
+ let json = data, glbBuffer;
360
+ if (data instanceof ArrayBuffer)
361
+ {
362
+ const view = new DataView(data);
363
+ if (data.byteLength >= 12 && view.getUint32(0, true) === 0x46546C67) // 'glTF', the binary container
364
+ {
365
+ if (view.getUint32(4, true) !== 2)
366
+ throw new Error('only GLB version 2 is read');
367
+ for (let offset = 12; offset + 8 <= data.byteLength;)
368
+ {
369
+ const length = view.getUint32(offset, true), type = view.getUint32(offset + 4, true);
370
+ const chunk = data.slice(offset + 8, offset + 8 + length);
371
+ if (type === 0x4E4F534A) json = new TextDecoder().decode(chunk); // JSON
372
+ else if (type === 0x004E4942) glbBuffer = chunk; // BIN
373
+ offset += 8 + length;
374
+ }
375
+ }
376
+ else
377
+ json = new TextDecoder().decode(data);
378
+ }
379
+ if (typeof json == 'string')
380
+ json = JSON.parse(json);
381
+ if (!json.asset || !String(json.asset.version).startsWith('2'))
382
+ throw new Error('only glTF 2.0 is read'); // a file problem, not a code one, so it throws in every build
383
+ // compressed geometry has no plain accessors to read; the other extensions add to what is here, and can be left out
384
+ for (const name of json.extensionsRequired || [])
385
+ if (name === 'KHR_draco_mesh_compression' || name === 'EXT_meshopt_compression')
386
+ throw new Error(`glTF with ${name} is not read, export it uncompressed`);
387
+
388
+ // the buffers: the GLB's own, a data uri, or a file beside the model
389
+ const buffers = await Promise.all((json.buffers || []).map((buffer, i)=>
390
+ {
391
+ if (!buffer.uri)
392
+ {
393
+ ASSERT(glbBuffer && !i, 'a buffer without a uri is the GLB chunk, and only the first can be');
394
+ return glbBuffer;
395
+ }
396
+ return gltfFetch(buffer.uri, baseUrl).then(r=> r.arrayBuffer());
397
+ }));
398
+
399
+ // the textures, decoded together first; none without WebGL, and a failed image only logs; only the base color
400
+ // textures are drawn with, so the normal, roughness and other maps are not loaded at all
401
+ const baseColorTextures = new Set((json.materials || []).map(m=> m.pbrMetallicRoughness?.baseColorTexture?.index));
402
+ const opaqueTextures = gltfOpaqueTextures(json);
403
+ const textures = await Promise.all((json.textures || []).map(async (texture, index)=>
404
+ {
405
+ if (!glContext || typeof createImageBitmap == 'undefined' || !baseColorTextures.has(index)) return;
406
+ try
407
+ {
408
+ // a WebP or AVIF image is named by its extension, the browser decodes those like any other
409
+ const source = texture.source ?? texture.extensions?.EXT_texture_webp?.source ?? texture.extensions?.EXT_texture_avif?.source;
410
+ const image = json.images[source], sampler = json.samplers?.[texture.sampler] || {};
411
+ let blob;
412
+ if (image.uri)
413
+ blob = await gltfFetch(image.uri, baseUrl).then(r=> r.blob());
414
+ else
415
+ {
416
+ const view = json.bufferViews[image.bufferView];
417
+ blob = new Blob([new Uint8Array(buffers[view.buffer], view.byteOffset || 0, view.byteLength)], {type: image.mimeType});
418
+ }
419
+ // an OPAQUE material ignores its texture's alpha, which the 3D pass would cut holes by, so a texture only
420
+ // those use has it set to 1; decoded as stored for that, and a jpeg has no alpha to set
421
+ const opaque = opaqueTextures.has(index) && blob.type !== 'image/jpeg';
422
+ const bitmap = opaque ? await createImageBitmap(blob, {premultiplyAlpha: 'none'}).then(gltfOpaqueImage) : await createImageBitmap(blob);
423
+ return new TextureInfo(bitmap, true, [sampler.wrapS ?? 10497, sampler.wrapT ?? 10497]); // REPEAT by default
424
+ }
425
+ catch (e) { LOG('glTF image not loaded', e); }
426
+ }));
427
+
428
+ // the parts: the scene's nodes walked with their transforms, every primitive of a node's mesh placed by it;
429
+ // each node's parent and resting place are kept, so an animation can move a part from where it rests
430
+ const parts = [], parents = [], restInverse = [], restPose = [];
431
+ const visit = (index, parentMatrix, parentIndex, parentRest)=>
432
+ {
433
+ const node = json.nodes[index], local = gltfNodeMatrix(node);
434
+ let matrix = parentMatrix ? parentMatrix.copy().multiply(local) : local;
435
+ let rest = parentRest && parentRest.copy().multiply(local); // where it rests, when that is not where it is baked
436
+ if (!node.matrix && node.scale?.includes(0))
437
+ {
438
+ // resting at scale 0, like a pop in exported at its first frame, would bake its parts onto a point and
439
+ // leave no inverse to pose them from; it and its children are baked at scale 1 on that axis instead,
440
+ // and a pose takes them back to 0, or up from there as an animation says
441
+ rest ||= matrix;
442
+ const unscaled = gltfNodeMatrix({...node, scale: node.scale.map(s=> s || 1)});
443
+ matrix = parentMatrix ? parentMatrix.copy().multiply(unscaled) : unscaled;
444
+ }
445
+ parents[index] = parentIndex;
446
+ restInverse[index] = matrix.copy().invert();
447
+ if (rest)
448
+ restPose[index] = rest.copy().multiply(restInverse[index]); // from where it is baked to where it rests
449
+ if (node.mesh !== undefined)
450
+ {
451
+ const mesh = json.meshes[node.mesh];
452
+ for (const primitive of mesh.primitives)
453
+ {
454
+ const part = gltfPart(json, buffers, textures, primitive, matrix, node.name || mesh.name || 'part ' + parts.length);
455
+ if (!part) continue;
456
+ part.node = index;
457
+ parts.push(part);
458
+ }
459
+ }
460
+ for (const child of node.children || [])
461
+ visit(child, matrix, index, rest);
462
+ };
463
+ let animations;
464
+ try
465
+ {
466
+ const scene = json.scenes?.[json.scene ?? 0];
467
+ if (scene)
468
+ (scene.nodes || []).forEach(i=> visit(i)); // a scene may be empty
469
+ else if (json.nodes)
470
+ {
471
+ // no scene: every node that is not another's child is a root
472
+ const children = new Set(json.nodes.flatMap(n=> n.children || []));
473
+ json.nodes.forEach((n, i)=> children.has(i) || visit(i));
474
+ }
475
+
476
+ // the animations: each channel that moves a node's translation, rotation or scale, with its keys; morph
477
+ // weights are not read, and a node given as a matrix cannot be animated, the format says
478
+ animations = (json.animations || []).map((animation, i)=> new GLTFAnimation(animation.name || 'animation ' + i,
479
+ animation.channels.filter(c=> c.target.node !== undefined &&
480
+ ['translation', 'rotation', 'scale'].includes(c.target.path) && !json.nodes[c.target.node].matrix).map(c=>
481
+ {
482
+ const sampler = animation.samplers[c.sampler];
483
+ return {node: c.target.node, path: c.target.path, interpolation: sampler.interpolation || 'LINEAR',
484
+ times: gltfAccessor(json, buffers, sampler.input).data,
485
+ values: gltfAccessor(json, buffers, sampler.output).data,
486
+ components: c.target.path === 'rotation' ? 4 : 3};
487
+ })));
488
+ }
489
+ catch (error)
490
+ {
491
+ // a file that fails partway leaves no model to dispose, so the textures it made go now
492
+ for (const texture of textures)
493
+ texture?.destroyWebGLTexture();
494
+ throw error;
495
+ }
496
+
497
+ // a texture none of the parts draws with, another scene's, goes now too, since dispose finds them by the parts
498
+ const used = new Set(parts.map(p=> p.textureInfo));
499
+ for (const texture of textures)
500
+ texture && !used.has(texture) && texture.destroyWebGLTexture();
501
+ return new GLTFModel(parts, animations, {nodes: json.nodes, parents, restInverse, restPose});
502
+ }
503
+
504
+ // a node's translation, rotation and scale as arrays to animate, copies so the file's stay as they rest
505
+ function gltfNodeTRS(node)
506
+ {
507
+ return {translation: [...(node.translation || [0, 0, 0])], rotation: [...(node.rotation || [0, 0, 0, 1])],
508
+ scale: [...(node.scale || [1, 1, 1])]};
509
+ }
510
+
511
+ // the base color textures that only OPAQUE materials use, the default mode, which the format says ignores alpha;
512
+ // one a MASK or BLEND material also uses keeps its alpha for that
513
+ function gltfOpaqueTextures(json)
514
+ {
515
+ const opaque = new Set, cut = new Set;
516
+ for (const material of json.materials || [])
517
+ {
518
+ const index = material.pbrMetallicRoughness?.baseColorTexture?.index;
519
+ if (index !== undefined)
520
+ (!material.alphaMode || material.alphaMode === 'OPAQUE' ? opaque : cut).add(index);
521
+ }
522
+ for (const index of cut)
523
+ opaque.delete(index);
524
+ return opaque;
525
+ }
526
+
527
+ // an image decoded without premultiplied alpha, given alpha 1 all over and its colors as they are; read back through
528
+ // a framebuffer, since a 2D canvas would multiply the colors by the alpha and lose those under a clear texel
529
+ function gltfOpaqueImage(image)
530
+ {
531
+ const gl = glContext, {width, height} = image;
532
+ if (gl.isContextLost()) return image; // nothing to read back through, it keeps its alpha
533
+ const texture = gl.createTexture(), framebuffer = gl.createFramebuffer(), bound = gl.getParameter(gl.FRAMEBUFFER_BINDING);
534
+ gl.bindTexture(gl.TEXTURE_2D, texture);
535
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
536
+ gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
537
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
538
+ const data = new Uint8ClampedArray(width * height * 4);
539
+ gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, data); // row 0 is the image's first row, as uploaded
540
+ gl.bindFramebuffer(gl.FRAMEBUFFER, bound);
541
+ gl.bindTexture(gl.TEXTURE_2D, glActiveTexture);
542
+ gl.deleteFramebuffer(framebuffer);
543
+ gl.deleteTexture(texture);
544
+ for (let i = 3; i < data.length; i += 4)
545
+ data[i] = 255;
546
+ image.close();
547
+ return createImageBitmap(new ImageData(data, width, height));
548
+ }
549
+
550
+ // write a channel's value at a time into out: its keys held before the first and after the last, stepped, straight
551
+ // between, or on the curve their tangents make; a rotation goes the short way round and stays a unit quaternion
552
+ function gltfSample(channel, time, out)
553
+ {
554
+ const {times, values, components: n, interpolation} = channel, last = times.length - 1;
555
+ const cubic = interpolation === 'CUBICSPLINE', stride = cubic ? 3 * n : n, at = cubic ? n : 0; // a cubic key is in, value, out
556
+ // the last key at or before the time, found by halving, since a baked animation has thousands of keys
557
+ let k = 0, high = last;
558
+ while (k < high)
559
+ {
560
+ const mid = k + high + 1 >> 1;
561
+ times[mid] <= time ? k = mid : high = mid - 1;
562
+ }
563
+ if (k >= last || time <= times[0] || interpolation === 'STEP')
564
+ {
565
+ for (let j = 0; j < n; ++j)
566
+ out[j] = values[k * stride + at + j];
567
+ return;
568
+ }
569
+ const dt = times[k + 1] - times[k], t = (time - times[k]) / dt, a = k * stride, b = a + stride;
570
+ if (cubic)
571
+ {
572
+ // hermite between the two values, with the out tangent of the first and the in tangent of the second
573
+ const t2 = t * t, t3 = t2 * t;
574
+ const h00 = 2*t3 - 3*t2 + 1, h10 = t3 - 2*t2 + t, h01 = -2*t3 + 3*t2, h11 = t3 - t2;
575
+ for (let j = 0; j < n; ++j)
576
+ out[j] = h00 * values[a + n + j] + h10 * dt * values[a + 2*n + j] + h01 * values[b + n + j] + h11 * dt * values[b + j];
577
+ }
578
+ else if (n === 4)
579
+ {
580
+ // a rotation turns along the arc between the two, the shorter way round
581
+ let dot = 0;
582
+ for (let j = 0; j < 4; ++j)
583
+ dot += values[a + j] * values[b + j];
584
+ const flip = dot < 0 ? -1 : 1;
585
+ dot *= flip;
586
+ let wa = 1 - t, wb = t * flip;
587
+ if (dot < .9995) // close together the straight line is as good and does not divide by nothing
588
+ {
589
+ const angle = Math.acos(dot), s = sin(angle);
590
+ wa = sin((1 - t) * angle) / s, wb = sin(t * angle) / s * flip;
591
+ }
592
+ for (let j = 0; j < 4; ++j)
593
+ out[j] = wa * values[a + j] + wb * values[b + j];
594
+ }
595
+ else
596
+ for (let j = 0; j < n; ++j)
597
+ out[j] = values[a + j] + (values[b + j] - values[a + j]) * t;
598
+ if (n === 4)
599
+ {
600
+ const l = hypot(out[0], out[1], out[2], out[3]) || 1;
601
+ for (let j = 0; j < 4; ++j)
602
+ out[j] /= l;
603
+ }
604
+ }
605
+
606
+ // fetch a uri beside the model, or decode a data uri without going out
607
+ function gltfFetch(uri, baseUrl)
608
+ {
609
+ if (uri.startsWith('data:'))
610
+ {
611
+ const comma = uri.indexOf(','), bytes = atob(uri.slice(comma + 1)), data = new Uint8Array(bytes.length);
612
+ for (let i = 0; i < bytes.length; ++i)
613
+ data[i] = bytes.charCodeAt(i);
614
+ return Promise.resolve(new Response(data.buffer, {headers: {'Content-Type': uri.slice(5, uri.indexOf(';'))}}));
615
+ }
616
+ // resolved as a link in the model is, from the model's folder: a leading slash from the site root, a leading
617
+ // // from the page's scheme, ../ up a folder; with no page to start from, a relative folder is only prefixed
618
+ const page = typeof location !== 'undefined' && location.href;
619
+ const base = page ? new URL(baseUrl, page) : /^[a-z][a-z0-9+.-]*:/i.test(baseUrl) ? baseUrl : undefined;
620
+ const url = base ? new URL(uri, base).href : baseUrl + uri;
621
+ return fetch(url).then(r=>
622
+ {
623
+ if (!r.ok) throw new Error('glTF file not found: ' + url);
624
+ return r;
625
+ });
626
+ }
627
+
628
+ // a node's local transform: its matrix, or translation, rotation quaternion and scale composed
629
+ function gltfNodeMatrix(node)
630
+ {
631
+ if (node.matrix)
632
+ return new Matrix4(node.matrix); // column major, like ours
633
+ const [x, y, z, w] = node.rotation || [0, 0, 0, 1], [sx, sy, sz] = node.scale || [1, 1, 1], t = node.translation || [0, 0, 0];
634
+ return new Matrix4([
635
+ (1 - 2*(y*y + z*z)) * sx, 2*(x*y + z*w) * sx, 2*(x*z - y*w) * sx, 0,
636
+ 2*(x*y - z*w) * sy, (1 - 2*(x*x + z*z)) * sy, 2*(y*z + x*w) * sy, 0,
637
+ 2*(x*z + y*w) * sz, 2*(y*z - x*w) * sz, (1 - 2*(x*x + y*y)) * sz, 0,
638
+ t[0], t[1], t[2], 1]);
639
+ }
640
+
641
+ // an accessor's values as floats, one row per element, normalized integer types brought to 0 to 1; a sparse one
642
+ // starts from its bufferView, or from zeros without one, and then has the listed elements replaced
643
+ function gltfAccessor(json, buffers, index)
644
+ {
645
+ const a = json.accessors[index], view = json.bufferViews?.[a.bufferView];
646
+ const components = {SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4, MAT4: 16}[a.type];
647
+ const types = {5120: Int8Array, 5121: Uint8Array, 5122: Int16Array, 5123: Uint16Array, 5125: Uint32Array, 5126: Float32Array};
648
+ const Type = types[a.componentType];
649
+ if (!components || !Type) // a file problem, so it throws in every build
650
+ throw new Error(`glTF accessor of ${a.type} ${a.componentType} is not read`);
651
+ const size = Type.BYTES_PER_ELEMENT;
652
+ const scales = /** @type {Array<[Object, number]>} */ ([[Int8Array, 127], [Uint8Array, 255], [Int16Array, 32767], [Uint16Array, 65535]]);
653
+ const scale = a.normalized ? new Map(scales).get(Type) || 1 : 1;
654
+ const out = new Float32Array(a.count * components);
655
+ if (view)
656
+ {
657
+ const buffer = buffers[view.buffer], offset = (view.byteOffset || 0) + (a.byteOffset || 0);
658
+ const stride = view.byteStride || components * size;
659
+ if (stride === components * size)
660
+ out.set(new Type(buffer, offset, a.count * components)); // packed, one view over all of it
661
+ else
662
+ for (let i = 0; i < a.count; ++i) // interleaved with other attributes, an element at each stride
663
+ out.set(new Type(buffer, offset + i * stride, components), i * components);
664
+ }
665
+ const sparse = a.sparse;
666
+ if (sparse)
667
+ {
668
+ // which elements change, and their new values packed in the accessor's own type
669
+ const {indices, values} = sparse, IndexType = types[indices.componentType];
670
+ const indexView = json.bufferViews?.[indices.bufferView], valueView = json.bufferViews?.[values.bufferView];
671
+ if (!IndexType || !indexView || !valueView)
672
+ throw new Error('glTF sparse accessor is missing its indices or values');
673
+ const at = new IndexType(buffers[indexView.buffer], (indexView.byteOffset || 0) + (indices.byteOffset || 0), sparse.count);
674
+ const data = new Type(buffers[valueView.buffer], (valueView.byteOffset || 0) + (values.byteOffset || 0), sparse.count * components);
675
+ for (let i = 0; i < sparse.count; ++i)
676
+ out.set(data.subarray(i * components, (i + 1) * components), at[i] * components);
677
+ }
678
+ if (scale !== 1)
679
+ for (let i = 0; i < out.length; ++i)
680
+ out[i] /= scale;
681
+ return {data: out, components, count: a.count};
682
+ }
683
+
684
+ // one primitive as a part: its attributes into a mesh, the indices as triangles, then placed and given its material
685
+ function gltfPart(json, buffers, textures, primitive, matrix, name)
686
+ {
687
+ const mode = primitive.mode ?? 4; // triangles
688
+ if (mode < 4)
689
+ {
690
+ LOG('glTF points and lines are not drawn:', name);
691
+ return;
692
+ }
693
+ const attributes = primitive.attributes;
694
+ ASSERT(attributes.POSITION !== undefined, 'a glTF primitive needs positions');
695
+ const read = (index, make)=>
696
+ {
697
+ const {data, components, count} = gltfAccessor(json, buffers, index), list = [];
698
+ for (let i = 0; i < count; ++i)
699
+ list.push(make(data, i * components, components));
700
+ return list;
701
+ };
702
+ const points = read(attributes.POSITION, (d, k)=> vec3(d[k], d[k+1], d[k+2]));
703
+ const normals = attributes.NORMAL !== undefined ? read(attributes.NORMAL, (d, k)=> vec3(d[k], d[k+1], d[k+2])) : undefined;
704
+ // the uv set the base color texture names, moved by its KHR_texture_transform once here, offset + rotation * scale
705
+ const material = json.materials?.[primitive.material] || {}, pbr = material.pbrMetallicRoughness || {};
706
+ const textureRef = pbr.baseColorTexture, uvTransform = textureRef?.extensions?.KHR_texture_transform;
707
+ const uvAccessor = attributes['TEXCOORD_' + (uvTransform?.texCoord ?? textureRef?.texCoord ?? 0)];
708
+ const [ox, oy] = uvTransform?.offset || [0, 0], [sx, sy] = uvTransform?.scale || [1, 1], r = uvTransform?.rotation || 0;
709
+ const c = cos(r), s = sin(r);
710
+ const uvs = uvAccessor !== undefined ? read(uvAccessor, (d, k)=>
711
+ vec2(c*sx*d[k] + s*sy*d[k+1] + ox, c*sy*d[k+1] - s*sx*d[k] + oy)) : undefined;
712
+ const colors = attributes.COLOR_0 !== undefined ? read(attributes.COLOR_0, (d, k, n)=>
713
+ rgb(gltfSRGB(d[k]), gltfSRGB(d[k+1]), gltfSRGB(d[k+2]), n > 3 ? d[k+3] : 1)) : undefined;
714
+ let indices = primitive.indices !== undefined ? Array.from(gltfAccessor(json, buffers, primitive.indices).data) : points.map((_, i)=> i);
715
+ if (mode === 5) // a strip: triangle i is the three entries up to i, the odd ones read the other way
716
+ indices = indices.flatMap((_, i, s)=> i < 2 ? [] : i & 1 ? [s[i-1], s[i-2], s[i]] : [s[i-2], s[i-1], s[i]]);
717
+ else if (mode === 6) // a fan around the first entry
718
+ indices = indices.flatMap((_, i, s)=> i < 2 ? [] : [s[0], s[i-1], s[i]]);
719
+ const mesh = new Mesh().addTriangles(points, indices, normals, uvs, colors);
720
+ normals || mesh.computeNormals(false); // flat when the file gives none, as the format says
721
+ mesh.transform(matrix);
722
+ const factor = pbr.baseColorFactor || [1, 1, 1, 1];
723
+ mesh.doubleSided = !!material.doubleSided;
724
+ // glass is usually made with transmission, an opaque white material the light passes through, which would
725
+ // draw solid white; it comes in blended instead, a faint tint of its color that lets the rest show through
726
+ // only a blending material reads its alpha, an opaque or masked one is solid whatever the factor says
727
+ const transmission = material.extensions?.KHR_materials_transmission?.transmissionFactor || 0;
728
+ const blend = material.alphaMode === 'BLEND', alpha = (blend ? factor[3] : 1) * (1 - .8 * transmission);
729
+ const texture = textureRef && json.textures?.[textureRef.index];
730
+ const part = new GLTFPart(name, mesh, rgb(gltfSRGB(factor[0]), gltfSRGB(factor[1]), gltfSRGB(factor[2]), alpha),
731
+ textureRef ? textures[textureRef.index] : undefined, blend || transmission > 0);
732
+ part.pixelated = json.samplers?.[texture?.sampler]?.magFilter === 9728; // NEAREST
733
+ part.unlit = !!material.extensions?.KHR_materials_unlit;
734
+ return part;
735
+ }
736
+
737
+ // a glTF color factor or vertex color is linear, where the renderer works in sRGB like the textures, so it is
738
+ // brought across at load or a mid gray material would come out nearly black; 0 and 1 stay exact
739
+ function gltfSRGB(c)
740
+ { return c <= .0031308 ? c * 12.92 : c >= 1 ? c : 1.055 * c ** (1 / 2.4) - .055; }