action-engine-js 1.0.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 (93) hide show
  1. package/LICENSE +45 -0
  2. package/README.md +348 -0
  3. package/actionengine/3rdparty/goblin/goblin.js +9609 -0
  4. package/actionengine/3rdparty/goblin/goblin.min.js +5 -0
  5. package/actionengine/camera/actioncamera.js +90 -0
  6. package/actionengine/camera/cameracollisionhandler.js +69 -0
  7. package/actionengine/character/actioncharacter.js +360 -0
  8. package/actionengine/character/actioncharacter3D.js +61 -0
  9. package/actionengine/core/app.js +430 -0
  10. package/actionengine/debug/basedebugpanel.js +858 -0
  11. package/actionengine/display/canvasmanager.js +75 -0
  12. package/actionengine/display/gl/programmanager.js +570 -0
  13. package/actionengine/display/gl/shaders/lineshader.js +118 -0
  14. package/actionengine/display/gl/shaders/objectshader.js +1756 -0
  15. package/actionengine/display/gl/shaders/particleshader.js +43 -0
  16. package/actionengine/display/gl/shaders/shadowshader.js +319 -0
  17. package/actionengine/display/gl/shaders/spriteshader.js +100 -0
  18. package/actionengine/display/gl/shaders/watershader.js +67 -0
  19. package/actionengine/display/graphics/actionmodel3D.js +191 -0
  20. package/actionengine/display/graphics/actionsprite3D.js +230 -0
  21. package/actionengine/display/graphics/lighting/actiondirectionalshadowlight.js +864 -0
  22. package/actionengine/display/graphics/lighting/actionlight.js +211 -0
  23. package/actionengine/display/graphics/lighting/actionomnidirectionalshadowlight.js +862 -0
  24. package/actionengine/display/graphics/lighting/lightingconstants.js +263 -0
  25. package/actionengine/display/graphics/lighting/lightmanager.js +789 -0
  26. package/actionengine/display/graphics/renderableobject.js +44 -0
  27. package/actionengine/display/graphics/renderers/actionrenderer2D.js +341 -0
  28. package/actionengine/display/graphics/renderers/actionrenderer3D/actionrenderer3D.js +655 -0
  29. package/actionengine/display/graphics/renderers/actionrenderer3D/canvasmanager3D.js +82 -0
  30. package/actionengine/display/graphics/renderers/actionrenderer3D/debugrenderer3D.js +493 -0
  31. package/actionengine/display/graphics/renderers/actionrenderer3D/objectrenderer3D.js +790 -0
  32. package/actionengine/display/graphics/renderers/actionrenderer3D/spriteRenderer3D.js +266 -0
  33. package/actionengine/display/graphics/renderers/actionrenderer3D/sunrenderer3D.js +140 -0
  34. package/actionengine/display/graphics/renderers/actionrenderer3D/waterrenderer3D.js +173 -0
  35. package/actionengine/display/graphics/renderers/actionrenderer3D/weatherrenderer3D.js +87 -0
  36. package/actionengine/display/graphics/texture/proceduraltexture.js +192 -0
  37. package/actionengine/display/graphics/texture/texturemanager.js +242 -0
  38. package/actionengine/display/graphics/texture/textureregistry.js +177 -0
  39. package/actionengine/input/actionscrollablearea.js +1405 -0
  40. package/actionengine/input/inputhandler.js +1647 -0
  41. package/actionengine/math/geometry/geometrybuilder.js +161 -0
  42. package/actionengine/math/geometry/glbexporter.js +364 -0
  43. package/actionengine/math/geometry/glbloader.js +722 -0
  44. package/actionengine/math/geometry/modelcodegenerator.js +97 -0
  45. package/actionengine/math/geometry/triangle.js +33 -0
  46. package/actionengine/math/geometry/triangleutils.js +34 -0
  47. package/actionengine/math/mathutils.js +25 -0
  48. package/actionengine/math/matrix4.js +785 -0
  49. package/actionengine/math/physics/actionphysics.js +108 -0
  50. package/actionengine/math/physics/actionphysicsobject3D.js +164 -0
  51. package/actionengine/math/physics/actionphysicsworld3D.js +238 -0
  52. package/actionengine/math/physics/actionraycast.js +129 -0
  53. package/actionengine/math/physics/shapes/actionphysicsbox3D.js +158 -0
  54. package/actionengine/math/physics/shapes/actionphysicscapsule3D.js +200 -0
  55. package/actionengine/math/physics/shapes/actionphysicscompoundshape3D.js +147 -0
  56. package/actionengine/math/physics/shapes/actionphysicscone3D.js +126 -0
  57. package/actionengine/math/physics/shapes/actionphysicsconvexshape3D.js +72 -0
  58. package/actionengine/math/physics/shapes/actionphysicscylinder3D.js +117 -0
  59. package/actionengine/math/physics/shapes/actionphysicsmesh3D.js +74 -0
  60. package/actionengine/math/physics/shapes/actionphysicsplane3D.js +100 -0
  61. package/actionengine/math/physics/shapes/actionphysicssphere3D.js +95 -0
  62. package/actionengine/math/quaternion.js +61 -0
  63. package/actionengine/math/vector2.js +277 -0
  64. package/actionengine/math/vector3.js +318 -0
  65. package/actionengine/math/viewfrustum.js +136 -0
  66. package/actionengine/network/ACTIONNETREADME.md +810 -0
  67. package/actionengine/network/client/ActionNetManager.js +802 -0
  68. package/actionengine/network/client/ActionNetManagerGUI.js +1709 -0
  69. package/actionengine/network/client/ActionNetManagerP2P.js +1537 -0
  70. package/actionengine/network/client/SyncSystem.js +422 -0
  71. package/actionengine/network/p2p/ActionNetPeer.js +142 -0
  72. package/actionengine/network/p2p/ActionNetTrackerClient.js +623 -0
  73. package/actionengine/network/p2p/DataConnection.js +282 -0
  74. package/actionengine/network/p2p/README.md +510 -0
  75. package/actionengine/network/p2p/example.html +502 -0
  76. package/actionengine/network/server/ActionNetServer.js +577 -0
  77. package/actionengine/network/server/ActionNetServerSSL.js +579 -0
  78. package/actionengine/network/server/ActionNetServerUtils.js +458 -0
  79. package/actionengine/network/server/SERVERREADME.md +314 -0
  80. package/actionengine/network/server/package-lock.json +35 -0
  81. package/actionengine/network/server/package.json +13 -0
  82. package/actionengine/network/server/start.bat +27 -0
  83. package/actionengine/network/server/start.sh +25 -0
  84. package/actionengine/network/server/startwss.bat +27 -0
  85. package/actionengine/sound/audiomanager.js +1589 -0
  86. package/actionengine/sound/soundfont/ACTIONSOUNDFONT_README.md +205 -0
  87. package/actionengine/sound/soundfont/actionparser.js +718 -0
  88. package/actionengine/sound/soundfont/actionreverb.js +252 -0
  89. package/actionengine/sound/soundfont/actionsoundfont.js +543 -0
  90. package/actionengine/sound/soundfont/sf2playerlicence.txt +29 -0
  91. package/actionengine/sound/soundfont/soundfont.js +2 -0
  92. package/dist/action-engine.min.js +328 -0
  93. package/package.json +35 -0
