@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
- return await this.parseSkinnedMesh(metadata, data);
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
- const dataView = new DataView(data.buffer, data.byteOffset, data.byteLength);
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
- // Pre-allocate combined buffers
751
- const totalVertexCount = sortedGeometryInfos.reduce((sum, info) => sum + info.vertexCount, 0);
752
- const totalIndexCount = sortedGeometryInfos.reduce((sum, info) => sum + info.indexCount, 0);
753
- const combinedPositions = new Float32Array(totalVertexCount * 3);
754
- const combinedNormals = new Float32Array(totalVertexCount * 3);
755
- const combinedUVs = new Float32Array(totalVertexCount * 2);
756
- const combinedSkinIndices = new Uint16Array(totalVertexCount * 4);
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.clearGroups();
761
- let vertexCursor = 0; // counts vertices
762
- let indexCursor = 0; // counts indices
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
- const vertexStrideBytes = 32;
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
- const textureStart = performance.now();
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 cache hit (IndexedDB): ${textureName} (${(performance.now() - startCache).toFixed(2)}ms)`);
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
- // Create blob URL - KTX2Loader requires a URL, can't use ArrayBuffer directly
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];