diff --git a/Sources/IO/Geometry/GLTFImporter/Reader.js b/Sources/IO/Geometry/GLTFImporter/Reader.js index f8e711b1f1d..541519c42ae 100644 --- a/Sources/IO/Geometry/GLTFImporter/Reader.js +++ b/Sources/IO/Geometry/GLTFImporter/Reader.js @@ -186,6 +186,22 @@ async function createPropertyFromGLTFMaterial(model, material, actor) { const emissiveFactor = material.emissiveFactor; const property = actor.getProperty(); + const texturePromises = { + diffuse: null, + rm: null, + ao: null, + emissive: null, + normal: null, + }; + + const loadTextureRef = async (texRef) => { + if (!texRef?.texture) return null; + const tex = texRef.texture; + const sampler = tex.sampler; + const img = await loadImage(tex.source); + return createVTKTextureFromGLTFTexture(img, sampler, texRef.extensions); + }; + const pbr = material.pbrMetallicRoughness; if (pbr != null) { @@ -220,7 +236,7 @@ async function createPropertyFromGLTFMaterial(model, material, actor) { property.setEmission(emissiveFactor); if (pbr.baseColorTexture) { - const extensions = pbr.baseColorTexture.extensions; + // const extensions = pbr.baseColorTexture.extensions; const tex = pbr.baseColorTexture.texture; if (tex.extensions != null) { @@ -235,57 +251,23 @@ async function createPropertyFromGLTFMaterial(model, material, actor) { }); } - const sampler = tex.sampler; - const image = await loadImage(tex.source); - const diffuseTex = createVTKTextureFromGLTFTexture( - image, - sampler, - extensions - ); - - property.setDiffuseTexture(diffuseTex); + texturePromises.diffuse = loadTextureRef(pbr.baseColorTexture); } // Handle metallic-roughness texture (metallicRoughnessTexture) if (pbr.metallicRoughnessTexture) { - const extensions = pbr.metallicRoughnessTexture.extensions; - const tex = pbr.metallicRoughnessTexture.texture; - const sampler = tex.sampler; - const rmImage = await loadImage(tex.source); - const rmTex = createVTKTextureFromGLTFTexture( - rmImage, - sampler, - extensions - ); - property.setRMTexture(rmTex); + texturePromises.rm = loadTextureRef(pbr.metallicRoughnessTexture); } // Handle ambient occlusion texture (occlusionTexture) if (material.occlusionTexture) { - const extensions = material.occlusionTexture.extensions; - const tex = material.occlusionTexture.texture; - const sampler = tex.sampler; - const aoImage = await loadImage(tex.source); - const aoTex = createVTKTextureFromGLTFTexture( - aoImage, - sampler, - extensions - ); - property.setAmbientOcclusionTexture(aoTex); + texturePromises.ao = loadTextureRef(material.occlusionTexture); + // TODO: Handle occlusionTexture.strength } // Handle emissive texture (emissiveTexture) if (material.emissiveTexture) { - const extensions = material.emissiveTexture.extensions; - const tex = material.emissiveTexture.texture; - const sampler = tex.sampler; - const emissiveImage = await loadImage(tex.source); - const emissiveTex = createVTKTextureFromGLTFTexture( - emissiveImage, - sampler, - extensions - ); - property.setEmissionTexture(emissiveTex); + texturePromises.emissive = loadTextureRef(material.emissiveTexture); // Handle mutiple Uvs if (material.emissiveTexture.texCoord != null) { @@ -296,21 +278,37 @@ async function createPropertyFromGLTFMaterial(model, material, actor) { // Handle normal texture (normalTexture) if (material.normalTexture) { - const extensions = material.normalTexture.extensions; - const tex = material.normalTexture.texture; - const sampler = tex.sampler; - const normalImage = await loadImage(tex.source); - const normalTex = createVTKTextureFromGLTFTexture( - normalImage, - sampler, - extensions - ); - property.setNormalTexture(normalTex); + texturePromises.normal = loadTextureRef(material.normalTexture); if (material.normalTexture.scale != null) { property.setNormalStrength(material.normalTexture.scale); } } + + const [diffuseTex, rmTex, aoTex, emissiveTex, normalTex] = + await Promise.all([ + texturePromises.diffuse, + texturePromises.rm, + texturePromises.ao, + texturePromises.emissive, + texturePromises.normal, + ]); + + if (diffuseTex) { + property.setDiffuseTexture(diffuseTex); + } + if (rmTex) { + property.setRMTexture(rmTex); + } + if (aoTex) { + property.setAmbientOcclusionTexture(aoTex); + } + if (emissiveTex) { + property.setEmissionTexture(emissiveTex); + } + if (normalTex) { + property.setNormalTexture(normalTex); + } } // Material extensions diff --git a/Sources/IO/Geometry/GLTFImporter/Utils.js b/Sources/IO/Geometry/GLTFImporter/Utils.js index f742cf019ca..19f102c89b3 100644 --- a/Sources/IO/Geometry/GLTFImporter/Utils.js +++ b/Sources/IO/Geometry/GLTFImporter/Utils.js @@ -8,6 +8,8 @@ import { } from 'vtk.js/Sources/IO/Geometry/GLTFImporter/Constants'; const { vtkWarningMacro, vtkErrorMacro } = macro; +const imageBufferViewCache = new WeakMap(); +const imageUriCache = new Map(); /** * Get GL enum from sampler parameter @@ -90,29 +92,85 @@ export function resolveUrl(url, originalPath) { * @returns */ export async function loadImage(image) { - if (image.bufferView) { - const blob = new Blob([image.bufferView.data], { type: image.mimeType }); - const bitmap = await createImageBitmap(blob, { - colorSpaceConversion: 'none', - imageOrientation: 'flipY', - }); - return bitmap; + if (!image) return null; + + const cacheKey = image.bufferView || image.uri; + const cache = image.bufferView ? imageBufferViewCache : imageUriCache; + + if (cacheKey) { + const cached = cache.get(cacheKey); + + if (cached) { + // In flight promise + if (typeof cached?.then === 'function') { + return cached; + } + + // WeakRef + const value = cached?.deref?.(); + if (value) return value; + + // Stale WeakRef + cache.delete(cacheKey); + } } - if (image.uri) { - vtkWarningMacro('Falling back to image uri', image.uri); - return new Promise((resolve, reject) => { + const loadPromise = (async () => { + if (image.bufferView) { + const blob = new Blob([image.bufferView.data], { + type: image.mimeType, + }); + return createImageBitmap(blob, { + premultiplyAlpha: 'none', + colorSpaceConversion: 'none', + imageOrientation: 'flipY', + }); + } + + if (image.uri) { const img = new Image(); img.crossOrigin = 'Anonymous'; - img.onload = () => { - resolve(img); - }; - img.onerror = reject; - img.src = image.uri; - }); + + await new Promise((resolve, reject) => { + img.onload = resolve; + img.onerror = reject; + img.src = image.uri; + }); + + return createImageBitmap(img, { + premultiplyAlpha: 'none', + colorSpaceConversion: 'none', + imageOrientation: 'flipY', + }); + } + + return null; + })(); + + if (cacheKey) { + cache.set(cacheKey, loadPromise); + } + + try { + const result = await loadPromise; + + if (cacheKey && result) { + // eslint-disable-next-line no-undef + cache.set(cacheKey, new WeakRef(result)); + } + + return result; + } catch (err) { + if (cacheKey) { + cache.delete(cacheKey); + } + throw err; } +} - return null; +export function clearImageCaches() { + imageUriCache.clear(); + imageBufferViewCache.clear(); } /** diff --git a/Sources/IO/Geometry/GLTFImporter/index.d.ts b/Sources/IO/Geometry/GLTFImporter/index.d.ts index 36e1cb8dd83..8cf2ef31b8a 100644 --- a/Sources/IO/Geometry/GLTFImporter/index.d.ts +++ b/Sources/IO/Geometry/GLTFImporter/index.d.ts @@ -204,6 +204,11 @@ export interface vtkGLTFImporter extends vtkGLTFImporterBase { * @param variantIndex The index of the variant to switch to. */ switchToVariant(variantIndex: number): void; + + /** + * Clear the importer to initial state, clearing all internal data structures. + */ + clear(): void; } /** diff --git a/Sources/IO/Geometry/GLTFImporter/index.js b/Sources/IO/Geometry/GLTFImporter/index.js index e5175f976fa..cc9cd0a9adc 100644 --- a/Sources/IO/Geometry/GLTFImporter/index.js +++ b/Sources/IO/Geometry/GLTFImporter/index.js @@ -13,6 +13,7 @@ import { import parseGLB from 'vtk.js/Sources/IO/Geometry/GLTFImporter/Decoder'; import { createAnimationMixer } from 'vtk.js/Sources/IO/Geometry/GLTFImporter/Animations'; import { BINARY_HEADER_MAGIC } from 'vtk.js/Sources/IO/Geometry/GLTFImporter/Constants'; +import { clearImageCaches } from 'vtk.js/Sources/IO/Geometry/GLTFImporter/Utils'; const { vtkDebugMacro, vtkErrorMacro } = macro; @@ -221,6 +222,30 @@ function vtkGLTFImporter(publicAPI, model) { await Promise.all(promises); }; + + publicAPI.clear = () => { + model.actors?.clear?.(); + model.cameras?.clear?.(); + model.lights?.clear?.(); + model.nodeLights?.clear?.(); + model.variantMappings?.clear?.(); + model.nodeTransforms?.clear?.(); + model.nodeChildren?.clear?.(); + model.skins?.clear?.(); + model.morphTargets?.clear?.(); + model.materialProperties?.clear?.(); + model.pointerAnimations = []; + model.nodeAnimations = []; + model.animationClips = []; + model.skeletons = []; + model.animations = []; + model.scenes = []; + model.glTFTree = null; + model.parseData = null; + clearImageCaches(); + }; + + publicAPI.delete = macro.chain(() => publicAPI.clear(), publicAPI.delete); } // ----------------------------------------------------------------------------