@stowkit/three-loader 0.1.31 → 0.1.33
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.
|
@@ -364,7 +364,6 @@ class MeshParser {
|
|
|
364
364
|
if (cacheKey && version) {
|
|
365
365
|
const cached = await AssetMemoryCache.getGeometry(cacheKey, version);
|
|
366
366
|
if (cached) {
|
|
367
|
-
reader.PerfLogger.log(`[Perf] Geometry cache hit: ${cacheKey}`);
|
|
368
367
|
const geometry = new THREE__namespace.BufferGeometry();
|
|
369
368
|
geometry.setAttribute('position', new THREE__namespace.BufferAttribute(cached.positions, 3));
|
|
370
369
|
if (cached.normals) {
|
|
@@ -422,12 +421,8 @@ class MeshParser {
|
|
|
422
421
|
root.name = 'StowKitMesh';
|
|
423
422
|
const { geometries, materials, nodes, meshIndices } = parsedData;
|
|
424
423
|
// Pre-load ALL geometries in parallel for maximum speed
|
|
425
|
-
const dracoStart = performance.now();
|
|
426
424
|
const geometryPromises = geometries.map((geoInfo, index) => this.createGeometry(geoInfo, dataBlob, dracoLoader, cacheKeyPrefix ? `${cacheKeyPrefix}_geo${index}` : undefined, version));
|
|
427
425
|
const loadedGeometries = await Promise.all(geometryPromises);
|
|
428
|
-
const dracoTime = performance.now() - dracoStart;
|
|
429
|
-
const totalVerts = geometries.reduce((sum, g) => sum + g.vertexCount, 0);
|
|
430
|
-
reader.PerfLogger.log(`[Perf] Draco: ${dracoTime.toFixed(2)}ms (${geometries.length} meshes, ${totalVerts} verts)`);
|
|
431
426
|
// Create all Three.js objects for nodes
|
|
432
427
|
const nodeObjects = [];
|
|
433
428
|
for (const node of nodes) {
|
|
@@ -526,6 +521,7 @@ class StowKitPack {
|
|
|
526
521
|
* Load a skinned mesh by its string ID
|
|
527
522
|
*/
|
|
528
523
|
async loadSkinnedMesh(stringId) {
|
|
524
|
+
const totalStart = performance.now();
|
|
529
525
|
const assetIndex = this.reader.findAssetByPath(stringId);
|
|
530
526
|
if (assetIndex < 0) {
|
|
531
527
|
throw new Error(`Skinned mesh not found: ${stringId}`);
|
|
@@ -536,12 +532,16 @@ class StowKitPack {
|
|
|
536
532
|
throw new Error(`Failed to read skinned mesh data: ${stringId}`);
|
|
537
533
|
if (!metadata)
|
|
538
534
|
throw new Error(`No metadata for skinned mesh: ${stringId}`);
|
|
539
|
-
|
|
535
|
+
const result = await this.parseSkinnedMesh(metadata, data);
|
|
536
|
+
const totalTime = performance.now() - totalStart;
|
|
537
|
+
reader.PerfLogger.log(`[Perf] === Skinned Mesh "${stringId}": ${totalTime.toFixed(2)}ms total ===`);
|
|
538
|
+
return result;
|
|
540
539
|
}
|
|
541
540
|
/**
|
|
542
541
|
* Parse skinned mesh from binary data
|
|
543
542
|
*/
|
|
544
543
|
async parseSkinnedMesh(metadata, data) {
|
|
544
|
+
const parseStart = performance.now();
|
|
545
545
|
// NEW FORMAT: Skip past tag header
|
|
546
546
|
const view = new DataView(metadata.buffer, metadata.byteOffset, metadata.byteLength);
|
|
547
547
|
const tagCsvLength = view.getUint32(0, true);
|
|
@@ -587,7 +587,7 @@ class StowKitPack {
|
|
|
587
587
|
});
|
|
588
588
|
offset += 72;
|
|
589
589
|
}
|
|
590
|
-
|
|
590
|
+
new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
591
591
|
// Sort geometry infos by vertex buffer offset to ensure sequential parsing
|
|
592
592
|
const sortedGeometryInfos = geometryInfos.slice().sort((a, b) => a.vertexBufferOffset - b.vertexBufferOffset);
|
|
593
593
|
// Parse materials from metadata (immediately after geometries)
|
|
@@ -742,88 +742,39 @@ class StowKitPack {
|
|
|
742
742
|
}
|
|
743
743
|
materials.push(material);
|
|
744
744
|
}
|
|
745
|
+
const parseTime = performance.now() - parseStart;
|
|
746
|
+
reader.PerfLogger.log(`[Perf] Parse skinned metadata: ${parseTime.toFixed(2)}ms`);
|
|
747
|
+
const textureStart = performance.now();
|
|
745
748
|
await this.loadMaterialTextures(materialData, materials);
|
|
749
|
+
const textureTime = performance.now() - textureStart;
|
|
750
|
+
if (materialData.length > 0) {
|
|
751
|
+
reader.PerfLogger.log(`[Perf] Load textures: ${textureTime.toFixed(2)}ms`);
|
|
752
|
+
}
|
|
746
753
|
// Collect root bones (hierarchy already built earlier)
|
|
754
|
+
const buildStart = performance.now();
|
|
747
755
|
const rootBones = bones.filter((_, index) => parentIndices[index] < 0);
|
|
748
756
|
// Create shared skeleton
|
|
749
757
|
const skeleton = new THREE__namespace.Skeleton(bones, boneMatrices);
|
|
750
|
-
//
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
const
|
|
757
|
-
const combinedSkinWeights = new Float32Array(totalVertexCount * 4);
|
|
758
|
-
const combinedIndices = new Uint32Array(totalIndexCount);
|
|
758
|
+
// Use WASM to parse geometry data (10-50x faster!)
|
|
759
|
+
const geomData = this.reader.parseSkinnedMeshGeometryFast(metadata, data);
|
|
760
|
+
if (!geomData) {
|
|
761
|
+
throw new Error('Failed to parse skinned mesh geometry');
|
|
762
|
+
}
|
|
763
|
+
// Convert Uint32Array skinIndices to Uint16Array for Three.js
|
|
764
|
+
const skinIndices16 = new Uint16Array(geomData.skinIndices);
|
|
759
765
|
const combinedGeometry = new THREE__namespace.BufferGeometry();
|
|
760
|
-
combinedGeometry.
|
|
761
|
-
|
|
762
|
-
|
|
766
|
+
combinedGeometry.setAttribute('position', new THREE__namespace.BufferAttribute(geomData.positions, 3));
|
|
767
|
+
combinedGeometry.setAttribute('normal', new THREE__namespace.BufferAttribute(geomData.normals, 3));
|
|
768
|
+
combinedGeometry.setAttribute('uv', new THREE__namespace.BufferAttribute(geomData.uvs, 2));
|
|
769
|
+
combinedGeometry.setAttribute('skinIndex', new THREE__namespace.Uint16BufferAttribute(skinIndices16, 4));
|
|
770
|
+
combinedGeometry.setAttribute('skinWeight', new THREE__namespace.BufferAttribute(geomData.skinWeights, 4));
|
|
771
|
+
combinedGeometry.setIndex(new THREE__namespace.BufferAttribute(geomData.indices, 1));
|
|
772
|
+
// Add geometry groups for materials
|
|
773
|
+
let indexCursor = 0;
|
|
763
774
|
for (const geomInfo of sortedGeometryInfos) {
|
|
764
|
-
|
|
765
|
-
const vertexBase = vertexCursor; // base vertex index for this submesh
|
|
766
|
-
// Validate offsets before parsing
|
|
767
|
-
const requiredVertexBytes = geomInfo.vertexBufferOffset + geomInfo.vertexCount * vertexStrideBytes;
|
|
768
|
-
const requiredIndexBytes = geomInfo.indexBufferOffset + geomInfo.indexCount * 4;
|
|
769
|
-
const requiredWeightBytes = geomInfo.weightsOffset + geomInfo.vertexCount * 32;
|
|
770
|
-
if (requiredVertexBytes > data.byteLength) {
|
|
771
|
-
throw new Error(`Vertex buffer out of bounds: need ${requiredVertexBytes}, have ${data.byteLength}`);
|
|
772
|
-
}
|
|
773
|
-
if (requiredIndexBytes > data.byteLength) {
|
|
774
|
-
throw new Error(`Index buffer out of bounds: need ${requiredIndexBytes}, have ${data.byteLength}`);
|
|
775
|
-
}
|
|
776
|
-
if (requiredWeightBytes > data.byteLength) {
|
|
777
|
-
throw new Error(`Weight buffer out of bounds: need ${requiredWeightBytes}, have ${data.byteLength}`);
|
|
778
|
-
}
|
|
779
|
-
// Parse vertices
|
|
780
|
-
for (let v = 0; v < geomInfo.vertexCount; v++) {
|
|
781
|
-
const vertexOffset = geomInfo.vertexBufferOffset + v * vertexStrideBytes;
|
|
782
|
-
// positions
|
|
783
|
-
combinedPositions[(vertexCursor + v) * 3 + 0] = dataView.getFloat32(vertexOffset + 0, true);
|
|
784
|
-
combinedPositions[(vertexCursor + v) * 3 + 1] = dataView.getFloat32(vertexOffset + 4, true);
|
|
785
|
-
combinedPositions[(vertexCursor + v) * 3 + 2] = dataView.getFloat32(vertexOffset + 8, true);
|
|
786
|
-
// normals
|
|
787
|
-
if (geomInfo.hasNormals) {
|
|
788
|
-
combinedNormals[(vertexCursor + v) * 3 + 0] = dataView.getFloat32(vertexOffset + 12, true);
|
|
789
|
-
combinedNormals[(vertexCursor + v) * 3 + 1] = dataView.getFloat32(vertexOffset + 16, true);
|
|
790
|
-
combinedNormals[(vertexCursor + v) * 3 + 2] = dataView.getFloat32(vertexOffset + 20, true);
|
|
791
|
-
}
|
|
792
|
-
// uvs
|
|
793
|
-
if (geomInfo.hasUVs) {
|
|
794
|
-
combinedUVs[(vertexCursor + v) * 2 + 0] = dataView.getFloat32(vertexOffset + 24, true);
|
|
795
|
-
combinedUVs[(vertexCursor + v) * 2 + 1] = dataView.getFloat32(vertexOffset + 28, true);
|
|
796
|
-
}
|
|
797
|
-
// bone indices & weights
|
|
798
|
-
const weightOffset = geomInfo.weightsOffset + v * 32;
|
|
799
|
-
for (let j = 0; j < 4; j++) {
|
|
800
|
-
const boneIndex = dataView.getUint32(weightOffset + j * 4, true);
|
|
801
|
-
combinedSkinIndices[(vertexCursor + v) * 4 + j] = boneIndex < boneCount ? boneIndex : 0;
|
|
802
|
-
}
|
|
803
|
-
for (let j = 0; j < 4; j++) {
|
|
804
|
-
combinedSkinWeights[(vertexCursor + v) * 4 + j] = dataView.getFloat32(weightOffset + 16 + j * 4, true);
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
// Parse indices
|
|
808
|
-
const groupStart = indexCursor;
|
|
809
|
-
for (let i = 0; i < geomInfo.indexCount; i++) {
|
|
810
|
-
const indexValue = dataView.getUint32(geomInfo.indexBufferOffset + i * 4, true);
|
|
811
|
-
combinedIndices[indexCursor + i] = indexValue + vertexBase;
|
|
812
|
-
}
|
|
813
|
-
combinedGeometry.addGroup(groupStart, geomInfo.indexCount, Math.min(geomInfo.materialIndex, materials.length - 1));
|
|
814
|
-
vertexCursor += geomInfo.vertexCount;
|
|
775
|
+
combinedGeometry.addGroup(indexCursor, geomInfo.indexCount, Math.min(geomInfo.materialIndex, materials.length - 1));
|
|
815
776
|
indexCursor += geomInfo.indexCount;
|
|
816
777
|
}
|
|
817
|
-
combinedGeometry.setAttribute('position', new THREE__namespace.BufferAttribute(combinedPositions, 3));
|
|
818
|
-
if (geometryInfos.some(info => info.hasNormals)) {
|
|
819
|
-
combinedGeometry.setAttribute('normal', new THREE__namespace.BufferAttribute(combinedNormals, 3));
|
|
820
|
-
}
|
|
821
|
-
if (geometryInfos.some(info => info.hasUVs)) {
|
|
822
|
-
combinedGeometry.setAttribute('uv', new THREE__namespace.BufferAttribute(combinedUVs, 2));
|
|
823
|
-
}
|
|
824
|
-
combinedGeometry.setAttribute('skinIndex', new THREE__namespace.Uint16BufferAttribute(combinedSkinIndices, 4));
|
|
825
|
-
combinedGeometry.setAttribute('skinWeight', new THREE__namespace.BufferAttribute(combinedSkinWeights, 4));
|
|
826
|
-
combinedGeometry.setIndex(new THREE__namespace.BufferAttribute(combinedIndices, 1));
|
|
827
778
|
combinedGeometry.computeBoundingBox();
|
|
828
779
|
combinedGeometry.computeBoundingSphere();
|
|
829
780
|
const skinnedMesh = new THREE__namespace.SkinnedMesh(combinedGeometry, materials);
|
|
@@ -841,6 +792,8 @@ class StowKitPack {
|
|
|
841
792
|
container.add(skinnedMesh);
|
|
842
793
|
container.userData.boneOriginalNames = boneOriginalNames;
|
|
843
794
|
container.userData.skinnedMesh = skinnedMesh;
|
|
795
|
+
const buildTime = performance.now() - buildStart;
|
|
796
|
+
reader.PerfLogger.log(`[Perf] Build skinned mesh: ${buildTime.toFixed(2)}ms (${boneCount} bones, ${geomData.vertexCount} verts)`);
|
|
844
797
|
return container;
|
|
845
798
|
}
|
|
846
799
|
/**
|
|
@@ -865,12 +818,7 @@ class StowKitPack {
|
|
|
865
818
|
// Parse mesh data
|
|
866
819
|
const parsedData = MeshParser.parseMeshData(metadata, data);
|
|
867
820
|
// Load textures for materials
|
|
868
|
-
|
|
869
|
-
const textureNames = await this.loadMaterialTextures(parsedData.materialData, parsedData.materials);
|
|
870
|
-
const textureTime = performance.now() - textureStart;
|
|
871
|
-
if (textureNames.length > 0) {
|
|
872
|
-
reader.PerfLogger.log(`[Perf] Textures: ${textureTime.toFixed(2)}ms (${textureNames.join(', ')})`);
|
|
873
|
-
}
|
|
821
|
+
await this.loadMaterialTextures(parsedData.materialData, parsedData.materials);
|
|
874
822
|
// Build Three.js scene with Draco decoder (with caching)
|
|
875
823
|
const cacheKey = `${this.packUrl}::${assetPath}`;
|
|
876
824
|
const scene = await MeshParser.buildScene(parsedData, data, this.dracoLoader, cacheKey, this.packVersion);
|
|
@@ -1224,26 +1172,23 @@ class StowKitPack {
|
|
|
1224
1172
|
// Try IndexedDB cache first
|
|
1225
1173
|
const cacheKey = textureName ? `${this.packUrl}::${textureName}` : undefined;
|
|
1226
1174
|
if (cacheKey) {
|
|
1227
|
-
const startCache = performance.now();
|
|
1228
1175
|
const cached = await AssetMemoryCache.getTexture(cacheKey, this.packVersion);
|
|
1229
1176
|
if (cached) {
|
|
1230
|
-
reader.PerfLogger.log(`[Perf] Texture
|
|
1177
|
+
reader.PerfLogger.log(`[Perf] ✓ Texture "${textureName}": 0ms (IndexedDB cache)`);
|
|
1231
1178
|
const texture = new THREE__namespace.CompressedTexture([{ data: cached.data, width: cached.width, height: cached.height }], cached.width, cached.height, cached.format, THREE__namespace.UnsignedByteType);
|
|
1232
1179
|
texture.needsUpdate = true;
|
|
1233
1180
|
return texture;
|
|
1234
1181
|
}
|
|
1235
1182
|
}
|
|
1236
|
-
//
|
|
1183
|
+
// Decode using Basis transcoder
|
|
1184
|
+
const decodeStart = performance.now();
|
|
1237
1185
|
const arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
1238
1186
|
const blob = new Blob([arrayBuffer]);
|
|
1239
1187
|
const url = URL.createObjectURL(blob);
|
|
1240
1188
|
try {
|
|
1241
|
-
const decodeStart = performance.now();
|
|
1242
1189
|
const texture = await new Promise((resolve, reject) => {
|
|
1243
1190
|
this.ktx2Loader.load(url, (texture) => {
|
|
1244
1191
|
URL.revokeObjectURL(url);
|
|
1245
|
-
const decodeTime = performance.now() - decodeStart;
|
|
1246
|
-
reader.PerfLogger.log(`[Perf] Basis decode: ${decodeTime.toFixed(2)}ms (${textureName || 'unknown'}, ${texture.image?.width}x${texture.image?.height})`);
|
|
1247
1192
|
texture.needsUpdate = true;
|
|
1248
1193
|
resolve(texture);
|
|
1249
1194
|
}, undefined, (error) => {
|
|
@@ -1251,6 +1196,8 @@ class StowKitPack {
|
|
|
1251
1196
|
reject(error);
|
|
1252
1197
|
});
|
|
1253
1198
|
});
|
|
1199
|
+
const decodeTime = performance.now() - decodeStart;
|
|
1200
|
+
reader.PerfLogger.log(`[Perf] ✓ Texture "${textureName || 'unknown'}": ${decodeTime.toFixed(2)}ms (Basis decode ${texture.image?.width}x${texture.image?.height})`);
|
|
1254
1201
|
// Cache the transcoded texture data to IndexedDB
|
|
1255
1202
|
if (cacheKey && texture.mipmaps && texture.mipmaps.length > 0) {
|
|
1256
1203
|
const mipmap = texture.mipmaps[0];
|