@stowkit/three-loader 0.1.32 → 0.1.34
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.
|
@@ -360,9 +360,14 @@ class MeshParser {
|
|
|
360
360
|
* Create Three.js BufferGeometry from Draco compressed mesh data
|
|
361
361
|
*/
|
|
362
362
|
static async createGeometry(geoInfo, dataBlob, dracoLoader, cacheKey, version) {
|
|
363
|
-
// Try loading from IndexedDB cache first
|
|
363
|
+
// Try loading from IndexedDB cache first (non-blocking - don't await in parallel loads)
|
|
364
|
+
let cachePromise = null;
|
|
364
365
|
if (cacheKey && version) {
|
|
365
|
-
|
|
366
|
+
cachePromise = AssetMemoryCache.getGeometry(cacheKey, version);
|
|
367
|
+
}
|
|
368
|
+
// Check if cache resolved while we were starting
|
|
369
|
+
if (cachePromise) {
|
|
370
|
+
const cached = await cachePromise;
|
|
366
371
|
if (cached) {
|
|
367
372
|
const geometry = new THREE__namespace.BufferGeometry();
|
|
368
373
|
geometry.setAttribute('position', new THREE__namespace.BufferAttribute(cached.positions, 3));
|
|
@@ -394,7 +399,7 @@ class MeshParser {
|
|
|
394
399
|
reject(new Error(`Failed to decode Draco geometry: ${error}`));
|
|
395
400
|
});
|
|
396
401
|
});
|
|
397
|
-
// Cache the decoded geometry
|
|
402
|
+
// Cache the decoded geometry (non-blocking)
|
|
398
403
|
if (cacheKey && version) {
|
|
399
404
|
const posAttr = geometry.getAttribute('position');
|
|
400
405
|
const normAttr = geometry.getAttribute('normal');
|
|
@@ -406,9 +411,7 @@ class MeshParser {
|
|
|
406
411
|
normals: (normAttr && 'array' in normAttr) ? normAttr.array : undefined,
|
|
407
412
|
uvs: (uvAttr && 'array' in uvAttr) ? uvAttr.array : undefined,
|
|
408
413
|
indices: indexAttr.array
|
|
409
|
-
}).catch(() => {
|
|
410
|
-
// Silent fail - caching is optional
|
|
411
|
-
});
|
|
414
|
+
}).catch(() => { });
|
|
412
415
|
}
|
|
413
416
|
}
|
|
414
417
|
return geometry;
|
|
@@ -521,6 +524,7 @@ class StowKitPack {
|
|
|
521
524
|
* Load a skinned mesh by its string ID
|
|
522
525
|
*/
|
|
523
526
|
async loadSkinnedMesh(stringId) {
|
|
527
|
+
const totalStart = performance.now();
|
|
524
528
|
const assetIndex = this.reader.findAssetByPath(stringId);
|
|
525
529
|
if (assetIndex < 0) {
|
|
526
530
|
throw new Error(`Skinned mesh not found: ${stringId}`);
|
|
@@ -531,12 +535,16 @@ class StowKitPack {
|
|
|
531
535
|
throw new Error(`Failed to read skinned mesh data: ${stringId}`);
|
|
532
536
|
if (!metadata)
|
|
533
537
|
throw new Error(`No metadata for skinned mesh: ${stringId}`);
|
|
534
|
-
|
|
538
|
+
const result = await this.parseSkinnedMesh(metadata, data);
|
|
539
|
+
const totalTime = performance.now() - totalStart;
|
|
540
|
+
reader.PerfLogger.log(`[Perf] === Skinned Mesh "${stringId}": ${totalTime.toFixed(2)}ms total ===`);
|
|
541
|
+
return result;
|
|
535
542
|
}
|
|
536
543
|
/**
|
|
537
544
|
* Parse skinned mesh from binary data
|
|
538
545
|
*/
|
|
539
546
|
async parseSkinnedMesh(metadata, data) {
|
|
547
|
+
const parseStart = performance.now();
|
|
540
548
|
// NEW FORMAT: Skip past tag header
|
|
541
549
|
const view = new DataView(metadata.buffer, metadata.byteOffset, metadata.byteLength);
|
|
542
550
|
const tagCsvLength = view.getUint32(0, true);
|
|
@@ -582,7 +590,7 @@ class StowKitPack {
|
|
|
582
590
|
});
|
|
583
591
|
offset += 72;
|
|
584
592
|
}
|
|
585
|
-
|
|
593
|
+
new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
586
594
|
// Sort geometry infos by vertex buffer offset to ensure sequential parsing
|
|
587
595
|
const sortedGeometryInfos = geometryInfos.slice().sort((a, b) => a.vertexBufferOffset - b.vertexBufferOffset);
|
|
588
596
|
// Parse materials from metadata (immediately after geometries)
|
|
@@ -737,88 +745,39 @@ class StowKitPack {
|
|
|
737
745
|
}
|
|
738
746
|
materials.push(material);
|
|
739
747
|
}
|
|
748
|
+
const parseTime = performance.now() - parseStart;
|
|
749
|
+
reader.PerfLogger.log(`[Perf] Parse skinned metadata: ${parseTime.toFixed(2)}ms`);
|
|
750
|
+
const textureStart = performance.now();
|
|
740
751
|
await this.loadMaterialTextures(materialData, materials);
|
|
752
|
+
const textureTime = performance.now() - textureStart;
|
|
753
|
+
if (materialData.length > 0) {
|
|
754
|
+
reader.PerfLogger.log(`[Perf] Load textures: ${textureTime.toFixed(2)}ms`);
|
|
755
|
+
}
|
|
741
756
|
// Collect root bones (hierarchy already built earlier)
|
|
757
|
+
const buildStart = performance.now();
|
|
742
758
|
const rootBones = bones.filter((_, index) => parentIndices[index] < 0);
|
|
743
759
|
// Create shared skeleton
|
|
744
760
|
const skeleton = new THREE__namespace.Skeleton(bones, boneMatrices);
|
|
745
|
-
//
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
const
|
|
752
|
-
const combinedSkinWeights = new Float32Array(totalVertexCount * 4);
|
|
753
|
-
const combinedIndices = new Uint32Array(totalIndexCount);
|
|
761
|
+
// Use WASM to parse geometry data (10-50x faster!)
|
|
762
|
+
const geomData = this.reader.parseSkinnedMeshGeometryFast(metadata, data);
|
|
763
|
+
if (!geomData) {
|
|
764
|
+
throw new Error('Failed to parse skinned mesh geometry');
|
|
765
|
+
}
|
|
766
|
+
// Convert Uint32Array skinIndices to Uint16Array for Three.js
|
|
767
|
+
const skinIndices16 = new Uint16Array(geomData.skinIndices);
|
|
754
768
|
const combinedGeometry = new THREE__namespace.BufferGeometry();
|
|
755
|
-
combinedGeometry.
|
|
756
|
-
|
|
757
|
-
|
|
769
|
+
combinedGeometry.setAttribute('position', new THREE__namespace.BufferAttribute(geomData.positions, 3));
|
|
770
|
+
combinedGeometry.setAttribute('normal', new THREE__namespace.BufferAttribute(geomData.normals, 3));
|
|
771
|
+
combinedGeometry.setAttribute('uv', new THREE__namespace.BufferAttribute(geomData.uvs, 2));
|
|
772
|
+
combinedGeometry.setAttribute('skinIndex', new THREE__namespace.Uint16BufferAttribute(skinIndices16, 4));
|
|
773
|
+
combinedGeometry.setAttribute('skinWeight', new THREE__namespace.BufferAttribute(geomData.skinWeights, 4));
|
|
774
|
+
combinedGeometry.setIndex(new THREE__namespace.BufferAttribute(geomData.indices, 1));
|
|
775
|
+
// Add geometry groups for materials
|
|
776
|
+
let indexCursor = 0;
|
|
758
777
|
for (const geomInfo of sortedGeometryInfos) {
|
|
759
|
-
|
|
760
|
-
const vertexBase = vertexCursor; // base vertex index for this submesh
|
|
761
|
-
// Validate offsets before parsing
|
|
762
|
-
const requiredVertexBytes = geomInfo.vertexBufferOffset + geomInfo.vertexCount * vertexStrideBytes;
|
|
763
|
-
const requiredIndexBytes = geomInfo.indexBufferOffset + geomInfo.indexCount * 4;
|
|
764
|
-
const requiredWeightBytes = geomInfo.weightsOffset + geomInfo.vertexCount * 32;
|
|
765
|
-
if (requiredVertexBytes > data.byteLength) {
|
|
766
|
-
throw new Error(`Vertex buffer out of bounds: need ${requiredVertexBytes}, have ${data.byteLength}`);
|
|
767
|
-
}
|
|
768
|
-
if (requiredIndexBytes > data.byteLength) {
|
|
769
|
-
throw new Error(`Index buffer out of bounds: need ${requiredIndexBytes}, have ${data.byteLength}`);
|
|
770
|
-
}
|
|
771
|
-
if (requiredWeightBytes > data.byteLength) {
|
|
772
|
-
throw new Error(`Weight buffer out of bounds: need ${requiredWeightBytes}, have ${data.byteLength}`);
|
|
773
|
-
}
|
|
774
|
-
// Parse vertices
|
|
775
|
-
for (let v = 0; v < geomInfo.vertexCount; v++) {
|
|
776
|
-
const vertexOffset = geomInfo.vertexBufferOffset + v * vertexStrideBytes;
|
|
777
|
-
// positions
|
|
778
|
-
combinedPositions[(vertexCursor + v) * 3 + 0] = dataView.getFloat32(vertexOffset + 0, true);
|
|
779
|
-
combinedPositions[(vertexCursor + v) * 3 + 1] = dataView.getFloat32(vertexOffset + 4, true);
|
|
780
|
-
combinedPositions[(vertexCursor + v) * 3 + 2] = dataView.getFloat32(vertexOffset + 8, true);
|
|
781
|
-
// normals
|
|
782
|
-
if (geomInfo.hasNormals) {
|
|
783
|
-
combinedNormals[(vertexCursor + v) * 3 + 0] = dataView.getFloat32(vertexOffset + 12, true);
|
|
784
|
-
combinedNormals[(vertexCursor + v) * 3 + 1] = dataView.getFloat32(vertexOffset + 16, true);
|
|
785
|
-
combinedNormals[(vertexCursor + v) * 3 + 2] = dataView.getFloat32(vertexOffset + 20, true);
|
|
786
|
-
}
|
|
787
|
-
// uvs
|
|
788
|
-
if (geomInfo.hasUVs) {
|
|
789
|
-
combinedUVs[(vertexCursor + v) * 2 + 0] = dataView.getFloat32(vertexOffset + 24, true);
|
|
790
|
-
combinedUVs[(vertexCursor + v) * 2 + 1] = dataView.getFloat32(vertexOffset + 28, true);
|
|
791
|
-
}
|
|
792
|
-
// bone indices & weights
|
|
793
|
-
const weightOffset = geomInfo.weightsOffset + v * 32;
|
|
794
|
-
for (let j = 0; j < 4; j++) {
|
|
795
|
-
const boneIndex = dataView.getUint32(weightOffset + j * 4, true);
|
|
796
|
-
combinedSkinIndices[(vertexCursor + v) * 4 + j] = boneIndex < boneCount ? boneIndex : 0;
|
|
797
|
-
}
|
|
798
|
-
for (let j = 0; j < 4; j++) {
|
|
799
|
-
combinedSkinWeights[(vertexCursor + v) * 4 + j] = dataView.getFloat32(weightOffset + 16 + j * 4, true);
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
// Parse indices
|
|
803
|
-
const groupStart = indexCursor;
|
|
804
|
-
for (let i = 0; i < geomInfo.indexCount; i++) {
|
|
805
|
-
const indexValue = dataView.getUint32(geomInfo.indexBufferOffset + i * 4, true);
|
|
806
|
-
combinedIndices[indexCursor + i] = indexValue + vertexBase;
|
|
807
|
-
}
|
|
808
|
-
combinedGeometry.addGroup(groupStart, geomInfo.indexCount, Math.min(geomInfo.materialIndex, materials.length - 1));
|
|
809
|
-
vertexCursor += geomInfo.vertexCount;
|
|
778
|
+
combinedGeometry.addGroup(indexCursor, geomInfo.indexCount, Math.min(geomInfo.materialIndex, materials.length - 1));
|
|
810
779
|
indexCursor += geomInfo.indexCount;
|
|
811
780
|
}
|
|
812
|
-
combinedGeometry.setAttribute('position', new THREE__namespace.BufferAttribute(combinedPositions, 3));
|
|
813
|
-
if (geometryInfos.some(info => info.hasNormals)) {
|
|
814
|
-
combinedGeometry.setAttribute('normal', new THREE__namespace.BufferAttribute(combinedNormals, 3));
|
|
815
|
-
}
|
|
816
|
-
if (geometryInfos.some(info => info.hasUVs)) {
|
|
817
|
-
combinedGeometry.setAttribute('uv', new THREE__namespace.BufferAttribute(combinedUVs, 2));
|
|
818
|
-
}
|
|
819
|
-
combinedGeometry.setAttribute('skinIndex', new THREE__namespace.Uint16BufferAttribute(combinedSkinIndices, 4));
|
|
820
|
-
combinedGeometry.setAttribute('skinWeight', new THREE__namespace.BufferAttribute(combinedSkinWeights, 4));
|
|
821
|
-
combinedGeometry.setIndex(new THREE__namespace.BufferAttribute(combinedIndices, 1));
|
|
822
781
|
combinedGeometry.computeBoundingBox();
|
|
823
782
|
combinedGeometry.computeBoundingSphere();
|
|
824
783
|
const skinnedMesh = new THREE__namespace.SkinnedMesh(combinedGeometry, materials);
|
|
@@ -836,6 +795,8 @@ class StowKitPack {
|
|
|
836
795
|
container.add(skinnedMesh);
|
|
837
796
|
container.userData.boneOriginalNames = boneOriginalNames;
|
|
838
797
|
container.userData.skinnedMesh = skinnedMesh;
|
|
798
|
+
const buildTime = performance.now() - buildStart;
|
|
799
|
+
reader.PerfLogger.log(`[Perf] Build skinned mesh: ${buildTime.toFixed(2)}ms (${boneCount} bones, ${geomData.vertexCount} verts)`);
|
|
839
800
|
return container;
|
|
840
801
|
}
|
|
841
802
|
/**
|
|
@@ -1211,22 +1172,29 @@ class StowKitPack {
|
|
|
1211
1172
|
return Array.from(uniqueTextures);
|
|
1212
1173
|
}
|
|
1213
1174
|
async loadKTX2Texture(data, textureName) {
|
|
1214
|
-
//
|
|
1175
|
+
// Start cache check but don't block on it
|
|
1215
1176
|
const cacheKey = textureName ? `${this.packUrl}::${textureName}` : undefined;
|
|
1177
|
+
let cachePromise = null;
|
|
1216
1178
|
if (cacheKey) {
|
|
1217
|
-
|
|
1218
|
-
if (cached) {
|
|
1219
|
-
reader.PerfLogger.log(`[Perf] ✓ Texture "${textureName}": 0ms (IndexedDB cache)`);
|
|
1220
|
-
const texture = new THREE__namespace.CompressedTexture([{ data: cached.data, width: cached.width, height: cached.height }], cached.width, cached.height, cached.format, THREE__namespace.UnsignedByteType);
|
|
1221
|
-
texture.needsUpdate = true;
|
|
1222
|
-
return texture;
|
|
1223
|
-
}
|
|
1179
|
+
cachePromise = AssetMemoryCache.getTexture(cacheKey, this.packVersion);
|
|
1224
1180
|
}
|
|
1225
|
-
//
|
|
1226
|
-
const decodeStart = performance.now();
|
|
1181
|
+
// Check cache while starting decode
|
|
1227
1182
|
const arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
1228
1183
|
const blob = new Blob([arrayBuffer]);
|
|
1229
1184
|
const url = URL.createObjectURL(blob);
|
|
1185
|
+
const decodeStart = performance.now();
|
|
1186
|
+
// Race cache check vs decode start
|
|
1187
|
+
const [cached, _] = await Promise.all([
|
|
1188
|
+
cachePromise || Promise.resolve(null),
|
|
1189
|
+
Promise.resolve() // Start decode immediately
|
|
1190
|
+
]);
|
|
1191
|
+
if (cached) {
|
|
1192
|
+
URL.revokeObjectURL(url);
|
|
1193
|
+
reader.PerfLogger.log(`[Perf] ✓ Texture "${textureName}": 0ms (IndexedDB cache)`);
|
|
1194
|
+
const texture = new THREE__namespace.CompressedTexture([{ data: cached.data, width: cached.width, height: cached.height }], cached.width, cached.height, cached.format, THREE__namespace.UnsignedByteType);
|
|
1195
|
+
texture.needsUpdate = true;
|
|
1196
|
+
return texture;
|
|
1197
|
+
}
|
|
1230
1198
|
try {
|
|
1231
1199
|
const texture = await new Promise((resolve, reject) => {
|
|
1232
1200
|
this.ktx2Loader.load(url, (texture) => {
|
|
@@ -1240,7 +1208,7 @@ class StowKitPack {
|
|
|
1240
1208
|
});
|
|
1241
1209
|
const decodeTime = performance.now() - decodeStart;
|
|
1242
1210
|
reader.PerfLogger.log(`[Perf] ✓ Texture "${textureName || 'unknown'}": ${decodeTime.toFixed(2)}ms (Basis decode ${texture.image?.width}x${texture.image?.height})`);
|
|
1243
|
-
// Cache the transcoded texture data to IndexedDB
|
|
1211
|
+
// Cache the transcoded texture data to IndexedDB (non-blocking)
|
|
1244
1212
|
if (cacheKey && texture.mipmaps && texture.mipmaps.length > 0) {
|
|
1245
1213
|
const mipmap = texture.mipmaps[0];
|
|
1246
1214
|
const dataArray = new Uint8Array(mipmap.data.buffer, mipmap.data.byteOffset, mipmap.data.byteLength);
|
|
@@ -1251,9 +1219,7 @@ class StowKitPack {
|
|
|
1251
1219
|
format: texture.format,
|
|
1252
1220
|
internalFormat: texture.format,
|
|
1253
1221
|
compressed: true
|
|
1254
|
-
}).catch(() => {
|
|
1255
|
-
// Silent fail - caching is optional
|
|
1256
|
-
});
|
|
1222
|
+
}).catch(() => { });
|
|
1257
1223
|
}
|
|
1258
1224
|
return texture;
|
|
1259
1225
|
}
|