@@ -0,0 +1,722 @@
1
+ // actionengine/math/geometry/glbloader.js
2
+
3
+ /**
4
+ * GLBLoader handles loading and parsing of GLTF/GLB 3D model files.
5
+ * Supports skeletal animations, mesh data, and materials.
6
+ */
7
+ class GLBLoader {
8
+ /**
9
+ * Creates a new GLBLoader instance.
10
+ * Initializes empty arrays for storing model data.
11
+ */
12
+ constructor() {
13
+ this.nodes = [];
14
+ this.meshes = [];
15
+ this.skins = [];
16
+ this.animations = [];
17
+ this.triangles = [];
18
+ }
19
+
20
+ /**
21
+ * Loads a 3D model from either base64 string or ArrayBuffer input.
22
+ * @param {string|ArrayBuffer} input - The model data as either base64 string or ArrayBuffer
23
+ * @returns {GLBLoader} A loader instance containing the parsed model
24
+ * @throws {Error} If input format is not supported
25
+ */
26
+ static loadModel(input) {
27
+ if (typeof input === "string") {
28
+ return GLBLoader.loadFromBase64(input);
29
+ } else if (input instanceof ArrayBuffer) {
30
+ return GLBLoader.loadFromArrayBuffer(input);
31
+ } else {
32
+ throw new Error("Unsupported input format. Please provide a base64 string or ArrayBuffer.");
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Loads a 3D model from a base64 encoded string.
38
+ * @param {string} base64String - The model data encoded as base64
39
+ * @returns {GLBLoader} A loader instance containing the parsed model
40
+ * @private
41
+ */
42
+ static loadFromBase64(base64String) {
43
+ const binaryString = atob(base64String);
44
+ const bytes = new Uint8Array(binaryString.length);
45
+ for (let i = 0; i < binaryString.length; i++) {
46
+ bytes[i] = binaryString.charCodeAt(i);
47
+ }
48
+ return GLBLoader.loadFromArrayBuffer(bytes.buffer);
49
+ }
50
+
51
+ /**
52
+ * Loads a 3D model from an ArrayBuffer containing GLB data.
53
+ * Handles the complete loading process including node hierarchy,
54
+ * skins, meshes, and animations.
55
+ * @param {ArrayBuffer} arrayBuffer - The GLB file data
56
+ * @returns {GLBLoader} A loader instance containing the parsed model
57
+ * @private
58
+ */
59
+ static loadFromArrayBuffer(arrayBuffer) {
60
+ const model = new GLBLoader();
61
+ const { gltf, binaryData } = GLBLoader.parseGLB(arrayBuffer);
62
+ gltf.binaryData = binaryData;
63
+
64
+ // First create all nodes
65
+ if (gltf.nodes) {
66
+ model.nodes = gltf.nodes.map((node, i) => new Node(node, i));
67
+
68
+ // Then hook up node hierarchy
69
+ for (let i = 0; i < gltf.nodes.length; i++) {
70
+ const nodeData = gltf.nodes[i];
71
+ if (nodeData.children) {
72
+ // Convert child indices to actual node references
73
+ model.nodes[i].children = nodeData.children.map((childIndex) => model.nodes[childIndex]);
74
+ }
75
+ }
76
+ }
77
+
78
+ // Create skins after nodes exist
79
+ if (gltf.skins) {
80
+ model.skins = gltf.skins.map((skin, i) => new Skin(gltf, skin, i));
81
+
82
+ // Hook up skin references in nodes
83
+ for (const node of model.nodes) {
84
+ if (node.skin !== null) {
85
+ node.skin = model.skins[node.skin];
86
+ }
87
+ }
88
+ }
89
+
90
+ // Load meshes with skin data
91
+ GLBLoader.loadMeshes(model, gltf, binaryData);
92
+
93
+ // Finally load animations after everything else is set up
94
+ if (gltf.animations) {
95
+ model.animations = gltf.animations.map((anim) => new Animation(gltf, anim));
96
+ }
97
+
98
+ return model;
99
+ }
100
+
101
+ /**
102
+ * Parses a GLB format binary buffer into JSON and binary data chunks.
103
+ * @param {ArrayBuffer} arrayBuffer - The GLB file data
104
+ * @returns {{gltf: Object, binaryData: ArrayBuffer}} Parsed GLB containing JSON and binary chunks
105
+ * @throws {Error} If GLB file format is invalid
106
+ * @private
107
+ */
108
+ static parseGLB(arrayBuffer) {
109
+ const dataView = new DataView(arrayBuffer);
110
+ const magic = dataView.getUint32(0, true);
111
+ if (magic !== 0x46546c67) {
112
+ throw new Error("Invalid GLB file");
113
+ }
114
+
115
+ const jsonLength = dataView.getUint32(12, true);
116
+ const jsonText = new TextDecoder().decode(new Uint8Array(arrayBuffer, 20, jsonLength));
117
+ const json = JSON.parse(jsonText);
118
+ const binaryData = arrayBuffer.slice(20 + jsonLength + 8);
119
+
120
+ return { gltf: json, binaryData };
121
+ }
122
+
123
+ /**
124
+ * Processes mesh data from the GLTF JSON and creates triangle geometry.
125
+ * @param {GLBLoader} model - The loader instance to store processed mesh data
126
+ * @param {Object} gltf - The parsed GLTF JSON data
127
+ * @param {ArrayBuffer} binaryData - The binary buffer containing geometry data
128
+ * @private
129
+ */
130
+ static loadMeshes(model, gltf, binaryData) {
131
+ if (!gltf.meshes) return;
132
+
133
+ for (const mesh of gltf.meshes) {
134
+ const meshData = {
135
+ name: mesh.name || `mesh_${model.meshes.length}`,
136
+ primitives: []
137
+ };
138
+
139
+ for (const primitive of mesh.primitives) {
140
+ const primData = {
141
+ positions: GLBLoader.getAttributeData(primitive.attributes.POSITION, gltf, binaryData),
142
+ indices: GLBLoader.getIndexData(primitive.indices, gltf, binaryData),
143
+ joints: primitive.attributes.JOINTS_0
144
+ ? GLBLoader.getAttributeData(primitive.attributes.JOINTS_0, gltf, binaryData)
145
+ : null,
146
+ weights: primitive.attributes.WEIGHTS_0
147
+ ? GLBLoader.getAttributeData(primitive.attributes.WEIGHTS_0, gltf, binaryData)
148
+ : null,
149
+ material:
150
+ primitive.material !== undefined
151
+ ? GLBLoader.getMaterialColor(gltf.materials[primitive.material])
152
+ : null
153
+ };
154
+
155
+ GLBLoader.addPrimitiveTriangles(model, primData);
156
+ meshData.primitives.push(primData);
157
+ }
158
+
159
+ model.meshes.push(meshData);
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Extracts color information from a GLTF material.
165
+ * @param {Object} material - GLTF material data
166
+ * @returns {string|null} Hex color string or null if no color defined
167
+ * @private
168
+ */
169
+ static getMaterialColor(material) {
170
+ if (material?.pbrMetallicRoughness?.baseColorFactor) {
171
+ const [r, g, b] = material.pbrMetallicRoughness.baseColorFactor;
172
+ return `#${Math.floor(r * 255)
173
+ .toString(16)
174
+ .padStart(2, "0")}${Math.floor(g * 255)
175
+ .toString(16)
176
+ .padStart(2, "0")}${Math.floor(b * 255)
177
+ .toString(16)
178
+ .padStart(2, "0")}`;
179
+ }
180
+ return null;
181
+ }
182
+
183
+ /**
184
+ * Gets typed array data from a GLTF accessor.
185
+ * Handles different component types and creates appropriate typed arrays.
186
+ * @param {number} accessorIndex - Index of the accessor in GLTF accessors array
187
+ * @param {Object} gltf - The parsed GLTF JSON data
188
+ * @param {ArrayBuffer} binaryData - The binary buffer containing the actual data
189
+ * @returns {TypedArray} Data as appropriate TypedArray (Float32Array, Uint16Array, etc)
190
+ * @private
191
+ */
192
+ static getAttributeData(accessorIndex, gltf, binaryData) {
193
+ const accessor = gltf.accessors[accessorIndex];
194
+ const bufferView = gltf.bufferViews[accessor.bufferView];
195
+ const byteOffset = (bufferView.byteOffset || 0) + (accessor.byteOffset || 0);
196
+ const count = accessor.count;
197
+ const components = {
198
+ SCALAR: 1,
199
+ VEC2: 2,
200
+ VEC3: 3,
201
+ VEC4: 4,
202
+ MAT4: 16
203
+ }[accessor.type];
204
+
205
+ // Choose array type based on component type
206
+ let ArrayType = Float32Array;
207
+ if (accessor.componentType === 5121) {
208
+ // UNSIGNED_BYTE
209
+ ArrayType = Uint8Array;
210
+ } else if (accessor.componentType === 5123) {
211
+ // UNSIGNED_SHORT
212
+ ArrayType = Uint16Array;
213
+ } else if (accessor.componentType === 5125) {
214
+ // UNSIGNED_INT
215
+ ArrayType = Uint32Array;
216
+ }
217
+
218
+ return new ArrayType(
219
+ binaryData.slice(byteOffset, byteOffset + count * components * ArrayType.BYTES_PER_ELEMENT)
220
+ );
221
+ }
222
+
223
+ /**
224
+ * Gets index data from a GLTF accessor.
225
+ * Creates appropriate typed array for vertex indices.
226
+ * @param {number} accessorIndex - Index of the accessor in GLTF accessors array
227
+ * @param {Object} gltf - The parsed GLTF JSON data
228
+ * @param {ArrayBuffer} binaryData - The binary buffer containing the actual data
229
+ * @returns {TypedArray|null} Index data as Uint32Array or Uint16Array, or null if no indices
230
+ * @private
231
+ */
232
+ static getIndexData(accessorIndex, gltf, binaryData) {
233
+ if (accessorIndex === undefined) return null;
234
+
235
+ const accessor = gltf.accessors[accessorIndex];
236
+ const bufferView = gltf.bufferViews[accessor.bufferView];
237
+ const byteOffset = (bufferView.byteOffset || 0) + (accessor.byteOffset || 0);
238
+
239
+ return accessor.componentType === 5125
240
+ ? new Uint32Array(binaryData, byteOffset, accessor.count)
241
+ : new Uint16Array(binaryData, byteOffset, accessor.count);
242
+ }
243
+
244
+ /**
245
+ * Processes primitive data into triangles with vertex attributes.
246
+ * Creates triangle geometry with positions, joint weights, and material data.
247
+ * @param {GLBLoader} model - The loader instance to store processed triangles
248
+ * @param {Object} primitive - Primitive data containing positions, indices, joints, weights
249
+ * @private
250
+ */
251
+ static addPrimitiveTriangles(model, primitive) {
252
+ const { positions, indices, joints, weights, material } = primitive;
253
+
254
+ // First create all vertices
255
+ const vertexData = [];
256
+ for (let i = 0; i < positions.length / 3; i++) {
257
+ vertexData.push({
258
+ position: new Vector3(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]),
259
+ jointIndices: joints ? [joints[i * 4], joints[i * 4 + 1], joints[i * 4 + 2], joints[i * 4 + 3]] : null,
260
+ weights: weights ? [weights[i * 4], weights[i * 4 + 1], weights[i * 4 + 2], weights[i * 4 + 3]] : null
261
+ });
262
+ }
263
+
264
+ // Then create triangles using indices
265
+ for (let i = 0; i < indices.length; i += 3) {
266
+ const vertices = [vertexData[indices[i]], vertexData[indices[i + 1]], vertexData[indices[i + 2]]];
267
+
268
+ const triangle = new Triangle(
269
+ vertices[0].position,
270
+ vertices[1].position,
271
+ vertices[2].position,
272
+ material || "#FF0000"
273
+ );
274
+
275
+ if (joints && weights) {
276
+ triangle.jointData = vertices.map((v) => v.jointIndices);
277
+ triangle.weightData = vertices.map((v) => v.weights);
278
+ }
279
+
280
+ model.triangles.push(triangle);
281
+ }
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Represents a node in the GLTF scene graph hierarchy.
287
+ * Handles transformations, mesh references, and skeletal data.
288
+ */
289
+ class Node {
290
+ /**
291
+ * Creates a new Node from GLTF node data.
292
+ * @param {Object} nodeData - The GLTF node data
293
+ * @param {number} nodeID - Unique identifier for this node
294
+ * @param {string} [nodeData.name] - Optional name for the node
295
+ * @param {number[]} [nodeData.children] - Array of child node indices
296
+ * @param {number} [nodeData.mesh] - Index of associated mesh
297
+ * @param {number} [nodeData.skin] - Index of associated skin
298
+ * @param {number[]} [nodeData.translation] - Translation [x,y,z]
299
+ * @param {number[]} [nodeData.rotation] - Rotation quaternion [x,y,z,w]
300
+ * @param {number[]} [nodeData.scale] - Scale [x,y,z]
301
+ */
302
+ constructor(nodeData, nodeID) {
303
+ this.nodeID = nodeID;
304
+ this.name = nodeData.name || `node_${nodeID}`;
305
+
306
+ // Core node properties
307
+ /** @type {Node[]} Array of child nodes */
308
+ this.children = nodeData.children || [];
309
+ /** @type {number|null} Index of associated mesh */
310
+ this.mesh = nodeData.mesh !== undefined ? nodeData.mesh : null;
311
+ /** @type {number|null} Index of associated skin */
312
+ this.skin = nodeData.skin !== undefined ? nodeData.skin : null;
313
+
314
+ // Transform components
315
+ /** @type {Vector3} Node's position in local space */
316
+ this.translation = new Vector3(
317
+ nodeData.translation ? nodeData.translation[0] : 0,
318
+ nodeData.translation ? nodeData.translation[1] : 0,
319
+ nodeData.translation ? nodeData.translation[2] : 0
320
+ );
321
+
322
+ /** @type {Quaternion} Node's rotation in local space */
323
+ this.rotation = new Quaternion(
324
+ nodeData.rotation ? nodeData.rotation[0] : 0,
325
+ nodeData.rotation ? nodeData.rotation[1] : 0,
326
+ nodeData.rotation ? nodeData.rotation[2] : 0,
327
+ nodeData.rotation ? nodeData.rotation[3] : 1
328
+ );
329
+
330
+ /** @type {Vector3} Node's scale in local space */
331
+ this.scale = new Vector3(
332
+ nodeData.scale ? nodeData.scale[0] : 1,
333
+ nodeData.scale ? nodeData.scale[1] : 1,
334
+ nodeData.scale ? nodeData.scale[2] : 1
335
+ );
336
+
337
+ /** @type {Float32Array} Node's transformation matrix */
338
+ this.matrix = Matrix4.create();
339
+ this.updateMatrix();
340
+ }
341
+
342
+ /**
343
+ * Updates the node's world matrix by combining local transform with parent's world transform.
344
+ * Recursively updates all child nodes.
345
+ * @param {Float32Array|null} parentWorldMatrix - Parent node's world transform matrix
346
+ */
347
+ updateWorldMatrix(parentWorldMatrix = null) {
348
+ // First update local matrix
349
+ const tempMatrix = Matrix4.create();
350
+ Matrix4.fromRotationTranslation(tempMatrix, this.rotation, this.translation);
351
+ Matrix4.scale(this.matrix, tempMatrix, this.scale.toArray());
352
+
353
+ // If we have a parent, multiply by parent's world matrix
354
+ if (parentWorldMatrix) {
355
+ Matrix4.multiply(this.matrix, parentWorldMatrix, this.matrix);
356
+ }
357
+
358
+ // Update all children
359
+ for (const child of this.children) {
360
+ child.updateWorldMatrix(this.matrix);
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Updates the node's local transformation matrix from TRS components.
366
+ */
367
+ updateMatrix() {
368
+ // Create matrix from TRS components
369
+ const tempMatrix = Matrix4.create();
370
+ Matrix4.fromRotationTranslation(tempMatrix, this.rotation, this.translation);
371
+ Matrix4.scale(this.matrix, tempMatrix, this.scale.toArray());
372
+ }
373
+
374
+ /**
375
+ * Traverses the node hierarchy starting from this node.
376
+ * @param {Node|null} parent - Parent node for hierarchy traversal
377
+ * @param {Function} executeFunc - Function to execute on each node
378
+ */
379
+ traverse(parent, executeFunc) {
380
+ executeFunc(this, parent);
381
+ for (const childIndex of this.children) {
382
+ nodes[childIndex].traverse(this, executeFunc);
383
+ }
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Handles interpolation of keyframe data for animations.
389
+ * Supports linear interpolation for translations/scales and spherical interpolation for rotations.
390
+ */
391
+ class AnimationSampler {
392
+ /**
393
+ * Creates a new AnimationSampler from GLTF sampler data.
394
+ * @param {Object} gltf - The complete GLTF data object
395
+ * @param {Object} samplerData - The GLTF animation sampler data
396
+ * @param {number} samplerData.input - Accessor index for keyframe times
397
+ * @param {number} samplerData.output - Accessor index for keyframe values
398
+ * @param {string} [samplerData.interpolation='LINEAR'] - Interpolation method
399
+ */
400
+ constructor(gltf, samplerData) {
401
+ /** @type {Float32Array} Array of keyframe timestamps */
402
+ this.times = GLBLoader.getAttributeData(samplerData.input, gltf, gltf.binaryData);
403
+
404
+ /** @type {Float32Array} Array of keyframe values (translations, rotations, or scales) */
405
+ this.values = GLBLoader.getAttributeData(samplerData.output, gltf, gltf.binaryData);
406
+
407
+ /** @type {string} Interpolation method ('LINEAR' by default) */
408
+ this.interpolation = samplerData.interpolation || "LINEAR";
409
+
410
+ /** @type {number} Current keyframe index for playback */
411
+ this.currentIndex = 0;
412
+
413
+ /** @type {number} Total duration of this animation track in seconds */
414
+ this.duration = this.times[this.times.length - 1];
415
+
416
+ /** @type {number} Time offset for handling animation loops */
417
+ this.loopOffset = 0;
418
+ }
419
+
420
+ /**
421
+ * Gets the interpolated value at the specified time.
422
+ * Handles looping and different types of transform data.
423
+ * @param {number} t - Current time in seconds
424
+ * @returns {Vector3|Quaternion} Interpolated value (Vector3 for translation/scale, Quaternion for rotation)
425
+ */
426
+ getValue(t) {
427
+ // Wrap time to animation duration
428
+ t = t % this.duration;
429
+
430
+ // Reset for new loop if needed
431
+ if (t < this.times[this.currentIndex]) {
432
+ this.currentIndex = 0;
433
+ this.loopOffset = 0;
434
+ }
435
+
436
+ // Find appropriate keyframe pair
437
+ while (this.currentIndex < this.times.length - 1 && t >= this.times[this.currentIndex + 1]) {
438
+ this.currentIndex++;
439
+ }
440
+
441
+ // Loop back if we hit the end
442
+ if (this.currentIndex >= this.times.length - 1) {
443
+ this.currentIndex = 0;
444
+ }
445
+
446
+ // Calculate interpolation parameters
447
+ const t0 = this.times[this.currentIndex];
448
+ const t1 = this.times[this.currentIndex + 1];
449
+ const progress = (t - t0) / (t1 - t0);
450
+
451
+ // Get value indices based on component count
452
+ const i0 = (this.currentIndex * this.values.length) / this.times.length;
453
+ const i1 = i0 + this.values.length / this.times.length;
454
+
455
+ // Handle different transform types
456
+ if (this.values.length / this.times.length === 3) {
457
+ // Translation or scale (Vector3)
458
+ return new Vector3(
459
+ this.lerp(this.values[i0], this.values[i1], progress),
460
+ this.lerp(this.values[i0 + 1], this.values[i1 + 1], progress),
461
+ this.lerp(this.values[i0 + 2], this.values[i1 + 2], progress)
462
+ );
463
+ } else {
464
+ // Rotation (Quaternion)
465
+ const start = new Quaternion(
466
+ this.values[i0],
467
+ this.values[i0 + 1],
468
+ this.values[i0 + 2],
469
+ this.values[i0 + 3]
470
+ );
471
+ const end = new Quaternion(this.values[i1], this.values[i1 + 1], this.values[i1 + 2], this.values[i1 + 3]);
472
+ return start.slerp(end, progress);
473
+ }
474
+ }
475
+
476
+ /**
477
+ * Linear interpolation between two values.
478
+ * @param {number} a - Start value
479
+ * @param {number} b - End value
480
+ * @param {number} t - Interpolation factor (0-1)
481
+ * @returns {number} Interpolated value
482
+ * @private
483
+ */
484
+ lerp(a, b, t) {
485
+ return a + (b - a) * t;
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Represents an animation in the GLTF model.
491
+ * Handles playback of keyframe animations affecting node transforms.
492
+ */
493
+ class Animation {
494
+ /**
495
+ * Creates a new Animation from GLTF animation data.
496
+ * @param {Object} gltf - The complete GLTF data object
497
+ * @param {Object} animData - The GLTF animation data
498
+ * @param {string} [animData.name] - Optional name for the animation
499
+ * @param {Object[]} animData.samplers - Array of animation sampler data
500
+ * @param {Object[]} animData.channels - Array of animation channel data
501
+ * @param {Object} animData.channels[].target - Target information for the channel
502
+ * @param {number} animData.channels[].target.node - Index of target node
503
+ * @param {string} animData.channels[].target.path - Property to animate ('translation', 'rotation', or 'scale')
504
+ * @param {number} animData.channels[].sampler - Index of sampler to use
505
+ */
506
+ constructor(gltf, animData) {
507
+ /** @type {string} Name of the animation */
508
+ this.name = animData.name || "unnamed";
509
+
510
+ /**
511
+ * @type {AnimationSampler[]} Array of samplers that handle interpolation
512
+ * Each sampler manages keyframe data for a specific transform component
513
+ */
514
+ this.samplers = animData.samplers.map((s) => new AnimationSampler(gltf, s));
515
+
516
+ /**
517
+ * @type {Object[]} Array of channels that connect samplers to nodes
518
+ * Each channel maps a sampler to a specific node's transform property
519
+ */
520
+ this.channels = animData.channels.map((c) => ({
521
+ sampler: this.samplers[c.sampler],
522
+ targetNode: c.target.node,
523
+ targetPath: c.target.path
524
+ }));
525
+
526
+ /** @type {number} Total duration of the animation in seconds */
527
+ this.duration = Math.max(...this.samplers.map((s) => s.duration));
528
+ }
529
+
530
+ /**
531
+ * Updates the animation state at the given time.
532
+ * Applies interpolated transform values to nodes and updates the node hierarchy.
533
+ * @param {number} t - Current time in seconds
534
+ * @param {Node[]} nodes - Array of all nodes in the model
535
+ */
536
+ update(t, nodes) {
537
+ // Update each animation channel
538
+ for (const channel of this.channels) {
539
+ // Get interpolated value from sampler
540
+ const value = channel.sampler.getValue(t);
541
+ const node = nodes[channel.targetNode];
542
+
543
+ // Apply value to appropriate transform component
544
+ switch (channel.targetPath) {
545
+ case "translation":
546
+ node.translation = value;
547
+ break;
548
+ case "rotation":
549
+ node.rotation = value;
550
+ break;
551
+ case "scale":
552
+ node.scale = value;
553
+ break;
554
+ }
555
+ }
556
+
557
+ // Update world matrices starting from root nodes
558
+ for (const node of nodes) {
559
+ // Only process root nodes (nodes with no parents)
560
+ if (node.children.length > 0 && !nodes.some((n) => n.children.includes(node))) {
561
+ node.updateWorldMatrix();
562
+ }
563
+ }
564
+ }
565
+ }
566
+
567
+ /**
568
+ * Represents a skin (skeleton) in the GLTF model.
569
+ * Handles skeletal animation data and joint transformations.
570
+ */
571
+ class Skin {
572
+ /**
573
+ * Creates a new Skin from GLTF skin data.
574
+ * @param {Object} gltf - The complete GLTF data object
575
+ * @param {Object} skinData - The GLTF skin data
576
+ * @param {number[]} skinData.joints - Array of node indices representing joints
577
+ * @param {number} [skinData.inverseBindMatrices] - Accessor index for inverse bind matrices
578
+ * @param {number} skinID - Unique identifier for this skin
579
+ */
580
+ constructor(gltf, skinData, skinID) {
581
+ /** @type {number} Unique identifier for this skin */
582
+ this.skinID = skinID;
583
+
584
+ /** @type {number[]} Array of node indices representing joints in the skeleton */
585
+ this.joints = skinData.joints;
586
+
587
+ /**
588
+ * @type {Float32Array[]} Array of inverse bind matrices for each joint
589
+ * These transform vertices from model space to joint space
590
+ */
591
+ if (skinData.inverseBindMatrices !== undefined) {
592
+ const data = GLBLoader.getAttributeData(skinData.inverseBindMatrices, gltf, gltf.binaryData);
593
+ this.inverseBindMatrices = [];
594
+ // Each matrix is 16 floats (4x4)
595
+ for (let i = 0; i < data.length; i += 16) {
596
+ const matrix = Matrix4.create();
597
+ for (let j = 0; j < 16; j++) {
598
+ matrix[j] = data[i + j];
599
+ }
600
+ this.inverseBindMatrices.push(matrix);
601
+ }
602
+ } else {
603
+ // Default to identity matrices if none provided
604
+ this.inverseBindMatrices = this.joints.map(() => Matrix4.create());
605
+ }
606
+
607
+ /**
608
+ * @type {Float32Array[]} Array of joint matrices for runtime transform updates
609
+ * These store the final transforms used for vertex skinning
610
+ */
611
+ this.jointMatrices = new Array(this.joints.length);
612
+ for (let i = 0; i < this.jointMatrices.length; i++) {
613
+ this.jointMatrices[i] = Matrix4.create();
614
+ }
615
+ }
616
+
617
+ /**
618
+ * Updates joint matrices based on current node transforms.
619
+ * Combines joint world matrices with inverse bind matrices to get final vertex transforms.
620
+ * @param {Node[]} nodes - Array of all nodes in the model
621
+ */
622
+ update(nodes) {
623
+ for (let i = 0; i < this.joints.length; i++) {
624
+ const joint = nodes[this.joints[i]];
625
+ const invBind = this.inverseBindMatrices[i];
626
+ const jointMatrix = this.jointMatrices[i];
627
+
628
+ // Final transform = joint's world transform * inverse bind matrix
629
+ Matrix4.multiply(jointMatrix, joint.matrix, invBind);
630
+ }
631
+ }
632
+ }
633
+
634
+ class ModelAnimationController {
635
+ constructor(model) {
636
+ this.model = model;
637
+ this.currentAnimation = null;
638
+ this.currentTime = 0;
639
+ this.isPlaying = false;
640
+ this.isLooping = true;
641
+ this.startTime = 0;
642
+
643
+ // Create animation name map
644
+ this.animationMap = new Map();
645
+ this.model.animations.forEach((anim, index) => {
646
+ if (anim.name) {
647
+ this.animationMap.set(anim.name.toLowerCase(), index);
648
+ }
649
+ });
650
+ }
651
+
652
+ play(animation, shouldLoop = true) {
653
+ let animationIndex;
654
+ if (typeof animation === "string") {
655
+ animationIndex = this.animationMap.get(animation.toLowerCase());
656
+ if (animationIndex === undefined) {
657
+ console.warn(`Animation "${animation}" not found`);
658
+ return;
659
+ }
660
+ } else if (typeof animation === "number") {
661
+ if (animation >= 0 && animation < this.model.animations.length) {
662
+ animationIndex = animation;
663
+ } else {
664
+ console.warn(`Animation index ${animation} out of range`);
665
+ return;
666
+ }
667
+ }
668
+
669
+ // If the same animation is already playing, just update loop status
670
+ if (this.currentAnimation === this.model.animations[animationIndex]) {
671
+ this.isLooping = shouldLoop;
672
+ return;
673
+ }
674
+
675
+ this.currentAnimation = this.model.animations[animationIndex];
676
+ this.isPlaying = true;
677
+ this.isLooping = shouldLoop;
678
+ this.startTime = performance.now() / 1000;
679
+ this.currentTime = 0;
680
+ }
681
+
682
+ getAnimationNames() {
683
+ return Array.from(this.animationMap.keys());
684
+ }
685
+
686
+ pause() {
687
+ this.isPlaying = false;
688
+ }
689
+
690
+ resume() {
691
+ if (this.currentAnimation) {
692
+ this.isPlaying = true;
693
+ this.startTime = performance.now() / 1000 - this.currentTime;
694
+ }
695
+ }
696
+
697
+ stop() {
698
+ this.isPlaying = false;
699
+ this.currentTime = 0;
700
+ }
701
+
702
+ update() {
703
+ if (!this.isPlaying || !this.currentAnimation) return;
704
+
705
+ this.currentTime = performance.now() / 1000 - this.startTime;
706
+
707
+ if (this.currentTime > this.currentAnimation.duration) {
708
+ if (this.isLooping) {
709
+ this.startTime = performance.now() / 1000;
710
+ this.currentTime = 0;
711
+ } else {
712
+ this.isPlaying = false;
713
+ return;
714
+ }
715
+ }
716
+
717
+ this.currentAnimation.update(this.currentTime, this.model.nodes);
718
+ if (this.model.skins.length > 0) {
719
+ this.model.skins[0].update(this.model.nodes);
720
+ }
721
+ }
722
+ }