Skip to content

Commit 3247463

Browse files
dakersankhesh
authored andcommitted
perf(GLTFImporter): optimize GLTF loading performance
1 parent 8c25c54 commit 3247463

File tree

10 files changed

+161
-195
lines changed

10 files changed

+161
-195
lines changed

Sources/IO/Geometry/GLTFImporter/Decoder.js

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,70 +8,88 @@ import {
88
} from 'vtk.js/Sources/IO/Geometry/GLTFImporter/Constants';
99

1010
function getChunkInfo(headerStart, data) {
11+
// Cache array view
1112
const header = new Uint32Array(data, headerStart, BINARY_CHUNK_HEADER_INTS);
1213
const chunkStart = headerStart + BINARY_CHUNK_HEADER_INTS * 4;
13-
const chunkLength = header[0];
14-
const chunkType = header[1];
15-
return { start: chunkStart, length: chunkLength, type: chunkType };
14+
return {
15+
start: chunkStart,
16+
length: header[0],
17+
type: header[1],
18+
};
1619
}
1720

1821
function getAllChunkInfos(data) {
19-
const infos = [];
22+
// Pre-calculate array size
23+
const maxChunks = Math.floor((data.byteLength - BINARY_HEADER_LENGTH) / 8);
24+
const infos = new Array(maxChunks);
2025
let chunkStart = BINARY_HEADER_INTS * 4;
26+
let chunkCount = 0;
27+
2128
while (chunkStart < data.byteLength) {
2229
const chunkInfo = getChunkInfo(chunkStart, data);
23-
infos.push(chunkInfo);
30+
infos[chunkCount++] = chunkInfo;
2431
chunkStart += chunkInfo.length + BINARY_CHUNK_HEADER_INTS * 4;
2532
}
26-
return infos;
33+
34+
// Trim array to actual size
35+
return infos.slice(0, chunkCount);
2736
}
2837

2938
function getJsonFromChunk(chunkInfo, data) {
30-
const chunkLength = chunkInfo.length;
3139
const jsonStart = (BINARY_HEADER_INTS + BINARY_CHUNK_HEADER_INTS) * 4;
32-
const jsonSlice = new Uint8Array(data, jsonStart, chunkLength);
33-
const stringBuffer = BinaryHelper.arrayBufferToString(jsonSlice);
34-
return JSON.parse(stringBuffer);
40+
const jsonView = new Uint8Array(data, jsonStart, chunkInfo.length);
41+
const decoder = new TextDecoder('utf-8');
42+
const jsonString = decoder.decode(jsonView);
43+
return JSON.parse(jsonString);
3544
}
3645

3746
function getBufferFromChunk(chunkInfo, data) {
3847
return data.slice(chunkInfo.start, chunkInfo.start + chunkInfo.length);
3948
}
4049

4150
function parseGLB(data) {
42-
let json;
43-
const buffers = [];
51+
if (data.byteLength < BINARY_HEADER_LENGTH) {
52+
throw new Error('Invalid GLB: File too small');
53+
}
4454

55+
// Cache DataView
4556
const headerView = new DataView(data, 0, BINARY_HEADER_LENGTH);
57+
const magic = new Uint8Array(data, 0, 4);
4658

4759
const header = {
48-
magic: BinaryHelper.arrayBufferToString(new Uint8Array(data, 0, 4)),
60+
magic: BinaryHelper.arrayBufferToString(magic),
4961
version: headerView.getUint32(4, true),
5062
length: headerView.getUint32(8, true),
5163
};
5264

5365
if (header.magic !== BINARY_HEADER_MAGIC) {
5466
throw new Error('Unsupported glTF-Binary header.');
55-
} else if (header.version < 2.0) {
67+
}
68+
if (header.version < 2.0) {
5669
throw new Error('Unsupported legacy binary file detected.');
5770
}
71+
if (header.length > data.byteLength) {
72+
throw new Error('Invalid GLB: Declared length exceeds file size');
73+
}
5874

5975
const chunkInfos = getAllChunkInfos(data);
76+
let json = null;
77+
const buffers = [];
6078

61-
chunkInfos.forEach((chunkInfo) => {
79+
// Process chunks sequentially
80+
for (let i = 0; i < chunkInfos.length; i++) {
81+
const chunkInfo = chunkInfos[i];
6282
if (chunkInfo.type === BINARY_CHUNK_TYPES.JSON && !json) {
6383
json = getJsonFromChunk(chunkInfo, data);
6484
} else if (chunkInfo.type === BINARY_CHUNK_TYPES.BIN) {
6585
buffers.push(getBufferFromChunk(chunkInfo, data));
6686
}
67-
});
87+
}
6888

6989
if (!json) {
7090
throw new Error('glTF-Binary: JSON content not found.');
7191
}
72-
if (!buffers) {
73-
throw new Error('glTF-Binary: Binary chunk not found.');
74-
}
92+
7593
return { json, buffers };
7694
}
7795

