@stowkit/three-loader 0.1.15 → 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/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 +479 -122
- package/dist/stowkit-three-loader.esm.js.map +1 -1
- package/dist/stowkit-three-loader.js +482 -122
- package/dist/stowkit-three-loader.js.map +1 -1
- package/package.json +1 -1
|
@@ -327,6 +327,317 @@ class StowKitPack {
|
|
|
327
327
|
this.ktx2Loader = ktx2Loader;
|
|
328
328
|
this.dracoLoader = dracoLoader;
|
|
329
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
|
+
}
|
|
330
641
|
/**
|
|
331
642
|
* Load a mesh by its canonical path/name
|
|
332
643
|
*/
|
|
@@ -424,10 +735,16 @@ class StowKitPack {
|
|
|
424
735
|
return this.reader.readAssetMetadata(index);
|
|
425
736
|
}
|
|
426
737
|
/**
|
|
427
|
-
*
|
|
738
|
+
* Load skinned mesh by index
|
|
428
739
|
*/
|
|
429
|
-
|
|
430
|
-
|
|
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);
|
|
431
748
|
}
|
|
432
749
|
/**
|
|
433
750
|
* Load mesh by index
|
|
@@ -469,56 +786,167 @@ class StowKitPack {
|
|
|
469
786
|
return audio;
|
|
470
787
|
}
|
|
471
788
|
/**
|
|
472
|
-
* Get audio metadata by path
|
|
789
|
+
* Get audio metadata by path (uses WASM parsing)
|
|
473
790
|
*/
|
|
474
791
|
getAudioMetadata(assetPath) {
|
|
475
792
|
const assetIndex = this.reader.findAssetByPath(assetPath);
|
|
476
793
|
if (assetIndex < 0)
|
|
477
794
|
return null;
|
|
478
|
-
|
|
479
|
-
if (!metadata || metadata.length < 140)
|
|
480
|
-
return null;
|
|
481
|
-
const view = new DataView(metadata.buffer, metadata.byteOffset, metadata.byteLength);
|
|
482
|
-
const decoder = new TextDecoder();
|
|
483
|
-
const stringIdBytes = metadata.slice(0, 128);
|
|
484
|
-
const nullIdx = stringIdBytes.indexOf(0);
|
|
485
|
-
const stringId = decoder.decode(stringIdBytes.slice(0, nullIdx >= 0 ? nullIdx : 128));
|
|
486
|
-
return {
|
|
487
|
-
stringId,
|
|
488
|
-
sampleRate: view.getUint32(128, true),
|
|
489
|
-
channels: view.getUint32(132, true),
|
|
490
|
-
durationMs: view.getUint32(136, true)
|
|
491
|
-
};
|
|
795
|
+
return this.reader.parseAudioMetadata(assetIndex);
|
|
492
796
|
}
|
|
493
797
|
/**
|
|
494
|
-
*
|
|
798
|
+
* Get animation metadata by index (uses WASM parsing)
|
|
495
799
|
*/
|
|
496
|
-
|
|
497
|
-
return this.
|
|
800
|
+
getAnimationMetadata(index) {
|
|
801
|
+
return this.reader.parseAnimationMetadata(index);
|
|
498
802
|
}
|
|
499
803
|
/**
|
|
500
|
-
* Get
|
|
804
|
+
* Get texture metadata by index (uses WASM parsing)
|
|
501
805
|
*/
|
|
502
|
-
|
|
503
|
-
return this.
|
|
806
|
+
getTextureMetadata(index) {
|
|
807
|
+
return this.reader.parseTextureMetadata(index);
|
|
504
808
|
}
|
|
505
809
|
/**
|
|
506
|
-
*
|
|
810
|
+
* Load animation clip by string ID
|
|
507
811
|
*/
|
|
508
|
-
|
|
509
|
-
const
|
|
510
|
-
if (
|
|
511
|
-
|
|
512
|
-
|
|
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);
|
|
513
818
|
}
|
|
514
819
|
/**
|
|
515
|
-
*
|
|
820
|
+
* Load and play animation on a skinned mesh group
|
|
516
821
|
*/
|
|
517
|
-
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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;
|
|
522
950
|
}
|
|
523
951
|
/**
|
|
524
952
|
* Close the pack and free resources
|
|
@@ -532,13 +960,15 @@ class StowKitPack {
|
|
|
532
960
|
const matData = materialData[i];
|
|
533
961
|
const material = materials[i];
|
|
534
962
|
for (const prop of matData.properties) {
|
|
535
|
-
|
|
963
|
+
// Trim and check for truly non-empty texture ID
|
|
964
|
+
const textureId = prop.textureId?.trim();
|
|
965
|
+
if (textureId && textureId.length > 0) {
|
|
536
966
|
try {
|
|
537
|
-
const texture = await this.loadTexture(
|
|
967
|
+
const texture = await this.loadTexture(textureId);
|
|
538
968
|
this.applyTextureToMaterial(material, prop.fieldName, texture);
|
|
539
969
|
}
|
|
540
970
|
catch (error) {
|
|
541
|
-
console.error(`Failed to load texture "${
|
|
971
|
+
console.error(`Failed to load texture "${textureId}":`, error);
|
|
542
972
|
}
|
|
543
973
|
}
|
|
544
974
|
}
|
|
@@ -585,88 +1015,11 @@ class StowKitPack {
|
|
|
585
1015
|
}
|
|
586
1016
|
material.needsUpdate = true;
|
|
587
1017
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
const decoder = new TextDecoder();
|
|
594
|
-
const stringIdBytes = metadata.slice(0, 128);
|
|
595
|
-
const schemaNameBytes = metadata.slice(128, 192);
|
|
596
|
-
const stringId = decoder.decode(stringIdBytes.slice(0, stringIdBytes.indexOf(0) || 128));
|
|
597
|
-
const schemaName = decoder.decode(schemaNameBytes.slice(0, schemaNameBytes.indexOf(0) || 64));
|
|
598
|
-
const fieldCount = view.getUint32(192, true);
|
|
599
|
-
const fields = [];
|
|
600
|
-
let offset = 196;
|
|
601
|
-
for (let i = 0; i < fieldCount; i++) {
|
|
602
|
-
if (offset + 88 > metadata.length)
|
|
603
|
-
break;
|
|
604
|
-
const fieldNameBytes = metadata.slice(offset, offset + 64);
|
|
605
|
-
const fieldName = decoder.decode(fieldNameBytes.slice(0, fieldNameBytes.indexOf(0) || 64));
|
|
606
|
-
const fieldType = view.getUint32(offset + 64, true);
|
|
607
|
-
const previewFlags = view.getUint32(offset + 68, true);
|
|
608
|
-
const defaultValue = [
|
|
609
|
-
view.getFloat32(offset + 72, true),
|
|
610
|
-
view.getFloat32(offset + 76, true),
|
|
611
|
-
view.getFloat32(offset + 80, true),
|
|
612
|
-
view.getFloat32(offset + 84, true)
|
|
613
|
-
];
|
|
614
|
-
const typeNames = ['Texture', 'Color', 'Float', 'Vec2', 'Vec3', 'Vec4', 'Int'];
|
|
615
|
-
const previewFlagNames = ['None', 'MainTex', 'Tint'];
|
|
616
|
-
fields.push({
|
|
617
|
-
name: fieldName,
|
|
618
|
-
type: typeNames[fieldType] || 'Float',
|
|
619
|
-
previewFlag: previewFlagNames[previewFlags] || 'None',
|
|
620
|
-
defaultValue
|
|
621
|
-
});
|
|
622
|
-
offset += 88;
|
|
623
|
-
}
|
|
624
|
-
return { stringId, schemaName, fields };
|
|
625
|
-
}
|
|
626
|
-
getMeshMaterialsByIndex_internal(index) {
|
|
627
|
-
const metadata = this.reader.readAssetMetadata(index);
|
|
628
|
-
if (!metadata || metadata.length < 140)
|
|
629
|
-
return null;
|
|
630
|
-
const view = new DataView(metadata.buffer, metadata.byteOffset, metadata.byteLength);
|
|
631
|
-
const decoder = new TextDecoder();
|
|
632
|
-
const meshGeometryCount = view.getUint32(0, true);
|
|
633
|
-
const materialCount = view.getUint32(4, true);
|
|
634
|
-
const nodeCount = view.getUint32(8, true);
|
|
635
|
-
let offset = 140;
|
|
636
|
-
offset += meshGeometryCount * 40;
|
|
637
|
-
const materials = [];
|
|
638
|
-
for (let i = 0; i < materialCount; i++) {
|
|
639
|
-
if (offset + 196 > metadata.length)
|
|
640
|
-
break;
|
|
641
|
-
const nameBytes = metadata.slice(offset, offset + 64);
|
|
642
|
-
const schemaIdBytes = metadata.slice(offset + 64, offset + 192);
|
|
643
|
-
const name = decoder.decode(nameBytes.slice(0, nameBytes.indexOf(0) || 64));
|
|
644
|
-
const schemaId = decoder.decode(schemaIdBytes.slice(0, schemaIdBytes.indexOf(0) || 128));
|
|
645
|
-
const propertyCount = view.getUint32(offset + 192, true);
|
|
646
|
-
materials.push({ name, schemaId, propertyCount, properties: [] });
|
|
647
|
-
offset += 196;
|
|
648
|
-
}
|
|
649
|
-
offset += nodeCount * 116;
|
|
650
|
-
offset += nodeCount * 4;
|
|
651
|
-
for (const mat of materials) {
|
|
652
|
-
for (let j = 0; j < mat.propertyCount; j++) {
|
|
653
|
-
if (offset + 144 > metadata.length)
|
|
654
|
-
break;
|
|
655
|
-
const fieldNameBytes = metadata.slice(offset, offset + 64);
|
|
656
|
-
const fieldName = decoder.decode(fieldNameBytes.slice(0, fieldNameBytes.indexOf(0) || 64));
|
|
657
|
-
const value = [
|
|
658
|
-
view.getFloat32(offset + 64, true),
|
|
659
|
-
view.getFloat32(offset + 68, true),
|
|
660
|
-
view.getFloat32(offset + 72, true),
|
|
661
|
-
view.getFloat32(offset + 76, true)
|
|
662
|
-
];
|
|
663
|
-
const textureIdBytes = metadata.slice(offset + 80, offset + 144);
|
|
664
|
-
const textureId = decoder.decode(textureIdBytes.slice(0, textureIdBytes.indexOf(0) || 64));
|
|
665
|
-
mat.properties.push({ fieldName, value, textureId });
|
|
666
|
-
offset += 144;
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
return materials;
|
|
1018
|
+
sanitizeTrackName(name) {
|
|
1019
|
+
return name
|
|
1020
|
+
.trim()
|
|
1021
|
+
.replace(/[^A-Za-z0-9_\-]+/g, '_')
|
|
1022
|
+
.replace(/_{2,}/g, '_');
|
|
670
1023
|
}
|
|
671
1024
|
}
|
|
672
1025
|
|
|
@@ -714,7 +1067,10 @@ class StowKitLoader {
|
|
|
714
1067
|
if (data instanceof ArrayBuffer) {
|
|
715
1068
|
arrayBuffer = data;
|
|
716
1069
|
}
|
|
717
|
-
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') {
|
|
718
1074
|
arrayBuffer = await data.arrayBuffer();
|
|
719
1075
|
}
|
|
720
1076
|
else {
|
|
@@ -753,6 +1109,10 @@ StowKitLoader.ktx2Loader = null;
|
|
|
753
1109
|
StowKitLoader.dracoLoader = null;
|
|
754
1110
|
StowKitLoader.initialized = false;
|
|
755
1111
|
|
|
1112
|
+
Object.defineProperty(exports, 'AssetType', {
|
|
1113
|
+
enumerable: true,
|
|
1114
|
+
get: function () { return reader.AssetType; }
|
|
1115
|
+
});
|
|
756
1116
|
exports.StowKitLoader = StowKitLoader;
|
|
757
1117
|
exports.StowKitPack = StowKitPack;
|
|
758
1118
|
//# sourceMappingURL=stowkit-three-loader.js.map
|