@stowkit/three-loader 0.1.14 → 0.1.16
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/README.md +210 -479
- package/dist/MeshParser.d.ts.map +1 -1
- package/dist/StowKitLoader.d.ts.map +1 -1
- package/dist/StowKitPack.d.ts +55 -65
- package/dist/StowKitPack.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/stowkit-three-loader.esm.js +481 -192
- package/dist/stowkit-three-loader.esm.js.map +1 -1
- package/dist/stowkit-three-loader.js +484 -192
- package/dist/stowkit-three-loader.js.map +1 -1
- package/package.json +1 -1
|
@@ -47,16 +47,13 @@ class MeshParser {
|
|
|
47
47
|
if (metadataBlob.length < 140) {
|
|
48
48
|
throw new Error(`Metadata blob too small: ${metadataBlob.length} bytes (expected at least 140)`);
|
|
49
49
|
}
|
|
50
|
-
console.log(`Parsing mesh metadata: ${metadataBlob.length} bytes metadata, ${dataBlob.length} bytes data`);
|
|
51
50
|
const metadata = this.parseMeshMetadata(metadataBlob);
|
|
52
|
-
console.log(`Mesh counts: ${metadata.meshGeometryCount} geometries, ${metadata.materialCount} materials, ${metadata.nodeCount} nodes`);
|
|
53
51
|
// Check if the metadata blob contains just the header or all the mesh info
|
|
54
52
|
const expectedMetadataSize = 140 +
|
|
55
53
|
(metadata.meshGeometryCount * 40) + // Draco compressed (40 bytes each)
|
|
56
54
|
(metadata.materialCount * 196) + // MaterialData header
|
|
57
55
|
(metadata.nodeCount * 116);
|
|
58
56
|
let hasExtendedMetadata = metadataBlob.length >= expectedMetadataSize;
|
|
59
|
-
console.log(`Expected metadata size: ${expectedMetadataSize}, actual: ${metadataBlob.length}, has extended: ${hasExtendedMetadata}`);
|
|
60
57
|
// If metadata only has the header, the mesh info might be at the start of the data blob
|
|
61
58
|
const sourceBlob = hasExtendedMetadata ? metadataBlob : dataBlob;
|
|
62
59
|
const metaView = new DataView(sourceBlob.buffer, sourceBlob.byteOffset, sourceBlob.byteLength);
|
|
@@ -102,7 +99,6 @@ class MeshParser {
|
|
|
102
99
|
}
|
|
103
100
|
// Parse nodes first (they come before material properties in the blob)
|
|
104
101
|
const nodes = [];
|
|
105
|
-
console.log(`\n=== Nodes from StowKit file (${metadata.nodeCount} total) ===`);
|
|
106
102
|
for (let i = 0; i < metadata.nodeCount; i++) {
|
|
107
103
|
if (offset + 116 > sourceBlob.length) {
|
|
108
104
|
console.warn(`Data truncated at node ${i}/${metadata.nodeCount}`);
|
|
@@ -130,11 +126,9 @@ class MeshParser {
|
|
|
130
126
|
meshCount: metaView.getUint32(offset + 108, true),
|
|
131
127
|
firstMeshIndex: metaView.getUint32(offset + 112, true)
|
|
132
128
|
};
|
|
133
|
-
console.log(`Node ${i}: "${node.name}" (parent=${node.parentIndex}, meshCount=${node.meshCount}, firstMeshIndex=${node.firstMeshIndex})`);
|
|
134
129
|
nodes.push(node);
|
|
135
130
|
offset += 116; // Size of Node struct
|
|
136
131
|
}
|
|
137
|
-
console.log(`=== End of nodes from file ===\n`);
|
|
138
132
|
// Parse mesh indices
|
|
139
133
|
const totalMeshRefs = nodes.reduce((sum, node) => sum + node.meshCount, 0);
|
|
140
134
|
console.log(`Parsing ${totalMeshRefs} mesh indices at offset ${offset}`);
|
|
@@ -153,7 +147,6 @@ class MeshParser {
|
|
|
153
147
|
// MaterialPropertyValue: field_name[64] + value[4*4=16] + texture_id[64] = 144 bytes
|
|
154
148
|
for (let i = 0; i < materialData.length; i++) {
|
|
155
149
|
const matData = materialData[i];
|
|
156
|
-
console.log(`Material "${matData.name}": schema="${matData.schemaId}", ${matData.propertyCount} properties`);
|
|
157
150
|
for (let j = 0; j < matData.propertyCount; j++) {
|
|
158
151
|
if (offset + 144 > sourceBlob.length) {
|
|
159
152
|
console.warn(`Data truncated at property ${j}/${matData.propertyCount} of material ${i}`);
|
|
@@ -172,7 +165,6 @@ class MeshParser {
|
|
|
172
165
|
value,
|
|
173
166
|
textureId
|
|
174
167
|
});
|
|
175
|
-
console.log(` Property "${fieldName}": value=[${value.join(', ')}], textureId="${textureId}"`);
|
|
176
168
|
offset += 144; // Size of MaterialPropertyValue
|
|
177
169
|
}
|
|
178
170
|
// Create Three.js material based on properties
|
|
@@ -182,16 +174,13 @@ class MeshParser {
|
|
|
182
174
|
// Start with default gray color
|
|
183
175
|
color: new THREE__namespace.Color(0.8, 0.8, 0.8)
|
|
184
176
|
});
|
|
185
|
-
console.log(`Creating material ${i}: "${matData.name}", ${matData.propertyCount} properties`);
|
|
186
177
|
// Apply properties
|
|
187
178
|
for (const prop of matData.properties) {
|
|
188
179
|
if (prop.fieldName.toLowerCase() === 'tint' || prop.fieldName.toLowerCase().includes('color')) {
|
|
189
180
|
// Apply color/tint
|
|
190
181
|
mat.color = new THREE__namespace.Color(prop.value[0], prop.value[1], prop.value[2]);
|
|
191
|
-
console.log(` Applied tint color: rgb(${prop.value[0]}, ${prop.value[1]}, ${prop.value[2]})`);
|
|
192
182
|
}
|
|
193
183
|
}
|
|
194
|
-
console.log(` Material ${i} final color: rgb(${mat.color.r}, ${mat.color.g}, ${mat.color.b})`);
|
|
195
184
|
materials.push(mat);
|
|
196
185
|
}
|
|
197
186
|
// Default material if none exist
|
|
@@ -209,14 +198,6 @@ class MeshParser {
|
|
|
209
198
|
* Create Three.js BufferGeometry from Draco compressed mesh data
|
|
210
199
|
*/
|
|
211
200
|
static async createGeometry(geoInfo, dataBlob, dracoLoader) {
|
|
212
|
-
console.log('Decoding Draco geometry:', {
|
|
213
|
-
vertexCount: geoInfo.vertexCount,
|
|
214
|
-
indexCount: geoInfo.indexCount,
|
|
215
|
-
hasNormals: geoInfo.hasNormals,
|
|
216
|
-
hasUVs: geoInfo.hasUVs,
|
|
217
|
-
compressedOffset: geoInfo.compressedBufferOffset,
|
|
218
|
-
compressedSize: geoInfo.compressedBufferSize
|
|
219
|
-
});
|
|
220
201
|
// Extract the Draco compressed buffer
|
|
221
202
|
if (geoInfo.compressedBufferOffset + geoInfo.compressedBufferSize > dataBlob.length) {
|
|
222
203
|
throw new Error(`Compressed buffer out of bounds: offset=${geoInfo.compressedBufferOffset}, size=${geoInfo.compressedBufferSize}, dataLength=${dataBlob.length}`);
|
|
@@ -230,8 +211,7 @@ class MeshParser {
|
|
|
230
211
|
const geometry = await new Promise((resolve, reject) => {
|
|
231
212
|
dracoLoader.load(url, (decoded) => {
|
|
232
213
|
URL.revokeObjectURL(url);
|
|
233
|
-
|
|
234
|
-
// UVs are pre-flipped in the packer now, no need to flip here
|
|
214
|
+
// UVs are pre-flipped in the packer now
|
|
235
215
|
resolve(decoded);
|
|
236
216
|
}, undefined, (error) => {
|
|
237
217
|
URL.revokeObjectURL(url);
|
|
@@ -242,13 +222,6 @@ class MeshParser {
|
|
|
242
222
|
// Compute bounding volumes
|
|
243
223
|
geometry.computeBoundingSphere();
|
|
244
224
|
geometry.computeBoundingBox();
|
|
245
|
-
console.log('Final geometry:', {
|
|
246
|
-
hasPositions: !!geometry.attributes.position,
|
|
247
|
-
hasNormals: !!geometry.attributes.normal,
|
|
248
|
-
hasUVs: !!geometry.attributes.uv,
|
|
249
|
-
hasIndices: !!geometry.index,
|
|
250
|
-
vertexCount: geometry.attributes.position?.count || 0
|
|
251
|
-
});
|
|
252
225
|
return geometry;
|
|
253
226
|
}
|
|
254
227
|
/**
|
|
@@ -258,18 +231,11 @@ class MeshParser {
|
|
|
258
231
|
const root = new THREE__namespace.Group();
|
|
259
232
|
root.name = 'StowKitMesh';
|
|
260
233
|
const { geometries, materials, nodes, meshIndices } = parsedData;
|
|
261
|
-
console.log('Building scene with:', {
|
|
262
|
-
geometries: geometries.length,
|
|
263
|
-
materials: materials.length,
|
|
264
|
-
nodes: nodes.length,
|
|
265
|
-
meshIndices: meshIndices.length
|
|
266
|
-
});
|
|
267
234
|
// Create all Three.js objects for nodes
|
|
268
235
|
const nodeObjects = [];
|
|
269
236
|
for (const node of nodes) {
|
|
270
237
|
const obj = new THREE__namespace.Group();
|
|
271
238
|
obj.name = node.name;
|
|
272
|
-
console.log(`Node ${node.name}: position=${node.position}, rotation=${node.rotation}, scale=${node.scale}, meshCount=${node.meshCount}, firstMeshIndex=${node.firstMeshIndex}`);
|
|
273
239
|
// Apply transform
|
|
274
240
|
obj.position.set(...node.position);
|
|
275
241
|
obj.quaternion.set(...node.rotation);
|
|
@@ -278,10 +244,8 @@ class MeshParser {
|
|
|
278
244
|
for (let i = 0; i < node.meshCount; i++) {
|
|
279
245
|
const meshIndexArrayPos = node.firstMeshIndex + i;
|
|
280
246
|
const meshIndex = meshIndices[meshIndexArrayPos];
|
|
281
|
-
console.log(` meshIndices[${meshIndexArrayPos}] = ${meshIndex} (adding to node ${node.name})`);
|
|
282
247
|
if (meshIndex < geometries.length) {
|
|
283
248
|
const geoInfo = geometries[meshIndex];
|
|
284
|
-
console.log(` Creating geometry from geoInfo: vertices=${geoInfo.vertexCount}, indices=${geoInfo.indexCount}, material=${geoInfo.materialIndex}`);
|
|
285
249
|
const geometry = await this.createGeometry(geoInfo, dataBlob, dracoLoader);
|
|
286
250
|
// Use assigned material if valid, otherwise create default
|
|
287
251
|
let material;
|
|
@@ -299,35 +263,28 @@ class MeshParser {
|
|
|
299
263
|
}
|
|
300
264
|
const mesh = new THREE__namespace.Mesh(geometry, material);
|
|
301
265
|
obj.add(mesh);
|
|
302
|
-
console.log(` ✓ Added mesh to node ${node.name} (${geometry.attributes.position?.count || 0} vertices, material: ${material.name || 'default'})`);
|
|
303
266
|
}
|
|
304
267
|
else {
|
|
305
|
-
console.error(`
|
|
268
|
+
console.error(`Mesh index ${meshIndex} out of bounds (${geometries.length} geometries available)`);
|
|
306
269
|
}
|
|
307
270
|
}
|
|
308
|
-
console.log(` Node ${node.name} now has ${obj.children.length} children`);
|
|
309
271
|
nodeObjects.push(obj);
|
|
310
272
|
}
|
|
311
273
|
// Build hierarchy
|
|
312
|
-
console.log('Building node hierarchy...');
|
|
313
274
|
for (let i = 0; i < nodes.length; i++) {
|
|
314
275
|
const node = nodes[i];
|
|
315
276
|
const obj = nodeObjects[i];
|
|
316
277
|
if (node.parentIndex >= 0 && node.parentIndex < nodeObjects.length) {
|
|
317
|
-
console.log(` Adding node "${node.name}" (index ${i}) as child of node "${nodes[node.parentIndex].name}" (index ${node.parentIndex})`);
|
|
318
278
|
nodeObjects[node.parentIndex].add(obj);
|
|
319
279
|
}
|
|
320
280
|
else {
|
|
321
|
-
console.log(` Adding node "${node.name}" (index ${i}) as direct child of root (parentIndex=${node.parentIndex})`);
|
|
322
281
|
root.add(obj);
|
|
323
282
|
}
|
|
324
283
|
}
|
|
325
284
|
// If no nodes, create a single mesh from all geometries
|
|
326
285
|
if (nodes.length === 0 && geometries.length > 0) {
|
|
327
|
-
console.log('No nodes found, creating meshes directly from geometries');
|
|
328
286
|
for (let index = 0; index < geometries.length; index++) {
|
|
329
287
|
const geoInfo = geometries[index];
|
|
330
|
-
console.log(`Creating direct mesh ${index}: materialIndex=${geoInfo.materialIndex}, vertices=${geoInfo.vertexCount}`);
|
|
331
288
|
const geometry = await this.createGeometry(geoInfo, dataBlob, dracoLoader);
|
|
332
289
|
// Use assigned material if valid, otherwise create default
|
|
333
290
|
let material;
|
|
@@ -346,33 +303,8 @@ class MeshParser {
|
|
|
346
303
|
const mesh = new THREE__namespace.Mesh(geometry, material);
|
|
347
304
|
mesh.name = `Mesh_${index}`;
|
|
348
305
|
root.add(mesh);
|
|
349
|
-
console.log(`Added mesh ${index} to root`);
|
|
350
306
|
}
|
|
351
307
|
}
|
|
352
|
-
console.log(`Final scene has ${root.children.length} direct children`);
|
|
353
|
-
// Debug: traverse and count all meshes, showing full hierarchy
|
|
354
|
-
let totalMeshCount = 0;
|
|
355
|
-
let hierarchyLog = 'Scene hierarchy:\n';
|
|
356
|
-
root.traverse((obj) => {
|
|
357
|
-
// Calculate depth by counting parents
|
|
358
|
-
let depth = 0;
|
|
359
|
-
let parent = obj.parent;
|
|
360
|
-
while (parent && parent !== root) {
|
|
361
|
-
depth++;
|
|
362
|
-
parent = parent.parent;
|
|
363
|
-
}
|
|
364
|
-
const indent = ' '.repeat(depth);
|
|
365
|
-
if (obj instanceof THREE__namespace.Mesh) {
|
|
366
|
-
totalMeshCount++;
|
|
367
|
-
const mat = obj.material;
|
|
368
|
-
hierarchyLog += `${indent}[Mesh] vertices=${obj.geometry.attributes.position?.count || 0}, material=${mat.name}, hasTexture=${!!mat.map}\n`;
|
|
369
|
-
}
|
|
370
|
-
else if (obj instanceof THREE__namespace.Group && obj !== root) {
|
|
371
|
-
hierarchyLog += `${indent}[Group] ${obj.name} (${obj.children.length} children)\n`;
|
|
372
|
-
}
|
|
373
|
-
});
|
|
374
|
-
console.log(hierarchyLog);
|
|
375
|
-
console.log(`Total meshes in scene: ${totalMeshCount}`);
|
|
376
308
|
return root;
|
|
377
309
|
}
|
|
378
310
|
/**
|
|
@@ -395,6 +327,317 @@ class StowKitPack {
|
|
|
395
327
|
this.ktx2Loader = ktx2Loader;
|
|
396
328
|
this.dracoLoader = dracoLoader;
|
|
397
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* Load a skinned mesh by its string ID
|
|
332
|
+
*/
|
|
333
|
+
async loadSkinnedMesh(stringId) {
|
|
334
|
+
const assetIndex = this.reader.findAssetByPath(stringId);
|
|
335
|
+
if (assetIndex < 0) {
|
|
336
|
+
throw new Error(`Skinned mesh not found: ${stringId}`);
|
|
337
|
+
}
|
|
338
|
+
const data = this.reader.readAssetData(assetIndex);
|
|
339
|
+
const metadata = this.reader.readAssetMetadata(assetIndex);
|
|
340
|
+
if (!data)
|
|
341
|
+
throw new Error(`Failed to read skinned mesh data: ${stringId}`);
|
|
342
|
+
if (!metadata)
|
|
343
|
+
throw new Error(`No metadata for skinned mesh: ${stringId}`);
|
|
344
|
+
return await this.parseSkinnedMesh(metadata, data);
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Parse skinned mesh from binary data
|
|
348
|
+
*/
|
|
349
|
+
async parseSkinnedMesh(metadata, data) {
|
|
350
|
+
// Parse skinned mesh metadata
|
|
351
|
+
const view = new DataView(metadata.buffer, metadata.byteOffset, metadata.byteLength);
|
|
352
|
+
const decoder = new TextDecoder();
|
|
353
|
+
// SkinnedMeshMetadata: 4 uint32s (16 bytes) THEN string_id[128] = 144 bytes total
|
|
354
|
+
const meshGeometryCount = view.getUint32(0, true);
|
|
355
|
+
const materialCount = view.getUint32(4, true);
|
|
356
|
+
const nodeCount = view.getUint32(8, true);
|
|
357
|
+
const boneCount = view.getUint32(12, true);
|
|
358
|
+
const stringIdBytes = metadata.slice(16, 144);
|
|
359
|
+
const stringIdNullIndex = stringIdBytes.indexOf(0);
|
|
360
|
+
const stringId = decoder
|
|
361
|
+
.decode(stringIdBytes.slice(0, stringIdNullIndex >= 0 ? stringIdNullIndex : 128))
|
|
362
|
+
.trim();
|
|
363
|
+
let offset = 144;
|
|
364
|
+
// Parse all geometry infos
|
|
365
|
+
const geometryInfos = [];
|
|
366
|
+
// SkinnedMeshGeometryInfo: 4 uint32s + 6 uint64s + 2 uint32s = 16 + 48 + 8 = 72 bytes
|
|
367
|
+
// Layout: vertex_count, index_count, has_normals, has_uvs (16 bytes)
|
|
368
|
+
// vertex_buffer_offset, vertex_buffer_size, index_buffer_offset, index_buffer_size, weights_offset, weights_size (48 bytes)
|
|
369
|
+
// material_index, _padding (8 bytes)
|
|
370
|
+
for (let g = 0; g < meshGeometryCount; g++) {
|
|
371
|
+
if (offset + 72 > metadata.length) {
|
|
372
|
+
throw new Error(`Metadata too small for geometry ${g}`);
|
|
373
|
+
}
|
|
374
|
+
geometryInfos.push({
|
|
375
|
+
vertexCount: view.getUint32(offset, true),
|
|
376
|
+
indexCount: view.getUint32(offset + 4, true),
|
|
377
|
+
hasNormals: view.getUint32(offset + 8, true),
|
|
378
|
+
hasUVs: view.getUint32(offset + 12, true),
|
|
379
|
+
vertexBufferOffset: Number(view.getBigUint64(offset + 16, true)),
|
|
380
|
+
vertexBufferSize: Number(view.getBigUint64(offset + 24, true)),
|
|
381
|
+
indexBufferOffset: Number(view.getBigUint64(offset + 32, true)),
|
|
382
|
+
indexBufferSize: Number(view.getBigUint64(offset + 40, true)),
|
|
383
|
+
weightsOffset: Number(view.getBigUint64(offset + 48, true)),
|
|
384
|
+
weightsSize: Number(view.getBigUint64(offset + 56, true)),
|
|
385
|
+
materialIndex: view.getUint32(offset + 64, true)
|
|
386
|
+
// _padding at offset + 68 (ignored)
|
|
387
|
+
});
|
|
388
|
+
offset += 72;
|
|
389
|
+
}
|
|
390
|
+
const dataView = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
391
|
+
// Sort geometry infos by vertex buffer offset to ensure sequential parsing
|
|
392
|
+
const sortedGeometryInfos = geometryInfos.slice().sort((a, b) => a.vertexBufferOffset - b.vertexBufferOffset);
|
|
393
|
+
// Parse materials from metadata (immediately after geometries)
|
|
394
|
+
let metadataOffset = 144 + meshGeometryCount * 72;
|
|
395
|
+
const materialData = [];
|
|
396
|
+
// Parse MaterialData headers
|
|
397
|
+
for (let i = 0; i < materialCount; i++) {
|
|
398
|
+
if (metadataOffset + 196 > metadata.length) {
|
|
399
|
+
console.warn('Skinned mesh metadata truncated while reading material headers');
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
const nameBytes = metadata.slice(metadataOffset, metadataOffset + 64);
|
|
403
|
+
const schemaIdBytes = metadata.slice(metadataOffset + 64, metadataOffset + 192);
|
|
404
|
+
const name = decoder.decode(nameBytes.slice(0, nameBytes.indexOf(0) || 64));
|
|
405
|
+
const schemaId = decoder.decode(schemaIdBytes.slice(0, schemaIdBytes.indexOf(0) || 128));
|
|
406
|
+
const propertyCount = view.getUint32(metadataOffset + 192, true);
|
|
407
|
+
materialData.push({ name, schemaId, propertyCount, properties: [] });
|
|
408
|
+
metadataOffset += 196;
|
|
409
|
+
}
|
|
410
|
+
// Skip Node array and node_mesh_indices (not needed for rendering)
|
|
411
|
+
// Node struct: transform[16 floats] + name[64 bytes] + mesh_index[4 bytes] + parent_index[4 bytes] = 116 bytes
|
|
412
|
+
const NODE_STRIDE = 116;
|
|
413
|
+
metadataOffset += nodeCount * NODE_STRIDE;
|
|
414
|
+
metadataOffset += nodeCount * 4;
|
|
415
|
+
// Parse material properties
|
|
416
|
+
const PROPERTY_STRIDE = 144;
|
|
417
|
+
for (let i = 0; i < materialCount; i++) {
|
|
418
|
+
const mat = materialData[i];
|
|
419
|
+
for (let p = 0; p < mat.propertyCount; p++) {
|
|
420
|
+
try {
|
|
421
|
+
if (metadataOffset + PROPERTY_STRIDE > metadata.length) {
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
const fieldNameBytes = metadata.slice(metadataOffset, metadataOffset + 64);
|
|
425
|
+
const fieldName = decoder
|
|
426
|
+
.decode(fieldNameBytes.slice(0, fieldNameBytes.indexOf(0) || 64))
|
|
427
|
+
.trim();
|
|
428
|
+
const value = [
|
|
429
|
+
view.getFloat32(metadataOffset + 64, true),
|
|
430
|
+
view.getFloat32(metadataOffset + 68, true),
|
|
431
|
+
view.getFloat32(metadataOffset + 72, true),
|
|
432
|
+
view.getFloat32(metadataOffset + 76, true)
|
|
433
|
+
];
|
|
434
|
+
const textureIdBytes = metadata.slice(metadataOffset + 80, metadataOffset + 144);
|
|
435
|
+
const textureIdNullIndex = textureIdBytes.indexOf(0);
|
|
436
|
+
const textureId = (textureIdNullIndex >= 0
|
|
437
|
+
? decoder.decode(textureIdBytes.slice(0, textureIdNullIndex))
|
|
438
|
+
: decoder.decode(textureIdBytes))
|
|
439
|
+
.trim();
|
|
440
|
+
mat.properties.push({ fieldName, value, textureId });
|
|
441
|
+
metadataOffset += PROPERTY_STRIDE;
|
|
442
|
+
}
|
|
443
|
+
catch (err) {
|
|
444
|
+
throw err;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
// Parse bones
|
|
449
|
+
const BONE_STRIDE = 64 + 4 + 16 * 4;
|
|
450
|
+
const bones = [];
|
|
451
|
+
const parentIndices = [];
|
|
452
|
+
const boneMatrices = [];
|
|
453
|
+
const boneOriginalNames = [];
|
|
454
|
+
const bindPoses = [];
|
|
455
|
+
for (let i = 0; i < boneCount; i++) {
|
|
456
|
+
if (metadataOffset + BONE_STRIDE > metadata.length) {
|
|
457
|
+
throw new Error(`Skinned mesh metadata truncated while reading bone ${i}`);
|
|
458
|
+
}
|
|
459
|
+
const nameBytes = new Uint8Array(metadata.buffer, metadata.byteOffset + metadataOffset, 64);
|
|
460
|
+
const nullIndex = nameBytes.indexOf(0);
|
|
461
|
+
const rawName = decoder.decode(nameBytes.subarray(0, nullIndex >= 0 ? nullIndex : 64)).trim();
|
|
462
|
+
const parentIndex = view.getInt32(metadataOffset + 64, true);
|
|
463
|
+
const offsetMatrix = new THREE__namespace.Matrix4();
|
|
464
|
+
const matrixElements = new Float32Array(16);
|
|
465
|
+
for (let j = 0; j < 16; j++) {
|
|
466
|
+
matrixElements[j] = view.getFloat32(metadataOffset + 68 + j * 4, true);
|
|
467
|
+
}
|
|
468
|
+
offsetMatrix.fromArray(matrixElements);
|
|
469
|
+
const sanitizedName = this.sanitizeTrackName(rawName);
|
|
470
|
+
const bone = new THREE__namespace.Bone();
|
|
471
|
+
bone.name = sanitizedName;
|
|
472
|
+
bone.userData.originalName = rawName;
|
|
473
|
+
bones.push(bone);
|
|
474
|
+
parentIndices.push(parentIndex);
|
|
475
|
+
boneMatrices.push(offsetMatrix);
|
|
476
|
+
boneOriginalNames.push(rawName);
|
|
477
|
+
bindPoses.push(offsetMatrix.clone().invert());
|
|
478
|
+
metadataOffset += BONE_STRIDE;
|
|
479
|
+
}
|
|
480
|
+
// Build bone hierarchy
|
|
481
|
+
for (let i = 0; i < bones.length; i++) {
|
|
482
|
+
const parentIndex = parentIndices[i];
|
|
483
|
+
if (parentIndex >= 0 && parentIndex < bones.length && parentIndex !== i) {
|
|
484
|
+
bones[parentIndex].add(bones[i]);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
// Set bones to bind pose for correct static display
|
|
488
|
+
for (let i = 0; i < bones.length; i++) {
|
|
489
|
+
const bone = bones[i];
|
|
490
|
+
const bindPose = bindPoses[i];
|
|
491
|
+
const parentIndex = parentIndices[i];
|
|
492
|
+
if (parentIndex < 0) {
|
|
493
|
+
const pos = new THREE__namespace.Vector3();
|
|
494
|
+
const quat = new THREE__namespace.Quaternion();
|
|
495
|
+
const scale = new THREE__namespace.Vector3();
|
|
496
|
+
bindPose.decompose(pos, quat, scale);
|
|
497
|
+
bone.position.copy(pos);
|
|
498
|
+
bone.quaternion.copy(quat);
|
|
499
|
+
bone.scale.set(1, 1, 1);
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
const parentBindPose = bindPoses[parentIndex];
|
|
503
|
+
const parentInv = new THREE__namespace.Matrix4().copy(parentBindPose).invert();
|
|
504
|
+
const localMatrix = new THREE__namespace.Matrix4().multiplyMatrices(parentInv, bindPose);
|
|
505
|
+
const pos = new THREE__namespace.Vector3();
|
|
506
|
+
const quat = new THREE__namespace.Quaternion();
|
|
507
|
+
const scale = new THREE__namespace.Vector3();
|
|
508
|
+
localMatrix.decompose(pos, quat, scale);
|
|
509
|
+
bone.position.copy(pos);
|
|
510
|
+
bone.quaternion.copy(quat);
|
|
511
|
+
bone.scale.set(1, 1, 1);
|
|
512
|
+
}
|
|
513
|
+
bone.updateMatrix();
|
|
514
|
+
}
|
|
515
|
+
// Create Three.js materials with properties (same as static meshes)
|
|
516
|
+
const materials = [];
|
|
517
|
+
for (let i = 0; i < materialCount; i++) {
|
|
518
|
+
const mat = materialData[i];
|
|
519
|
+
const material = new THREE__namespace.MeshStandardMaterial({
|
|
520
|
+
side: THREE__namespace.DoubleSide,
|
|
521
|
+
name: mat.name || `Material_${i}`,
|
|
522
|
+
color: new THREE__namespace.Color(0.8, 0.8, 0.8) // Default gray
|
|
523
|
+
});
|
|
524
|
+
// Apply non-texture properties
|
|
525
|
+
for (const prop of mat.properties) {
|
|
526
|
+
const fieldName = prop.fieldName.toLowerCase();
|
|
527
|
+
// Handle color properties
|
|
528
|
+
if (fieldName.includes('color') || fieldName.includes('tint')) {
|
|
529
|
+
material.color.setRGB(prop.value[0], prop.value[1], prop.value[2]);
|
|
530
|
+
}
|
|
531
|
+
else if (fieldName === 'metallic') {
|
|
532
|
+
material.metalness = prop.value[0];
|
|
533
|
+
}
|
|
534
|
+
else if (fieldName === 'roughness') {
|
|
535
|
+
material.roughness = prop.value[0];
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
materials.push(material);
|
|
539
|
+
}
|
|
540
|
+
await this.loadMaterialTextures(materialData, materials);
|
|
541
|
+
// Collect root bones (hierarchy already built earlier)
|
|
542
|
+
const rootBones = bones.filter((_, index) => parentIndices[index] < 0);
|
|
543
|
+
// Create shared skeleton
|
|
544
|
+
const skeleton = new THREE__namespace.Skeleton(bones, boneMatrices);
|
|
545
|
+
// Pre-allocate combined buffers
|
|
546
|
+
const totalVertexCount = sortedGeometryInfos.reduce((sum, info) => sum + info.vertexCount, 0);
|
|
547
|
+
const totalIndexCount = sortedGeometryInfos.reduce((sum, info) => sum + info.indexCount, 0);
|
|
548
|
+
const combinedPositions = new Float32Array(totalVertexCount * 3);
|
|
549
|
+
const combinedNormals = new Float32Array(totalVertexCount * 3);
|
|
550
|
+
const combinedUVs = new Float32Array(totalVertexCount * 2);
|
|
551
|
+
const combinedSkinIndices = new Uint16Array(totalVertexCount * 4);
|
|
552
|
+
const combinedSkinWeights = new Float32Array(totalVertexCount * 4);
|
|
553
|
+
const combinedIndices = new Uint32Array(totalIndexCount);
|
|
554
|
+
const combinedGeometry = new THREE__namespace.BufferGeometry();
|
|
555
|
+
combinedGeometry.clearGroups();
|
|
556
|
+
let vertexCursor = 0; // counts vertices
|
|
557
|
+
let indexCursor = 0; // counts indices
|
|
558
|
+
for (const geomInfo of sortedGeometryInfos) {
|
|
559
|
+
const vertexStrideBytes = 32;
|
|
560
|
+
const vertexBase = vertexCursor; // base vertex index for this submesh
|
|
561
|
+
// Validate offsets before parsing
|
|
562
|
+
const requiredVertexBytes = geomInfo.vertexBufferOffset + geomInfo.vertexCount * vertexStrideBytes;
|
|
563
|
+
const requiredIndexBytes = geomInfo.indexBufferOffset + geomInfo.indexCount * 4;
|
|
564
|
+
const requiredWeightBytes = geomInfo.weightsOffset + geomInfo.vertexCount * 32;
|
|
565
|
+
if (requiredVertexBytes > data.byteLength) {
|
|
566
|
+
throw new Error(`Vertex buffer out of bounds: need ${requiredVertexBytes}, have ${data.byteLength}`);
|
|
567
|
+
}
|
|
568
|
+
if (requiredIndexBytes > data.byteLength) {
|
|
569
|
+
throw new Error(`Index buffer out of bounds: need ${requiredIndexBytes}, have ${data.byteLength}`);
|
|
570
|
+
}
|
|
571
|
+
if (requiredWeightBytes > data.byteLength) {
|
|
572
|
+
throw new Error(`Weight buffer out of bounds: need ${requiredWeightBytes}, have ${data.byteLength}`);
|
|
573
|
+
}
|
|
574
|
+
// Parse vertices
|
|
575
|
+
for (let v = 0; v < geomInfo.vertexCount; v++) {
|
|
576
|
+
const vertexOffset = geomInfo.vertexBufferOffset + v * vertexStrideBytes;
|
|
577
|
+
// positions
|
|
578
|
+
combinedPositions[(vertexCursor + v) * 3 + 0] = dataView.getFloat32(vertexOffset + 0, true);
|
|
579
|
+
combinedPositions[(vertexCursor + v) * 3 + 1] = dataView.getFloat32(vertexOffset + 4, true);
|
|
580
|
+
combinedPositions[(vertexCursor + v) * 3 + 2] = dataView.getFloat32(vertexOffset + 8, true);
|
|
581
|
+
// normals
|
|
582
|
+
if (geomInfo.hasNormals) {
|
|
583
|
+
combinedNormals[(vertexCursor + v) * 3 + 0] = dataView.getFloat32(vertexOffset + 12, true);
|
|
584
|
+
combinedNormals[(vertexCursor + v) * 3 + 1] = dataView.getFloat32(vertexOffset + 16, true);
|
|
585
|
+
combinedNormals[(vertexCursor + v) * 3 + 2] = dataView.getFloat32(vertexOffset + 20, true);
|
|
586
|
+
}
|
|
587
|
+
// uvs
|
|
588
|
+
if (geomInfo.hasUVs) {
|
|
589
|
+
combinedUVs[(vertexCursor + v) * 2 + 0] = dataView.getFloat32(vertexOffset + 24, true);
|
|
590
|
+
combinedUVs[(vertexCursor + v) * 2 + 1] = dataView.getFloat32(vertexOffset + 28, true);
|
|
591
|
+
}
|
|
592
|
+
// bone indices & weights
|
|
593
|
+
const weightOffset = geomInfo.weightsOffset + v * 32;
|
|
594
|
+
for (let j = 0; j < 4; j++) {
|
|
595
|
+
const boneIndex = dataView.getUint32(weightOffset + j * 4, true);
|
|
596
|
+
combinedSkinIndices[(vertexCursor + v) * 4 + j] = boneIndex < boneCount ? boneIndex : 0;
|
|
597
|
+
}
|
|
598
|
+
for (let j = 0; j < 4; j++) {
|
|
599
|
+
combinedSkinWeights[(vertexCursor + v) * 4 + j] = dataView.getFloat32(weightOffset + 16 + j * 4, true);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
// Parse indices
|
|
603
|
+
const groupStart = indexCursor;
|
|
604
|
+
for (let i = 0; i < geomInfo.indexCount; i++) {
|
|
605
|
+
const indexValue = dataView.getUint32(geomInfo.indexBufferOffset + i * 4, true);
|
|
606
|
+
combinedIndices[indexCursor + i] = indexValue + vertexBase;
|
|
607
|
+
}
|
|
608
|
+
combinedGeometry.addGroup(groupStart, geomInfo.indexCount, Math.min(geomInfo.materialIndex, materials.length - 1));
|
|
609
|
+
vertexCursor += geomInfo.vertexCount;
|
|
610
|
+
indexCursor += geomInfo.indexCount;
|
|
611
|
+
}
|
|
612
|
+
combinedGeometry.setAttribute('position', new THREE__namespace.BufferAttribute(combinedPositions, 3));
|
|
613
|
+
if (geometryInfos.some(info => info.hasNormals)) {
|
|
614
|
+
combinedGeometry.setAttribute('normal', new THREE__namespace.BufferAttribute(combinedNormals, 3));
|
|
615
|
+
}
|
|
616
|
+
if (geometryInfos.some(info => info.hasUVs)) {
|
|
617
|
+
combinedGeometry.setAttribute('uv', new THREE__namespace.BufferAttribute(combinedUVs, 2));
|
|
618
|
+
}
|
|
619
|
+
combinedGeometry.setAttribute('skinIndex', new THREE__namespace.Uint16BufferAttribute(combinedSkinIndices, 4));
|
|
620
|
+
combinedGeometry.setAttribute('skinWeight', new THREE__namespace.BufferAttribute(combinedSkinWeights, 4));
|
|
621
|
+
combinedGeometry.setIndex(new THREE__namespace.BufferAttribute(combinedIndices, 1));
|
|
622
|
+
combinedGeometry.computeBoundingBox();
|
|
623
|
+
combinedGeometry.computeBoundingSphere();
|
|
624
|
+
const skinnedMesh = new THREE__namespace.SkinnedMesh(combinedGeometry, materials);
|
|
625
|
+
// Create a separate bone container to preserve hierarchy
|
|
626
|
+
const boneContainer = new THREE__namespace.Group();
|
|
627
|
+
boneContainer.name = 'BoneContainer';
|
|
628
|
+
rootBones.forEach((root) => boneContainer.add(root));
|
|
629
|
+
// Bind skeleton with our provided inverse bind matrices
|
|
630
|
+
skinnedMesh.bind(skeleton, skinnedMesh.matrixWorld);
|
|
631
|
+
skinnedMesh.normalizeSkinWeights();
|
|
632
|
+
skinnedMesh.updateMatrixWorld(true);
|
|
633
|
+
const container = new THREE__namespace.Group();
|
|
634
|
+
container.name = stringId || 'SkinnedMesh';
|
|
635
|
+
container.add(boneContainer);
|
|
636
|
+
container.add(skinnedMesh);
|
|
637
|
+
container.userData.boneOriginalNames = boneOriginalNames;
|
|
638
|
+
container.userData.skinnedMesh = skinnedMesh;
|
|
639
|
+
return container;
|
|
640
|
+
}
|
|
398
641
|
/**
|
|
399
642
|
* Load a mesh by its canonical path/name
|
|
400
643
|
*/
|
|
@@ -492,10 +735,16 @@ class StowKitPack {
|
|
|
492
735
|
return this.reader.readAssetMetadata(index);
|
|
493
736
|
}
|
|
494
737
|
/**
|
|
495
|
-
*
|
|
738
|
+
* Load skinned mesh by index
|
|
496
739
|
*/
|
|
497
|
-
|
|
498
|
-
|
|
740
|
+
async loadSkinnedMeshByIndex(index) {
|
|
741
|
+
const data = this.reader.readAssetData(index);
|
|
742
|
+
const metadata = this.reader.readAssetMetadata(index);
|
|
743
|
+
if (!data)
|
|
744
|
+
throw new Error(`Failed to read skinned mesh data for index ${index}`);
|
|
745
|
+
if (!metadata)
|
|
746
|
+
throw new Error(`No metadata for skinned mesh index ${index}`);
|
|
747
|
+
return await this.parseSkinnedMesh(metadata, data);
|
|
499
748
|
}
|
|
500
749
|
/**
|
|
501
750
|
* Load mesh by index
|
|
@@ -537,56 +786,167 @@ class StowKitPack {
|
|
|
537
786
|
return audio;
|
|
538
787
|
}
|
|
539
788
|
/**
|
|
540
|
-
* Get audio metadata by path
|
|
789
|
+
* Get audio metadata by path (uses WASM parsing)
|
|
541
790
|
*/
|
|
542
791
|
getAudioMetadata(assetPath) {
|
|
543
792
|
const assetIndex = this.reader.findAssetByPath(assetPath);
|
|
544
793
|
if (assetIndex < 0)
|
|
545
794
|
return null;
|
|
546
|
-
|
|
547
|
-
if (!metadata || metadata.length < 140)
|
|
548
|
-
return null;
|
|
549
|
-
const view = new DataView(metadata.buffer, metadata.byteOffset, metadata.byteLength);
|
|
550
|
-
const decoder = new TextDecoder();
|
|
551
|
-
const stringIdBytes = metadata.slice(0, 128);
|
|
552
|
-
const nullIdx = stringIdBytes.indexOf(0);
|
|
553
|
-
const stringId = decoder.decode(stringIdBytes.slice(0, nullIdx >= 0 ? nullIdx : 128));
|
|
554
|
-
return {
|
|
555
|
-
stringId,
|
|
556
|
-
sampleRate: view.getUint32(128, true),
|
|
557
|
-
channels: view.getUint32(132, true),
|
|
558
|
-
durationMs: view.getUint32(136, true)
|
|
559
|
-
};
|
|
795
|
+
return this.reader.parseAudioMetadata(assetIndex);
|
|
560
796
|
}
|
|
561
797
|
/**
|
|
562
|
-
*
|
|
798
|
+
* Get animation metadata by index (uses WASM parsing)
|
|
563
799
|
*/
|
|
564
|
-
|
|
565
|
-
return this.
|
|
800
|
+
getAnimationMetadata(index) {
|
|
801
|
+
return this.reader.parseAnimationMetadata(index);
|
|
566
802
|
}
|
|
567
803
|
/**
|
|
568
|
-
* Get
|
|
804
|
+
* Get texture metadata by index (uses WASM parsing)
|
|
569
805
|
*/
|
|
570
|
-
|
|
571
|
-
return this.
|
|
806
|
+
getTextureMetadata(index) {
|
|
807
|
+
return this.reader.parseTextureMetadata(index);
|
|
572
808
|
}
|
|
573
809
|
/**
|
|
574
|
-
*
|
|
810
|
+
* Load animation clip by string ID
|
|
575
811
|
*/
|
|
576
|
-
|
|
577
|
-
const
|
|
578
|
-
if (
|
|
579
|
-
|
|
580
|
-
|
|
812
|
+
async loadAnimationClip(stringId) {
|
|
813
|
+
const index = this.reader.findAssetByPath(stringId);
|
|
814
|
+
if (index < 0) {
|
|
815
|
+
throw new Error(`Animation clip not found: ${stringId}`);
|
|
816
|
+
}
|
|
817
|
+
return this.loadAnimationClipByIndex(index);
|
|
581
818
|
}
|
|
582
819
|
/**
|
|
583
|
-
*
|
|
820
|
+
* Load and play animation on a skinned mesh group
|
|
584
821
|
*/
|
|
585
|
-
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
822
|
+
async loadAnimation(skinnedMeshGroup, animationPath) {
|
|
823
|
+
const clip = await this.loadAnimationClip(animationPath);
|
|
824
|
+
const mixer = new THREE__namespace.AnimationMixer(skinnedMeshGroup);
|
|
825
|
+
const action = mixer.clipAction(clip);
|
|
826
|
+
action.setLoop(THREE__namespace.LoopRepeat, Infinity);
|
|
827
|
+
action.play();
|
|
828
|
+
return { mixer, action, clip };
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Load and play animation by index
|
|
832
|
+
*/
|
|
833
|
+
async loadAnimationByIndex(skinnedMeshGroup, animationIndex) {
|
|
834
|
+
const clip = await this.loadAnimationClipByIndex(animationIndex);
|
|
835
|
+
const mixer = new THREE__namespace.AnimationMixer(skinnedMeshGroup);
|
|
836
|
+
const action = mixer.clipAction(clip);
|
|
837
|
+
action.setLoop(THREE__namespace.LoopRepeat, Infinity);
|
|
838
|
+
action.play();
|
|
839
|
+
return { mixer, action, clip };
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Load animation clip by index
|
|
843
|
+
*/
|
|
844
|
+
async loadAnimationClipByIndex(index) {
|
|
845
|
+
const metadata = this.reader.readAssetMetadata(index);
|
|
846
|
+
const data = this.reader.readAssetData(index);
|
|
847
|
+
if (!metadata || !data) {
|
|
848
|
+
throw new Error('Failed to read animation clip data');
|
|
849
|
+
}
|
|
850
|
+
return this.parseAnimationClip(metadata, data);
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Parse animation clip from binary data
|
|
854
|
+
*/
|
|
855
|
+
parseAnimationClip(metadata, data) {
|
|
856
|
+
const view = new DataView(metadata.buffer, metadata.byteOffset, metadata.byteLength);
|
|
857
|
+
const decoder = new TextDecoder();
|
|
858
|
+
// Parse AnimationClipMetadata (272 bytes total)
|
|
859
|
+
const stringIdBytes = metadata.slice(0, 128);
|
|
860
|
+
const stringIdNullIndex = stringIdBytes.indexOf(0);
|
|
861
|
+
const stringId = decoder.decode(stringIdBytes.slice(0, stringIdNullIndex >= 0 ? stringIdNullIndex : 128));
|
|
862
|
+
const targetMeshIdBytes = metadata.slice(128, 256);
|
|
863
|
+
const targetMeshIdNullIndex = targetMeshIdBytes.indexOf(0);
|
|
864
|
+
decoder
|
|
865
|
+
.decode(targetMeshIdBytes.slice(0, targetMeshIdNullIndex >= 0 ? targetMeshIdNullIndex : 128))
|
|
866
|
+
.trim();
|
|
867
|
+
const durationSeconds = view.getFloat32(256, true); // Duration in seconds (per C header)
|
|
868
|
+
view.getFloat32(260, true); // For custom playback rate if needed
|
|
869
|
+
const channelCount = view.getUint32(264, true);
|
|
870
|
+
const boneCount = view.getUint32(268, true);
|
|
871
|
+
// Parse AnimationChannel[] (starts at offset 272, each is 28 bytes)
|
|
872
|
+
const channelsOffset = 272;
|
|
873
|
+
const channels = [];
|
|
874
|
+
for (let i = 0; i < channelCount; i++) {
|
|
875
|
+
const chOffset = channelsOffset + i * 28;
|
|
876
|
+
channels.push({
|
|
877
|
+
boneIndex: view.getUint32(chOffset, true),
|
|
878
|
+
keyframeType: view.getUint32(chOffset + 4, true),
|
|
879
|
+
keyframeCount: view.getUint32(chOffset + 8, true),
|
|
880
|
+
keyframeTimesOffset: Number(view.getBigUint64(chOffset + 12, true)),
|
|
881
|
+
keyframeValuesOffset: Number(view.getBigUint64(chOffset + 20, true))
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
// Parse AnimationBoneInfo[] (starts after all channels, each is 68 bytes)
|
|
885
|
+
const bonesOffset = channelsOffset + channelCount * 28;
|
|
886
|
+
const bones = [];
|
|
887
|
+
for (let i = 0; i < boneCount; i++) {
|
|
888
|
+
const boneOffset = bonesOffset + i * 68;
|
|
889
|
+
const nameBytes = metadata.slice(boneOffset, boneOffset + 64);
|
|
890
|
+
const nullIndex = nameBytes.indexOf(0);
|
|
891
|
+
const rawName = decoder
|
|
892
|
+
.decode(nameBytes.slice(0, nullIndex >= 0 ? nullIndex : 64))
|
|
893
|
+
.trim();
|
|
894
|
+
const sanitizedName = this.sanitizeTrackName(rawName);
|
|
895
|
+
const parentIndex = view.getInt32(boneOffset + 64, true);
|
|
896
|
+
bones.push({ name: rawName, parentIndex, sanitizedName });
|
|
897
|
+
}
|
|
898
|
+
// Create DataView for keyframe data
|
|
899
|
+
const dataView = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
900
|
+
// Create Three.js KeyframeTracks
|
|
901
|
+
const tracks = [];
|
|
902
|
+
let maxKeyframeTime = 0;
|
|
903
|
+
// Process channels - each channel specifies which bone it animates via boneIndex
|
|
904
|
+
for (let i = 0; i < channelCount; i++) {
|
|
905
|
+
const channel = channels[i];
|
|
906
|
+
// Validate bone index
|
|
907
|
+
if (channel.boneIndex >= boneCount) {
|
|
908
|
+
console.warn(`[AnimationClip] Channel ${i} references invalid bone index ${channel.boneIndex} (have ${boneCount} bones)`);
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
const boneName = bones[channel.boneIndex].sanitizedName;
|
|
912
|
+
// Skip scale tracks - bones should always have unit scale
|
|
913
|
+
if (channel.keyframeType === 2) {
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
// Read times (already in seconds per C header)
|
|
917
|
+
const times = this.readFloatArray(dataView, channel.keyframeTimesOffset, channel.keyframeCount);
|
|
918
|
+
// Track the maximum keyframe time to calculate actual duration
|
|
919
|
+
if (times.length > 0) {
|
|
920
|
+
const channelMaxTime = times[times.length - 1];
|
|
921
|
+
if (channelMaxTime > maxKeyframeTime) {
|
|
922
|
+
maxKeyframeTime = channelMaxTime;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
// Create track based on keyframe type
|
|
926
|
+
if (channel.keyframeType === 0) {
|
|
927
|
+
// Position (vec3)
|
|
928
|
+
const values = this.readFloatArray(dataView, channel.keyframeValuesOffset, channel.keyframeCount * 3);
|
|
929
|
+
tracks.push(new THREE__namespace.VectorKeyframeTrack(`${boneName}.position`, times, values));
|
|
930
|
+
}
|
|
931
|
+
else if (channel.keyframeType === 1) {
|
|
932
|
+
// Rotation (quaternion)
|
|
933
|
+
const values = this.readFloatArray(dataView, channel.keyframeValuesOffset, channel.keyframeCount * 4);
|
|
934
|
+
tracks.push(new THREE__namespace.QuaternionKeyframeTrack(`${boneName}.quaternion`, times, values));
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
// Use the actual maximum keyframe time as duration instead of metadata duration
|
|
938
|
+
const actualDuration = maxKeyframeTime > 0 ? maxKeyframeTime : durationSeconds;
|
|
939
|
+
return new THREE__namespace.AnimationClip(stringId, actualDuration, tracks);
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Helper to read float array from DataView
|
|
943
|
+
*/
|
|
944
|
+
readFloatArray(view, offset, count) {
|
|
945
|
+
const result = new Float32Array(count);
|
|
946
|
+
for (let i = 0; i < count; i++) {
|
|
947
|
+
result[i] = view.getFloat32(offset + i * 4, true);
|
|
948
|
+
}
|
|
949
|
+
return result;
|
|
590
950
|
}
|
|
591
951
|
/**
|
|
592
952
|
* Close the pack and free resources
|
|
@@ -600,13 +960,15 @@ class StowKitPack {
|
|
|
600
960
|
const matData = materialData[i];
|
|
601
961
|
const material = materials[i];
|
|
602
962
|
for (const prop of matData.properties) {
|
|
603
|
-
|
|
963
|
+
// Trim and check for truly non-empty texture ID
|
|
964
|
+
const textureId = prop.textureId?.trim();
|
|
965
|
+
if (textureId && textureId.length > 0) {
|
|
604
966
|
try {
|
|
605
|
-
const texture = await this.loadTexture(
|
|
967
|
+
const texture = await this.loadTexture(textureId);
|
|
606
968
|
this.applyTextureToMaterial(material, prop.fieldName, texture);
|
|
607
969
|
}
|
|
608
970
|
catch (error) {
|
|
609
|
-
console.error(`Failed to load texture "${
|
|
971
|
+
console.error(`Failed to load texture "${textureId}":`, error);
|
|
610
972
|
}
|
|
611
973
|
}
|
|
612
974
|
}
|
|
@@ -653,88 +1015,11 @@ class StowKitPack {
|
|
|
653
1015
|
}
|
|
654
1016
|
material.needsUpdate = true;
|
|
655
1017
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
const decoder = new TextDecoder();
|
|
662
|
-
const stringIdBytes = metadata.slice(0, 128);
|
|
663
|
-
const schemaNameBytes = metadata.slice(128, 192);
|
|
664
|
-
const stringId = decoder.decode(stringIdBytes.slice(0, stringIdBytes.indexOf(0) || 128));
|
|
665
|
-
const schemaName = decoder.decode(schemaNameBytes.slice(0, schemaNameBytes.indexOf(0) || 64));
|
|
666
|
-
const fieldCount = view.getUint32(192, true);
|
|
667
|
-
const fields = [];
|
|
668
|
-
let offset = 196;
|
|
669
|
-
for (let i = 0; i < fieldCount; i++) {
|
|
670
|
-
if (offset + 88 > metadata.length)
|
|
671
|
-
break;
|
|
672
|
-
const fieldNameBytes = metadata.slice(offset, offset + 64);
|
|
673
|
-
const fieldName = decoder.decode(fieldNameBytes.slice(0, fieldNameBytes.indexOf(0) || 64));
|
|
674
|
-
const fieldType = view.getUint32(offset + 64, true);
|
|
675
|
-
const previewFlags = view.getUint32(offset + 68, true);
|
|
676
|
-
const defaultValue = [
|
|
677
|
-
view.getFloat32(offset + 72, true),
|
|
678
|
-
view.getFloat32(offset + 76, true),
|
|
679
|
-
view.getFloat32(offset + 80, true),
|
|
680
|
-
view.getFloat32(offset + 84, true)
|
|
681
|
-
];
|
|
682
|
-
const typeNames = ['Texture', 'Color', 'Float', 'Vec2', 'Vec3', 'Vec4', 'Int'];
|
|
683
|
-
const previewFlagNames = ['None', 'MainTex', 'Tint'];
|
|
684
|
-
fields.push({
|
|
685
|
-
name: fieldName,
|
|
686
|
-
type: typeNames[fieldType] || 'Float',
|
|
687
|
-
previewFlag: previewFlagNames[previewFlags] || 'None',
|
|
688
|
-
defaultValue
|
|
689
|
-
});
|
|
690
|
-
offset += 88;
|
|
691
|
-
}
|
|
692
|
-
return { stringId, schemaName, fields };
|
|
693
|
-
}
|
|
694
|
-
getMeshMaterialsByIndex_internal(index) {
|
|
695
|
-
const metadata = this.reader.readAssetMetadata(index);
|
|
696
|
-
if (!metadata || metadata.length < 140)
|
|
697
|
-
return null;
|
|
698
|
-
const view = new DataView(metadata.buffer, metadata.byteOffset, metadata.byteLength);
|
|
699
|
-
const decoder = new TextDecoder();
|
|
700
|
-
const meshGeometryCount = view.getUint32(0, true);
|
|
701
|
-
const materialCount = view.getUint32(4, true);
|
|
702
|
-
const nodeCount = view.getUint32(8, true);
|
|
703
|
-
let offset = 140;
|
|
704
|
-
offset += meshGeometryCount * 40;
|
|
705
|
-
const materials = [];
|
|
706
|
-
for (let i = 0; i < materialCount; i++) {
|
|
707
|
-
if (offset + 196 > metadata.length)
|
|
708
|
-
break;
|
|
709
|
-
const nameBytes = metadata.slice(offset, offset + 64);
|
|
710
|
-
const schemaIdBytes = metadata.slice(offset + 64, offset + 192);
|
|
711
|
-
const name = decoder.decode(nameBytes.slice(0, nameBytes.indexOf(0) || 64));
|
|
712
|
-
const schemaId = decoder.decode(schemaIdBytes.slice(0, schemaIdBytes.indexOf(0) || 128));
|
|
713
|
-
const propertyCount = view.getUint32(offset + 192, true);
|
|
714
|
-
materials.push({ name, schemaId, propertyCount, properties: [] });
|
|
715
|
-
offset += 196;
|
|
716
|
-
}
|
|
717
|
-
offset += nodeCount * 116;
|
|
718
|
-
offset += nodeCount * 4;
|
|
719
|
-
for (const mat of materials) {
|
|
720
|
-
for (let j = 0; j < mat.propertyCount; j++) {
|
|
721
|
-
if (offset + 144 > metadata.length)
|
|
722
|
-
break;
|
|
723
|
-
const fieldNameBytes = metadata.slice(offset, offset + 64);
|
|
724
|
-
const fieldName = decoder.decode(fieldNameBytes.slice(0, fieldNameBytes.indexOf(0) || 64));
|
|
725
|
-
const value = [
|
|
726
|
-
view.getFloat32(offset + 64, true),
|
|
727
|
-
view.getFloat32(offset + 68, true),
|
|
728
|
-
view.getFloat32(offset + 72, true),
|
|
729
|
-
view.getFloat32(offset + 76, true)
|
|
730
|
-
];
|
|
731
|
-
const textureIdBytes = metadata.slice(offset + 80, offset + 144);
|
|
732
|
-
const textureId = decoder.decode(textureIdBytes.slice(0, textureIdBytes.indexOf(0) || 64));
|
|
733
|
-
mat.properties.push({ fieldName, value, textureId });
|
|
734
|
-
offset += 144;
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
return materials;
|
|
1018
|
+
sanitizeTrackName(name) {
|
|
1019
|
+
return name
|
|
1020
|
+
.trim()
|
|
1021
|
+
.replace(/[^A-Za-z0-9_\-]+/g, '_')
|
|
1022
|
+
.replace(/_{2,}/g, '_');
|
|
738
1023
|
}
|
|
739
1024
|
}
|
|
740
1025
|
|
|
@@ -782,7 +1067,10 @@ class StowKitLoader {
|
|
|
782
1067
|
if (data instanceof ArrayBuffer) {
|
|
783
1068
|
arrayBuffer = data;
|
|
784
1069
|
}
|
|
785
|
-
else if (
|
|
1070
|
+
else if (typeof Blob !== 'undefined' && data instanceof Blob) {
|
|
1071
|
+
arrayBuffer = await data.arrayBuffer();
|
|
1072
|
+
}
|
|
1073
|
+
else if ('arrayBuffer' in data && typeof data.arrayBuffer === 'function') {
|
|
786
1074
|
arrayBuffer = await data.arrayBuffer();
|
|
787
1075
|
}
|
|
788
1076
|
else {
|
|
@@ -821,6 +1109,10 @@ StowKitLoader.ktx2Loader = null;
|
|
|
821
1109
|
StowKitLoader.dracoLoader = null;
|
|
822
1110
|
StowKitLoader.initialized = false;
|
|
823
1111
|
|
|
1112
|
+
Object.defineProperty(exports, 'AssetType', {
|
|
1113
|
+
enumerable: true,
|
|
1114
|
+
get: function () { return reader.AssetType; }
|
|
1115
|
+
});
|
|
824
1116
|
exports.StowKitLoader = StowKitLoader;
|
|
825
1117
|
exports.StowKitPack = StowKitPack;
|
|
826
1118
|
//# sourceMappingURL=stowkit-three-loader.js.map
|