UNPKG

@mcmhomes/panorama-viewer

Version:
390 lines (345 loc) 9.29 kB
import {KTX2Loader} from 'three-stdlib'; import {CompressedCubeTexture, CubeTexture, LinearFilter, LinearSRGBColorSpace, MeshBasicMaterial, TextureLoader, UnsignedByteType} from 'three'; import {each, FLOAT_LAX, ISSET, promiseTimeout, uniqueId} from './PanoramaUtils.jsx'; export const dispose = (...objects) => { objects?.forEach(obj => { if(Array.isArray(obj)) { dispose(...obj); return; } try { obj?.dispose(); } catch(e) { console.error('[PanoramaViewer] THREE disposal failed:', typeof obj, obj?.constructor?.name, e); } }); }; export const loadTextures = (() => { let ktx2Loader = null; let textureLoader = null; return async (params) => { const {gl, textures, basisTranscoderPath} = params; if(typeof textures[0] !== 'string') { return textures; } const loader = (() => { if(textures[0].toLowerCase().endsWith('.ktx2')) { if(ktx2Loader === null) { ktx2Loader = new KTX2Loader(); ktx2Loader.setTranscoderPath(basisTranscoderPath); ktx2Loader.detectSupport(gl); } return ktx2Loader; } if(textureLoader === null) { textureLoader = new TextureLoader(); } return textureLoader; })(); const loadedTextures = await Promise.all(textures.map(url => loader.loadAsync(url))); loadedTextures.forEach(tex => { tex.colorSpace = LinearSRGBColorSpace; tex.anisotropy = 16; tex.type = UnsignedByteType; tex.minFilter = LinearFilter; tex.magFilter = LinearFilter; tex.generateMipmaps = false; tex.flipY = true; tex.needsUpdate = true; }); return loadedTextures; }; })(); export const getTextureIds = (...textures) => { let result = []; const loop = (textures) => { textures.forEach(texture => { if(Array.isArray(texture)) { loop(texture); return; } if(texture?.uuid) { result.push(texture.uuid); } }); }; loop(textures); return result; }; export const createCubeTexture = (textures) => { const cubeTexture = (() => { if(textures[0].isCompressedTexture) { return new CompressedCubeTexture(textures.map(tex => ({...tex.image, ...tex})), textures[0].format, textures[0].type); } return new CubeTexture(textures.map(tex => tex.image)); })(); cubeTexture.format = textures[0].format; cubeTexture.type = textures[0].type; cubeTexture.wrapS = textures[0].wrapS; cubeTexture.wrapT = textures[0].wrapT; cubeTexture.colorSpace = textures[0].colorSpace; cubeTexture.anisotropy = textures[0].anisotropy; cubeTexture.minFilter = textures[0].minFilter; cubeTexture.magFilter = textures[0].magFilter; cubeTexture.generateMipmaps = false; cubeTexture.flipY = false; cubeTexture.needsUpdate = true; return cubeTexture; }; export const createCubeMaterial = (params) => { const {maskEnvMap, ...props} = params; const material = new MeshBasicMaterial(props); material.onBeforeCompile = (shader) => { shader.uniforms.maskEnvMap = {value:maskEnvMap}; const fragmentShaderLower = shader.fragmentShader.toLowerCase(); const mainStartIndex = fragmentShaderLower.indexOf('void main()'.toLowerCase()); if(mainStartIndex < 0) { console.error('[PanoramaViewer] createCubeMaterial error: shader fragment "void main()" not found', shader.fragmentShader); return; } const envmapFragmentStartIndex = fragmentShaderLower.indexOf('#include <envmap_fragment>'.toLowerCase()); if(envmapFragmentStartIndex < 0) { console.error('[PanoramaViewer] createCubeMaterial error: shader fragment "#include <envmap_fragment>" not found', shader.fragmentShader); return; } const envmapFragmentEndIndex = fragmentShaderLower.indexOf('>'.toLowerCase(), envmapFragmentStartIndex) + 1; const startToMain = shader.fragmentShader.substring(0, mainStartIndex); const mainToEndEnvmapFragment = shader.fragmentShader.substring(mainStartIndex, envmapFragmentEndIndex); const endEnvmapFragmentToEnd = shader.fragmentShader.substring(envmapFragmentEndIndex); shader.fragmentShader = startToMain + ` ` + (!maskEnvMap ? `` : `uniform samplerCube maskEnvMap;`) + ` ` + mainToEndEnvmapFragment + ` vec4 maskColor = ` + (!maskEnvMap ? `vec4(1,1,1,1)` : `textureCube(maskEnvMap, envMapRotation * vec3(flipEnvMap * reflectVec.x, reflectVec.yz))`) + `; diffuseColor.a *= maskColor.r; ` + endEnvmapFragmentToEnd; }; return material; }; export const getTexturePathsOfBasePath = (params) => { const {basePath, type = 'ktx2'} = params; if(!basePath) { return null; } return [ [ basePath + '/0.' + type, // low-res ], [ basePath + '/1r.' + type, // right basePath + '/1l.' + type, // left basePath + '/1d.' + type, // down basePath + '/1u.' + type, // up basePath + '/1f.' + type, // front basePath + '/1b.' + type, // back ], ]; }; export const loadMultiresTexture = (params) => { const {gl, basePath, maskBasePath, type, basisTranscoderPath, minimumLoadTime:givenMinimumLoadTime, onLoadingLevelDone, onLoadingLevelFail} = params; const minimumLoadTime = FLOAT_LAX(givenMinimumLoadTime); const textures = getTexturePathsOfBasePath({basePath, type}); const maskTextures = getTexturePathsOfBasePath({basePath:maskBasePath, type}); if(!gl || !textures) { return; } let listeners = []; let allLoadTextures = []; let lastLoadedLevel = -1; let lastLoadedTextures = null; let lastLoadedMaskTextures = null; let cancel = false; let newLoadedTextures = null; let newLoadedMaskTextures = null; const removeListener = (key) => { listeners = listeners.filter(listener => (listener.key !== key)); }; const addListener = ({onDone, onFail}) => { const key = uniqueId(); listeners.push({key, onDone, onFail}); if(lastLoadedTextures) { try { onDone?.({level:lastLoadedLevel, textures:lastLoadedTextures, maskTextures:lastLoadedMaskTextures}); } catch(e) { console.error('[PanoramaViewer] loadMultiresTexture listener.onDone failed:', e); } } return {remove:() => removeListener(key)}; }; const disposeLoadTextures = () => { cancel = true; (async () => { dispose(...allLoadTextures); allLoadTextures = []; lastLoadedTextures = null; lastLoadedMaskTextures = null; dispose(await newLoadedTextures, await newLoadedMaskTextures); newLoadedTextures = null; newLoadedMaskTextures = null; dispose(...allLoadTextures); allLoadTextures = []; lastLoadedTextures = null; lastLoadedMaskTextures = null; })(); }; const loadLevel = (async (level, attempt = 1) => { try { if(cancel) { return; } if(!ISSET(textures[level])) { // no more levels return; } newLoadedTextures = loadTextures({gl, textures:textures[level], basisTranscoderPath}); newLoadedMaskTextures = !maskTextures ? null : loadTextures({gl, textures:maskTextures[level], basisTranscoderPath}); let error = null; { try { newLoadedTextures = await newLoadedTextures; } catch(e) { newLoadedTextures = null; error = e; } try { newLoadedMaskTextures = await newLoadedMaskTextures; } catch(e) { newLoadedMaskTextures = null; error = e; } } if(error) { dispose(newLoadedTextures, newLoadedMaskTextures); throw error; } if(cancel) { dispose(newLoadedTextures, newLoadedMaskTextures); return; } allLoadTextures.push(newLoadedTextures); allLoadTextures.push(newLoadedMaskTextures); lastLoadedLevel = level; lastLoadedTextures = newLoadedTextures; lastLoadedMaskTextures = newLoadedMaskTextures; try { await onLoadingLevelDone?.({level, textures:newLoadedTextures, maskTextures:newLoadedMaskTextures}); } catch(e) { console.error('[PanoramaViewer] loadMultiresTexture onLoadingLevelDone failed:', e); } each(listeners, listener => { try { listener?.onDone?.({level:lastLoadedLevel, textures:lastLoadedTextures, maskTextures:lastLoadedMaskTextures}); } catch(e) { console.error('[PanoramaViewer] loadMultiresTexture listener.onDone failed:', e); } }); await loadLevel(level + 1); } catch(e) { if(cancel) { return; } if(attempt <= 3) { await promiseTimeout(500); await loadLevel(level, attempt + 1); return; } cancel = true; console.error('[PanoramaViewer] loadMultiresTexture texture loading failed:', e); try { await onLoadingLevelFail?.({level, error:e}); } catch(e) { console.error('[PanoramaViewer] loadMultiresTexture onLoadingLevelFail failed:', e); } each(listeners, listener => { try { listener?.onFail?.({level:lastLoadedLevel, error:e}); } catch(e) { console.error('[PanoramaViewer] loadMultiresTexture listener.onFail failed:', e); } }); } }); // noinspection JSIgnoredPromiseFromCall loadLevel(0); let minimumTimeWaited = (minimumLoadTime <= 0); if(!minimumTimeWaited) { setTimeout(() => { minimumTimeWaited = true; }, minimumLoadTime); } return { loaderId: uniqueId(), dispose: disposeLoadTextures, isReady: (minLevel = 0) => cancel || ((lastLoadedLevel >= minLevel) && minimumTimeWaited), addListener:addListener, }; };