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