@stowkit/three-loader 0.1.2 → 0.1.4
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 +548 -80
- package/dist/MeshParser.d.ts +9 -11
- package/dist/MeshParser.d.ts.map +1 -1
- package/dist/StowKitLoader.d.ts +13 -0
- package/dist/StowKitLoader.d.ts.map +1 -1
- package/dist/stowkit-three-loader.esm.js +107 -97
- package/dist/stowkit-three-loader.esm.js.map +1 -1
- package/dist/stowkit-three-loader.js +106 -96
- package/dist/stowkit-three-loader.js.map +1 -1
- package/package.json +5 -3
- package/public/draco/draco_decoder.js +33 -0
- package/public/draco/draco_decoder.wasm +0 -0
- package/public/draco/draco_wasm_wrapper.js +116 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var THREE = require('three');
|
|
4
4
|
var KTX2Loader_js = require('three/examples/jsm/loaders/KTX2Loader.js');
|
|
5
|
+
var DRACOLoader_js = require('three/examples/jsm/loaders/DRACOLoader.js');
|
|
5
6
|
var reader = require('@stowkit/reader');
|
|
6
7
|
|
|
7
8
|
function _interopNamespaceDefault(e) {
|
|
@@ -51,8 +52,8 @@ class MeshParser {
|
|
|
51
52
|
console.log(`Mesh counts: ${metadata.meshGeometryCount} geometries, ${metadata.materialCount} materials, ${metadata.nodeCount} nodes`);
|
|
52
53
|
// Check if the metadata blob contains just the header or all the mesh info
|
|
53
54
|
const expectedMetadataSize = 140 +
|
|
54
|
-
(metadata.meshGeometryCount *
|
|
55
|
-
(metadata.materialCount *
|
|
55
|
+
(metadata.meshGeometryCount * 40) + // Draco compressed (40 bytes each)
|
|
56
|
+
(metadata.materialCount * 196) + // MaterialData header
|
|
56
57
|
(metadata.nodeCount * 116);
|
|
57
58
|
let hasExtendedMetadata = metadataBlob.length >= expectedMetadataSize;
|
|
58
59
|
console.log(`Expected metadata size: ${expectedMetadataSize}, actual: ${metadataBlob.length}, has extended: ${hasExtendedMetadata}`);
|
|
@@ -60,28 +61,25 @@ class MeshParser {
|
|
|
60
61
|
const sourceBlob = hasExtendedMetadata ? metadataBlob : dataBlob;
|
|
61
62
|
const metaView = new DataView(sourceBlob.buffer, sourceBlob.byteOffset, sourceBlob.byteLength);
|
|
62
63
|
let offset = hasExtendedMetadata ? 140 : 0; // Start after MeshMetadata if in metadata blob, or at beginning of data blob
|
|
63
|
-
// Parse geometry infos
|
|
64
|
+
// Parse geometry infos (now Draco compressed!)
|
|
64
65
|
const geometries = [];
|
|
65
66
|
for (let i = 0; i < metadata.meshGeometryCount; i++) {
|
|
66
|
-
if (offset +
|
|
67
|
+
if (offset + 40 > sourceBlob.length) {
|
|
67
68
|
console.warn(`Data truncated at geometry ${i}/${metadata.meshGeometryCount}`);
|
|
68
69
|
break;
|
|
69
70
|
}
|
|
70
71
|
const geo = {
|
|
71
72
|
vertexCount: metaView.getUint32(offset, true),
|
|
72
73
|
indexCount: metaView.getUint32(offset + 4, true),
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
normalBufferSize: Number(metaView.getBigUint64(offset + 56, true)),
|
|
80
|
-
uvBufferSize: Number(metaView.getBigUint64(offset + 64, true)),
|
|
81
|
-
materialIndex: metaView.getUint32(offset + 72, true)
|
|
74
|
+
hasNormals: metaView.getUint32(offset + 8, true),
|
|
75
|
+
hasUVs: metaView.getUint32(offset + 12, true),
|
|
76
|
+
compressedBufferOffset: Number(metaView.getBigUint64(offset + 16, true)),
|
|
77
|
+
compressedBufferSize: Number(metaView.getBigUint64(offset + 24, true)),
|
|
78
|
+
materialIndex: metaView.getUint32(offset + 32, true),
|
|
79
|
+
padding: metaView.getUint32(offset + 36, true)
|
|
82
80
|
};
|
|
83
81
|
geometries.push(geo);
|
|
84
|
-
offset +=
|
|
82
|
+
offset += 40; // Size of MeshGeometryInfo (Draco compressed)
|
|
85
83
|
}
|
|
86
84
|
// Parse materials - MaterialData structure: name[64] + schema_id[128] + property_count(4) = 196 bytes
|
|
87
85
|
const materialData = [];
|
|
@@ -208,76 +206,40 @@ class MeshParser {
|
|
|
208
206
|
return { metadata, geometries, materials, materialData, nodes, meshIndices };
|
|
209
207
|
}
|
|
210
208
|
/**
|
|
211
|
-
* Create Three.js BufferGeometry from mesh data
|
|
209
|
+
* Create Three.js BufferGeometry from Draco compressed mesh data
|
|
212
210
|
*/
|
|
213
|
-
static createGeometry(geoInfo, dataBlob) {
|
|
214
|
-
console.log('
|
|
211
|
+
static async createGeometry(geoInfo, dataBlob, dracoLoader) {
|
|
212
|
+
console.log('Decoding Draco geometry:', {
|
|
215
213
|
vertexCount: geoInfo.vertexCount,
|
|
216
214
|
indexCount: geoInfo.indexCount,
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
215
|
+
hasNormals: geoInfo.hasNormals,
|
|
216
|
+
hasUVs: geoInfo.hasUVs,
|
|
217
|
+
compressedOffset: geoInfo.compressedBufferOffset,
|
|
218
|
+
compressedSize: geoInfo.compressedBufferSize
|
|
220
219
|
});
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (geoInfo.vertexBufferSize > 0 && geoInfo.vertexBufferOffset + geoInfo.vertexBufferSize <= dataBlob.length) {
|
|
225
|
-
const positions = new Float32Array(geoInfo.vertexCount * 3);
|
|
226
|
-
for (let i = 0; i < geoInfo.vertexCount * 3; i++) {
|
|
227
|
-
positions[i] = dataView.getFloat32(geoInfo.vertexBufferOffset + i * 4, true);
|
|
228
|
-
}
|
|
229
|
-
geometry.setAttribute('position', new THREE__namespace.BufferAttribute(positions, 3));
|
|
230
|
-
console.log('Added positions:', positions.slice(0, 9)); // First 3 vertices
|
|
231
|
-
}
|
|
232
|
-
else {
|
|
233
|
-
console.warn('No vertex data or out of bounds');
|
|
234
|
-
}
|
|
235
|
-
// Extract normals
|
|
236
|
-
if (geoInfo.normalBufferSize > 0 && geoInfo.normalBufferOffset + geoInfo.normalBufferSize <= dataBlob.length) {
|
|
237
|
-
const normals = new Float32Array(geoInfo.vertexCount * 3);
|
|
238
|
-
for (let i = 0; i < geoInfo.vertexCount * 3; i++) {
|
|
239
|
-
normals[i] = dataView.getFloat32(geoInfo.normalBufferOffset + i * 4, true);
|
|
240
|
-
}
|
|
241
|
-
geometry.setAttribute('normal', new THREE__namespace.BufferAttribute(normals, 3));
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
244
|
-
// Compute normals if not provided
|
|
245
|
-
geometry.computeVertexNormals();
|
|
246
|
-
}
|
|
247
|
-
// Extract UVs
|
|
248
|
-
if (geoInfo.uvBufferSize > 0 && geoInfo.uvBufferOffset + geoInfo.uvBufferSize <= dataBlob.length) {
|
|
249
|
-
const uvs = new Float32Array(geoInfo.vertexCount * 2);
|
|
250
|
-
for (let i = 0; i < geoInfo.vertexCount * 2; i += 2) {
|
|
251
|
-
uvs[i] = dataView.getFloat32(geoInfo.uvBufferOffset + i * 4, true); // U
|
|
252
|
-
uvs[i + 1] = 1.0 - dataView.getFloat32(geoInfo.uvBufferOffset + (i + 1) * 4, true); // V (flipped)
|
|
253
|
-
}
|
|
254
|
-
geometry.setAttribute('uv', new THREE__namespace.BufferAttribute(uvs, 2));
|
|
255
|
-
}
|
|
256
|
-
// Extract indices
|
|
257
|
-
if (geoInfo.indexCount > 0 && geoInfo.indexBufferOffset + geoInfo.indexBufferSize <= dataBlob.length) {
|
|
258
|
-
const indices = new Uint32Array(geoInfo.indexCount);
|
|
259
|
-
// Read the first index to determine the base offset
|
|
260
|
-
let minIndex = Number.MAX_SAFE_INTEGER;
|
|
261
|
-
for (let i = 0; i < geoInfo.indexCount; i++) {
|
|
262
|
-
const index = dataView.getUint32(geoInfo.indexBufferOffset + i * 4, true);
|
|
263
|
-
if (index < minIndex)
|
|
264
|
-
minIndex = index;
|
|
265
|
-
}
|
|
266
|
-
// Read indices again and offset them to start from 0
|
|
267
|
-
for (let i = 0; i < geoInfo.indexCount; i++) {
|
|
268
|
-
const absoluteIndex = dataView.getUint32(geoInfo.indexBufferOffset + i * 4, true);
|
|
269
|
-
indices[i] = absoluteIndex - minIndex;
|
|
270
|
-
}
|
|
271
|
-
geometry.setIndex(new THREE__namespace.BufferAttribute(indices, 1));
|
|
272
|
-
console.log(`Added indices (offset by ${minIndex}):`, indices.slice(0, 12)); // First 4 triangles
|
|
273
|
-
}
|
|
274
|
-
else if (geoInfo.indexCount > 0) {
|
|
275
|
-
console.warn('Index data out of bounds');
|
|
276
|
-
}
|
|
277
|
-
// Ensure we have normals (compute if missing)
|
|
278
|
-
if (!geometry.attributes.normal) {
|
|
279
|
-
geometry.computeVertexNormals();
|
|
220
|
+
// Extract the Draco compressed buffer
|
|
221
|
+
if (geoInfo.compressedBufferOffset + geoInfo.compressedBufferSize > dataBlob.length) {
|
|
222
|
+
throw new Error(`Compressed buffer out of bounds: offset=${geoInfo.compressedBufferOffset}, size=${geoInfo.compressedBufferSize}, dataLength=${dataBlob.length}`);
|
|
280
223
|
}
|
|
224
|
+
const compressedData = dataBlob.slice(geoInfo.compressedBufferOffset, geoInfo.compressedBufferOffset + geoInfo.compressedBufferSize);
|
|
225
|
+
// Decode using Draco
|
|
226
|
+
// Create a blob URL for the Draco data (DRACOLoader expects a URL)
|
|
227
|
+
const arrayBuffer = compressedData.buffer.slice(compressedData.byteOffset, compressedData.byteOffset + compressedData.byteLength);
|
|
228
|
+
const blob = new Blob([arrayBuffer]);
|
|
229
|
+
const url = URL.createObjectURL(blob);
|
|
230
|
+
const geometry = await new Promise((resolve, reject) => {
|
|
231
|
+
dracoLoader.load(url, (decoded) => {
|
|
232
|
+
URL.revokeObjectURL(url);
|
|
233
|
+
console.log('Draco decoded successfully:', decoded);
|
|
234
|
+
// UVs are pre-flipped in the packer now, no need to flip here
|
|
235
|
+
resolve(decoded);
|
|
236
|
+
}, undefined, (error) => {
|
|
237
|
+
URL.revokeObjectURL(url);
|
|
238
|
+
console.error('Draco decode failed:', error);
|
|
239
|
+
reject(new Error(`Failed to decode Draco geometry: ${error}`));
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
// Compute bounding volumes
|
|
281
243
|
geometry.computeBoundingSphere();
|
|
282
244
|
geometry.computeBoundingBox();
|
|
283
245
|
console.log('Final geometry:', {
|
|
@@ -292,7 +254,7 @@ class MeshParser {
|
|
|
292
254
|
/**
|
|
293
255
|
* Build Three.js scene from parsed mesh data
|
|
294
256
|
*/
|
|
295
|
-
static buildScene(parsedData, dataBlob) {
|
|
257
|
+
static async buildScene(parsedData, dataBlob, dracoLoader) {
|
|
296
258
|
const root = new THREE__namespace.Group();
|
|
297
259
|
root.name = 'StowKitMesh';
|
|
298
260
|
const { geometries, materials, nodes, meshIndices } = parsedData;
|
|
@@ -320,7 +282,7 @@ class MeshParser {
|
|
|
320
282
|
if (meshIndex < geometries.length) {
|
|
321
283
|
const geoInfo = geometries[meshIndex];
|
|
322
284
|
console.log(` Creating geometry from geoInfo: vertices=${geoInfo.vertexCount}, indices=${geoInfo.indexCount}, material=${geoInfo.materialIndex}`);
|
|
323
|
-
const geometry = this.createGeometry(geoInfo, dataBlob);
|
|
285
|
+
const geometry = await this.createGeometry(geoInfo, dataBlob, dracoLoader);
|
|
324
286
|
// Use assigned material if valid, otherwise create default
|
|
325
287
|
let material;
|
|
326
288
|
if (geoInfo.materialIndex < materials.length) {
|
|
@@ -363,9 +325,10 @@ class MeshParser {
|
|
|
363
325
|
// If no nodes, create a single mesh from all geometries
|
|
364
326
|
if (nodes.length === 0 && geometries.length > 0) {
|
|
365
327
|
console.log('No nodes found, creating meshes directly from geometries');
|
|
366
|
-
geometries.
|
|
328
|
+
for (let index = 0; index < geometries.length; index++) {
|
|
329
|
+
const geoInfo = geometries[index];
|
|
367
330
|
console.log(`Creating direct mesh ${index}: materialIndex=${geoInfo.materialIndex}, vertices=${geoInfo.vertexCount}`);
|
|
368
|
-
const geometry = this.createGeometry(geoInfo, dataBlob);
|
|
331
|
+
const geometry = await this.createGeometry(geoInfo, dataBlob, dracoLoader);
|
|
369
332
|
// Use assigned material if valid, otherwise create default
|
|
370
333
|
let material;
|
|
371
334
|
if (geoInfo.materialIndex < materials.length) {
|
|
@@ -384,7 +347,7 @@ class MeshParser {
|
|
|
384
347
|
mesh.name = `Mesh_${index}`;
|
|
385
348
|
root.add(mesh);
|
|
386
349
|
console.log(`Added mesh ${index} to root`);
|
|
387
|
-
}
|
|
350
|
+
}
|
|
388
351
|
}
|
|
389
352
|
console.log(`Final scene has ${root.children.length} direct children`);
|
|
390
353
|
// Debug: traverse and count all meshes, showing full hierarchy
|
|
@@ -451,16 +414,26 @@ class StowKitLoader extends THREE__namespace.Loader {
|
|
|
451
414
|
this.reader = new reader.StowKitReader(wasmPath);
|
|
452
415
|
this.ownedReader = true;
|
|
453
416
|
}
|
|
454
|
-
// Setup KTX2 loader for textures with default path
|
|
455
|
-
const transcoderPath = parameters?.transcoderPath || '/basis/';
|
|
417
|
+
// Setup KTX2 loader for textures with default path (user must provide basis files)
|
|
418
|
+
const transcoderPath = parameters?.transcoderPath || '/basis/'; // Users still need to provide this
|
|
456
419
|
this.ktx2Loader = new KTX2Loader_js.KTX2Loader(manager);
|
|
457
420
|
this.ktx2Loader.setTranscoderPath(transcoderPath);
|
|
421
|
+
// Setup Draco loader for mesh decompression
|
|
422
|
+
this.dracoLoader = new DRACOLoader_js.DRACOLoader(manager);
|
|
423
|
+
this.dracoLoader.setDecoderPath('/stowkit/draco/'); // Draco files copied by postinstall
|
|
458
424
|
// Detect support with a temporary renderer
|
|
459
425
|
// This is required for KTX2Loader to work
|
|
460
426
|
const tempRenderer = new THREE__namespace.WebGLRenderer();
|
|
461
427
|
this.ktx2Loader.detectSupport(tempRenderer);
|
|
462
428
|
tempRenderer.dispose();
|
|
463
429
|
}
|
|
430
|
+
/**
|
|
431
|
+
* Set the path to the Draco decoder
|
|
432
|
+
*/
|
|
433
|
+
setDracoDecoderPath(path) {
|
|
434
|
+
this.dracoLoader.setDecoderPath(path);
|
|
435
|
+
return this;
|
|
436
|
+
}
|
|
464
437
|
/**
|
|
465
438
|
* Set the path to the Basis Universal transcoder
|
|
466
439
|
*/
|
|
@@ -490,6 +463,42 @@ class StowKitLoader extends THREE__namespace.Loader {
|
|
|
490
463
|
const arrayBuffer = await response.arrayBuffer();
|
|
491
464
|
await this.reader.open(arrayBuffer);
|
|
492
465
|
}
|
|
466
|
+
/**
|
|
467
|
+
* Load and parse a mesh asset by its index
|
|
468
|
+
* @param index - Asset index
|
|
469
|
+
* @param onLoad - Callback when loading completes
|
|
470
|
+
* @param onProgress - Progress callback
|
|
471
|
+
* @param onError - Error callback
|
|
472
|
+
*/
|
|
473
|
+
async loadMeshByIndex(index, onLoad, onProgress, onError) {
|
|
474
|
+
try {
|
|
475
|
+
// Read mesh data and metadata
|
|
476
|
+
const data = this.reader.readAssetData(index);
|
|
477
|
+
const metadata = this.reader.readAssetMetadata(index);
|
|
478
|
+
if (!data) {
|
|
479
|
+
throw new Error(`Failed to read mesh data for index ${index}`);
|
|
480
|
+
}
|
|
481
|
+
if (!metadata) {
|
|
482
|
+
throw new Error(`No metadata available for index ${index}`);
|
|
483
|
+
}
|
|
484
|
+
// Parse mesh data
|
|
485
|
+
const parsedData = MeshParser.parseMeshData(metadata, data);
|
|
486
|
+
// Load textures for materials
|
|
487
|
+
await this.loadMaterialTextures(parsedData.materialData, parsedData.materials);
|
|
488
|
+
// Build Three.js scene with Draco decoder
|
|
489
|
+
const scene = await MeshParser.buildScene(parsedData, data, this.dracoLoader);
|
|
490
|
+
if (onLoad) {
|
|
491
|
+
onLoad(scene);
|
|
492
|
+
}
|
|
493
|
+
return scene;
|
|
494
|
+
}
|
|
495
|
+
catch (error) {
|
|
496
|
+
if (onError) {
|
|
497
|
+
onError(error);
|
|
498
|
+
}
|
|
499
|
+
throw error;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
493
502
|
/**
|
|
494
503
|
* Load and parse a mesh asset from a StowKit pack
|
|
495
504
|
* @param url - URL to the .stow file (or omit if already opened with openPack)
|
|
@@ -522,8 +531,8 @@ class StowKitLoader extends THREE__namespace.Loader {
|
|
|
522
531
|
const parsedData = MeshParser.parseMeshData(metadata, data);
|
|
523
532
|
// Load textures for materials
|
|
524
533
|
await this.loadMaterialTextures(parsedData.materialData, parsedData.materials);
|
|
525
|
-
// Build Three.js scene
|
|
526
|
-
const scene = MeshParser.buildScene(parsedData, data);
|
|
534
|
+
// Build Three.js scene with Draco decoder
|
|
535
|
+
const scene = await MeshParser.buildScene(parsedData, data, this.dracoLoader);
|
|
527
536
|
if (onLoad) {
|
|
528
537
|
onLoad(scene);
|
|
529
538
|
}
|
|
@@ -759,20 +768,20 @@ class StowKitLoader extends THREE__namespace.Loader {
|
|
|
759
768
|
if (assetIndex < 0)
|
|
760
769
|
return null;
|
|
761
770
|
const metadata = this.reader.readAssetMetadata(assetIndex);
|
|
762
|
-
if (!metadata || metadata.length <
|
|
763
|
-
return null;
|
|
771
|
+
if (!metadata || metadata.length < 144)
|
|
772
|
+
return null; // TextureMetadata is now 144 bytes
|
|
764
773
|
const view = new DataView(metadata.buffer, metadata.byteOffset, metadata.byteLength);
|
|
765
774
|
const decoder = new TextDecoder();
|
|
766
|
-
// Parse TextureMetadata structure
|
|
767
|
-
const stringIdBytes = metadata.slice(
|
|
775
|
+
// Parse TextureMetadata structure (width, height, channels, channel_format, string_id[128])
|
|
776
|
+
const stringIdBytes = metadata.slice(16, 144); // After 4 uint32s (16 bytes)
|
|
768
777
|
const nullIdx = stringIdBytes.indexOf(0);
|
|
769
778
|
const stringId = decoder.decode(stringIdBytes.slice(0, nullIdx >= 0 ? nullIdx : 128));
|
|
770
779
|
return {
|
|
771
780
|
width: view.getUint32(0, true),
|
|
772
781
|
height: view.getUint32(4, true),
|
|
773
782
|
channels: view.getUint32(8, true),
|
|
774
|
-
isKtx2:
|
|
775
|
-
channelFormat: view.getUint32(
|
|
783
|
+
isKtx2: true, // All textures are KTX2 now
|
|
784
|
+
channelFormat: view.getUint32(12, true),
|
|
776
785
|
stringId
|
|
777
786
|
};
|
|
778
787
|
}
|
|
@@ -901,8 +910,8 @@ class StowKitLoader extends THREE__namespace.Loader {
|
|
|
901
910
|
const materialCount = view.getUint32(4, true);
|
|
902
911
|
const nodeCount = view.getUint32(8, true);
|
|
903
912
|
let offset = 140;
|
|
904
|
-
// Skip geometries (
|
|
905
|
-
offset += meshGeometryCount *
|
|
913
|
+
// Skip geometries (40 bytes each - Draco compressed)
|
|
914
|
+
offset += meshGeometryCount * 40;
|
|
906
915
|
// Parse materials
|
|
907
916
|
const materials = [];
|
|
908
917
|
for (let i = 0; i < materialCount; i++) {
|
|
@@ -1023,6 +1032,7 @@ class StowKitLoader extends THREE__namespace.Loader {
|
|
|
1023
1032
|
*/
|
|
1024
1033
|
dispose() {
|
|
1025
1034
|
this.ktx2Loader.dispose();
|
|
1035
|
+
this.dracoLoader.dispose();
|
|
1026
1036
|
if (this.ownedReader) {
|
|
1027
1037
|
this.reader.close();
|
|
1028
1038
|
}
|