@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
- const cached = await AssetMemoryCache.getGeometry(cacheKey, version);
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
- return await this.parseSkinnedMesh(metadata, data);
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
- const dataView = new DataView(data.buffer, data.byteOffset, data.byteLength);
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
- // Pre-allocate combined buffers
746
- const totalVertexCount = sortedGeometryInfos.reduce((sum, info) => sum + info.vertexCount, 0);
747
- const totalIndexCount = sortedGeometryInfos.reduce((sum, info) => sum + info.indexCount, 0);
748
- const combinedPositions = new Float32Array(totalVertexCount * 3);
749
- const combinedNormals = new Float32Array(totalVertexCount * 3);
750
- const combinedUVs = new Float32Array(totalVertexCount * 2);
751
- const combinedSkinIndices = new Uint16Array(totalVertexCount * 4);
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.clearGroups();
756
- let vertexCursor = 0; // counts vertices
757
- let indexCursor = 0; // counts indices
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
- const vertexStrideBytes = 32;
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
- // Try IndexedDB cache first
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
- const cached = await AssetMemoryCache.getTexture(cacheKey, this.packVersion);
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
- // Decode using Basis transcoder
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
  }