Sources/IO/Geometry/GLTFImporter/ORMTexture.worker.js

Lines changed: 0 additions & 35 deletions
This file was deleted.

Sources/IO/Geometry/GLTFImporter/Parser.js

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -299,30 +299,42 @@ class GLTFParser {
299299
// WebGLBuffer's using the bufferViews.
300300
if (accessor.bufferView) {
301301
const buffer = accessor.bufferView.buffer;
302-
const { ArrayType, byteLength } = getAccessorArrayTypeAndLength(
302+
const { ArrayType } = getAccessorArrayTypeAndLength(
303303
accessor,
304304
accessor.bufferView
305305
);
306-
const byteOffset =
307-
(accessor.bufferView.byteOffset || 0) +
308-
(accessor.byteOffset || 0) +
309-
buffer.byteOffset;
310-
311-
let slicedBufffer = buffer.arrayBuffer.slice(
312-
byteOffset,
313-
byteOffset + byteLength
314-
);
306+
const baseByteOffset =
307+
(accessor.bufferView.byteOffset || 0) + buffer.byteOffset;
308+
const byteOffset = baseByteOffset + (accessor.byteOffset || 0);
315309

310+
let arrayBufferView;
316311
if (accessor.bufferView.byteStride) {
317-
slicedBufffer = this.getValueFromInterleavedBuffer(
318-
buffer,
312+
// Only extract if stride is not equal to element size
313+
if (accessor.bufferView.byteStride === accessor.bytesPerElement) {
314+
arrayBufferView = new ArrayType(
315+
buffer.arrayBuffer,
316+
byteOffset,
317+
accessor.count * accessor.components
318+
);
319+
} else {
320+
// Interleaved buffer, extract only needed bytes
321+
const interleavedBuffer = this.getValueFromInterleavedBuffer(
322+
buffer,
323+
byteOffset,
324+
accessor.bufferView.byteStride,
325+
accessor.bytesPerElement,
326+
accessor.count
327+
);
328+
arrayBufferView = new ArrayType(interleavedBuffer);
329+
}
330+
} else {
331+
arrayBufferView = new ArrayType(
332+
buffer.arrayBuffer,
319333
byteOffset,
320-
accessor.bufferView.byteStride,
321-
accessor.bytesPerElement,
322-
accessor.count
334+
accessor.count * accessor.components
323335
);
324336
}
325-
accessor.value = new ArrayType(slicedBufffer);
337+
accessor.value = arrayBufferView;
326338
}
327339

328340
return accessor;

Sources/IO/Geometry/GLTFImporter/Reader.js

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ async function createPolyDataFromGLTFMesh(primitive) {
167167
polyData.setStrips(cells);
168168
break;
169169
default:
170+
cells.delete();
170171
vtkWarningMacro('Invalid primitive draw mode. Ignoring connectivity.');
171172
}
172173

@@ -242,14 +243,7 @@ async function createPropertyFromGLTFMaterial(model, material, actor) {
242243
extensions
243244
);
244245

245-
// FIXME: Workaround for textures not showing up in WebGL
246-
const viewAPI = model.renderer.getRenderWindow();
247-
const isWebGL = viewAPI.getViews()[0].isA('vtkOpenGLRenderWindow');
248-
if (isWebGL) {
249-
actor.addTexture(diffuseTex);
250-
} else {
251-
property.setDiffuseTexture(diffuseTex);
252-
}
246+
property.setDiffuseTexture(diffuseTex);
253247
}
254248

255249
// Handle metallic-roughness texture (metallicRoughnessTexture)
@@ -271,7 +265,7 @@ async function createPropertyFromGLTFMaterial(model, material, actor) {
271265
const extensions = material.occlusionTexture.extensions;
272266
const tex = material.occlusionTexture.texture;
273267
const sampler = tex.sampler;
274-
const aoImage = await loadImage(tex.source, 'r');
268+
const aoImage = await loadImage(tex.source);
275269
const aoTex = createVTKTextureFromGLTFTexture(
276270
aoImage,
277271
sampler,
@@ -361,6 +355,8 @@ function handlePrimitiveExtensions(nodeId, extensions, model) {
361355
case 'KHR_materials_variants':
362356
model.variantMappings.set(nodeId, extension.mappings);
363357
break;
358+
case 'KHR_draco_mesh_compression':
359+
break;
364360
default:
365361
vtkWarningMacro(`Unhandled extension: ${extensionName}`);
366362
}
@@ -376,6 +372,7 @@ async function createActorFromGTLFNode(worldMatrix) {
376372
const actor = vtkActor.newInstance();
377373
const mapper = vtkMapper.newInstance();
378374
mapper.setColorModeToDirectScalars();
375+
mapper.setInterpolateScalarsBeforeMapping(true);
379376
actor.setMapper(mapper);
380377
actor.setUserMatrix(worldMatrix);
381378

@@ -393,6 +390,8 @@ async function createActorFromGTLFPrimitive(model, primitive, worldMatrix) {
393390
const actor = vtkActor.newInstance();
394391
const mapper = vtkMapper.newInstance();
395392
mapper.setColorModeToDirectScalars();
393+
mapper.setInterpolateScalarsBeforeMapping(true);
394+
396395
actor.setMapper(mapper);
397396
actor.setUserMatrix(worldMatrix);
398397

Sources/IO/Geometry/GLTFImporter/Utils.js

Lines changed: 8 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import WebworkerPromise from 'webworker-promise';
21
import macro from 'vtk.js/Sources/macros';
32
import vtkTexture from 'vtk.js/Sources/Rendering/Core/Texture';
4-
import Worker from 'vtk.js/Sources/IO/Geometry/GLTFImporter/ORMTexture.worker';
53
import {
64
BYTES,
75
COMPONENTS,
@@ -89,37 +87,16 @@ export function resolveUrl(url, originalPath) {
8987
/**
9088
* Loads image from buffer or URI
9189
* @param {*} image
92-
* @param {*} channel
9390
* @returns
9491
*/
95-
export async function loadImage(image, channel, forceReLoad = false) {
96-
// Initialize cache if it doesn't exist
97-
if (!image.cache) {
98-
image.cache = {};
99-
}
100-
101-
// Return cached result for the channel if available and not forced to reload
102-
if (!forceReLoad && image.cache[channel]) {
103-
return image.cache[channel];
104-
}
105-
106-
const worker = new WebworkerPromise(new Worker());
107-
92+
export async function loadImage(image) {
10893
if (image.bufferView) {
109-
return worker
110-
.postMessage({
111-
imageBuffer: image.bufferView.data,
112-
mimeType: image.mimeType,
113-
channel,
114-
})
115-
.then((result) => {
116-
// Cache the bitmap based on the channel
117-
image.cache[channel] = result.bitmap;
118-
return result.bitmap;
119-
})
120-
.finally(() => {
121-
worker.terminate();
122-
});
94+
const blob = new Blob([image.bufferView.data], { type: image.mimeType });
95+
const bitmap = await createImageBitmap(blob, {
96+
colorSpaceConversion: 'none',
97+
imageOrientation: 'flipY',
98+
});
99+
return bitmap;
123100
}
124101

125102
if (image.uri) {
@@ -128,7 +105,6 @@ export async function loadImage(image, channel, forceReLoad = false) {
128105
const img = new Image();
129106
img.crossOrigin = 'Anonymous';
130107
img.onload = () => {
131-
image.cache[channel] = img; // Cache the loaded image based on the channel
132108
resolve(img);
133109
};
134110
img.onerror = reject;
@@ -189,7 +165,6 @@ export function createVTKTextureFromGLTFTexture(image, sampler, extensions) {
189165
texture.setEdgeClamp(true);
190166
}
191167
}
192-
193-
texture.setJsImageData(image);
168+
texture.setImageBitmap(image);
194169
return texture;
195170
}

0 commit comments

Comments
 (0)