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