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.
- package/LICENSE +45 -0
- package/README.md +348 -0
- package/actionengine/3rdparty/goblin/goblin.js +9609 -0
- package/actionengine/3rdparty/goblin/goblin.min.js +5 -0
- package/actionengine/camera/actioncamera.js +90 -0
- package/actionengine/camera/cameracollisionhandler.js +69 -0
- package/actionengine/character/actioncharacter.js +360 -0
- package/actionengine/character/actioncharacter3D.js +61 -0
- package/actionengine/core/app.js +430 -0
- package/actionengine/debug/basedebugpanel.js +858 -0
- package/actionengine/display/canvasmanager.js +75 -0
- package/actionengine/display/gl/programmanager.js +570 -0
- package/actionengine/display/gl/shaders/lineshader.js +118 -0
- package/actionengine/display/gl/shaders/objectshader.js +1756 -0
- package/actionengine/display/gl/shaders/particleshader.js +43 -0
- package/actionengine/display/gl/shaders/shadowshader.js +319 -0
- package/actionengine/display/gl/shaders/spriteshader.js +100 -0
- package/actionengine/display/gl/shaders/watershader.js +67 -0
- package/actionengine/display/graphics/actionmodel3D.js +191 -0
- package/actionengine/display/graphics/actionsprite3D.js +230 -0
- package/actionengine/display/graphics/lighting/actiondirectionalshadowlight.js +864 -0
- package/actionengine/display/graphics/lighting/actionlight.js +211 -0
- package/actionengine/display/graphics/lighting/actionomnidirectionalshadowlight.js +862 -0
- package/actionengine/display/graphics/lighting/lightingconstants.js +263 -0
- package/actionengine/display/graphics/lighting/lightmanager.js +789 -0
- package/actionengine/display/graphics/renderableobject.js +44 -0
- package/actionengine/display/graphics/renderers/actionrenderer2D.js +341 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/actionrenderer3D.js +655 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/canvasmanager3D.js +82 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/debugrenderer3D.js +493 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/objectrenderer3D.js +790 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/spriteRenderer3D.js +266 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/sunrenderer3D.js +140 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/waterrenderer3D.js +173 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/weatherrenderer3D.js +87 -0
- package/actionengine/display/graphics/texture/proceduraltexture.js +192 -0
- package/actionengine/display/graphics/texture/texturemanager.js +242 -0
- package/actionengine/display/graphics/texture/textureregistry.js +177 -0
- package/actionengine/input/actionscrollablearea.js +1405 -0
- package/actionengine/input/inputhandler.js +1647 -0
- package/actionengine/math/geometry/geometrybuilder.js +161 -0
- package/actionengine/math/geometry/glbexporter.js +364 -0
- package/actionengine/math/geometry/glbloader.js +722 -0
- package/actionengine/math/geometry/modelcodegenerator.js +97 -0
- package/actionengine/math/geometry/triangle.js +33 -0
- package/actionengine/math/geometry/triangleutils.js +34 -0
- package/actionengine/math/mathutils.js +25 -0
- package/actionengine/math/matrix4.js +785 -0
- package/actionengine/math/physics/actionphysics.js +108 -0
- package/actionengine/math/physics/actionphysicsobject3D.js +164 -0
- package/actionengine/math/physics/actionphysicsworld3D.js +238 -0
- package/actionengine/math/physics/actionraycast.js +129 -0
- package/actionengine/math/physics/shapes/actionphysicsbox3D.js +158 -0
- package/actionengine/math/physics/shapes/actionphysicscapsule3D.js +200 -0
- package/actionengine/math/physics/shapes/actionphysicscompoundshape3D.js +147 -0
- package/actionengine/math/physics/shapes/actionphysicscone3D.js +126 -0
- package/actionengine/math/physics/shapes/actionphysicsconvexshape3D.js +72 -0
- package/actionengine/math/physics/shapes/actionphysicscylinder3D.js +117 -0
- package/actionengine/math/physics/shapes/actionphysicsmesh3D.js +74 -0
- package/actionengine/math/physics/shapes/actionphysicsplane3D.js +100 -0
- package/actionengine/math/physics/shapes/actionphysicssphere3D.js +95 -0
- package/actionengine/math/quaternion.js +61 -0
- package/actionengine/math/vector2.js +277 -0
- package/actionengine/math/vector3.js +318 -0
- package/actionengine/math/viewfrustum.js +136 -0
- package/actionengine/network/ACTIONNETREADME.md +810 -0
- package/actionengine/network/client/ActionNetManager.js +802 -0
- package/actionengine/network/client/ActionNetManagerGUI.js +1709 -0
- package/actionengine/network/client/ActionNetManagerP2P.js +1537 -0
- package/actionengine/network/client/SyncSystem.js +422 -0
- package/actionengine/network/p2p/ActionNetPeer.js +142 -0
- package/actionengine/network/p2p/ActionNetTrackerClient.js +623 -0
- package/actionengine/network/p2p/DataConnection.js +282 -0
- package/actionengine/network/p2p/README.md +510 -0
- package/actionengine/network/p2p/example.html +502 -0
- package/actionengine/network/server/ActionNetServer.js +577 -0
- package/actionengine/network/server/ActionNetServerSSL.js +579 -0
- package/actionengine/network/server/ActionNetServerUtils.js +458 -0
- package/actionengine/network/server/SERVERREADME.md +314 -0
- package/actionengine/network/server/package-lock.json +35 -0
- package/actionengine/network/server/package.json +13 -0
- package/actionengine/network/server/start.bat +27 -0
- package/actionengine/network/server/start.sh +25 -0
- package/actionengine/network/server/startwss.bat +27 -0
- package/actionengine/sound/audiomanager.js +1589 -0
- package/actionengine/sound/soundfont/ACTIONSOUNDFONT_README.md +205 -0
- package/actionengine/sound/soundfont/actionparser.js +718 -0
- package/actionengine/sound/soundfont/actionreverb.js +252 -0
- package/actionengine/sound/soundfont/actionsoundfont.js +543 -0
- package/actionengine/sound/soundfont/sf2playerlicence.txt +29 -0
- package/actionengine/sound/soundfont/soundfont.js +2 -0
- package/dist/action-engine.min.js +328 -0
- 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
|
+
}
|