UNPKG

@needle-tools/gltf-progressive

Version:

three.js support for loading glTF or GLB files that contain progressive loading data

224 lines (223 loc) 8.95 kB
import { LODsManager } from "../lods_manager.js"; import { EXTENSION_NAME, NEEDLE_progressive } from "../extension.js"; const $meshLODSymbol = Symbol("NEEDLE_mesh_lod"); const $textureLODSymbol = Symbol("NEEDLE_texture_lod"); let documentObserver = null; export function patchModelViewer() { const ModelViewerElement = tryGetModelViewerConstructor(); if (!ModelViewerElement) { return; } ModelViewerElement.mapURLs(function (url) { searchModelViewers(); return url; }); searchModelViewers(); // observe the document for new model-viewers documentObserver?.disconnect(); documentObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node instanceof HTMLElement && node.tagName.toLowerCase() === "model-viewer") { _patchModelViewer(node); } }); }); }); documentObserver.observe(document, { childList: true, subtree: true }); } /** * Tries to get the mode-viewer constructor from the custom element registry. If it doesnt exist yet we will wait for it to be loaded in case it's added to the document at a later point */ function tryGetModelViewerConstructor() { if (typeof customElements === 'undefined') return null; // If model-viewer is already registered we can ignore this const ModelViewerElement = customElements.get('model-viewer'); if (ModelViewerElement) return ModelViewerElement; // wait for model-viewer to be defined customElements.whenDefined('model-viewer').then(() => { console.debug("[gltf-progressive] model-viewer defined"); patchModelViewer(); }); return null; } function searchModelViewers() { if (typeof document === 'undefined') return; // Query once for model viewer. If a user does not have model-viewer in their page, this will return null. const modelviewers = document.querySelectorAll("model-viewer"); modelviewers.forEach((modelviewer) => { _patchModelViewer(modelviewer); }); } const foundModelViewers = new WeakSet(); let modelViewerCount = 0; /** Patch modelviewer to support NEEDLE progressive system * @returns a function to remove the patch */ function _patchModelViewer(modelviewer) { if (!modelviewer) return null; if (foundModelViewers.has(modelviewer)) return null; foundModelViewers.add(modelviewer); console.debug("[gltf-progressive] found new model-viewer..." + (++modelViewerCount) + "\n", modelviewer.getAttribute("src")); // Find the necessary internal methods and properties. We need access to the scene, renderer let renderer = null; let scene = null; let needsRender = null; // < used to force render updates for a few frames for (let p = modelviewer; p != null; p = Object.getPrototypeOf(p)) { const privateAPI = Object.getOwnPropertySymbols(p); const rendererSymbol = privateAPI.find((value) => value.toString() == 'Symbol(renderer)'); const sceneSymbol = privateAPI.find((value) => value.toString() == 'Symbol(scene)'); const needsRenderSymbol = privateAPI.find((value) => value.toString() == 'Symbol(needsRender)'); if (!renderer && rendererSymbol != null) { renderer = modelviewer[rendererSymbol].threeRenderer; } if (!scene && sceneSymbol != null) { scene = modelviewer[sceneSymbol]; } if (!needsRender && needsRenderSymbol != null) { needsRender = modelviewer[needsRenderSymbol]; } } if (renderer && scene) { console.debug("[gltf-progressive] setup model-viewer"); const lod = LODsManager.get(renderer, { engine: "model-viewer" }); LODsManager.addPlugin(new RegisterModelviewerDataPlugin()); lod.enable(); // Trigger a render when a LOD has changed lod.addEventListener("changed", () => { needsRender?.call(modelviewer); }); // Trigger a render when the model viewer visibility changes modelviewer.addEventListener("model-visibility", (evt) => { const visible = evt.detail.visible; if (visible) needsRender?.call(modelviewer); }); modelviewer.addEventListener("load", () => { renderFrames(); }); /** * For model viewer to immediately update without interaction we need to trigger a few renders * We do this so that the LODs are loaded */ function renderFrames() { if (needsRender) { let forcedFrames = 0; let interval = setInterval(() => { if (forcedFrames++ > 5) { clearInterval(interval); return; } needsRender?.call(modelviewer); }, 300); } } return () => { lod.disable(); }; } return null; } /** * LODs manager plugin that registers LOD data to the NEEDLE progressive system */ class RegisterModelviewerDataPlugin { _didWarnAboutMissingUrl = false; onBeforeUpdateLOD(_renderer, scene, _camera, object) { this.tryParseMeshLOD(scene, object); this.tryParseTextureLOD(scene, object); } getUrl(element) { if (!element) { return null; } let url = element.getAttribute("src"); // fallback in case the attribute is not set but the src property is if (!url) { url = element["src"]; } if (!url) { if (!this._didWarnAboutMissingUrl) console.warn("No url found in modelviewer", element); this._didWarnAboutMissingUrl = true; } return url; } tryGetCurrentGLTF(scene) { return scene._currentGLTF; } tryGetCurrentModelViewer(scene) { return scene.element; } tryParseTextureLOD(scene, object) { if (object[$textureLODSymbol] == true) return; object[$textureLODSymbol] = true; const currentGLTF = this.tryGetCurrentGLTF(scene); const element = this.tryGetCurrentModelViewer(scene); const url = this.getUrl(element); if (!url) { return; } if (currentGLTF) { if (object.material) { const mat = object.material; if (Array.isArray(mat)) for (const m of mat) handleMaterial(m); else handleMaterial(mat); function handleMaterial(mat) { if (mat[$textureLODSymbol] == true) return; mat[$textureLODSymbol] = true; // make sure to force the material to be updated if (mat.userData) mat.userData.LOD = -1; const keys = Object.keys(mat); for (let i = 0; i < keys.length; i++) { const key = keys[i]; const value = mat[key]; if (value?.isTexture === true) { const textureIndex = value.userData?.associations?.textures; if (textureIndex == null) continue; const textureData = currentGLTF.parser.json.textures[textureIndex]; if (!textureData) { console.warn("Texture data not found for texture index " + textureIndex); continue; } if (textureData?.extensions?.[EXTENSION_NAME]) { const ext = textureData.extensions[EXTENSION_NAME]; if (ext && url) { NEEDLE_progressive.registerTexture(url, value, ext.lods.length, textureIndex, ext); } } } } } } } } tryParseMeshLOD(scene, object) { if (object[$meshLODSymbol] == true) return; object[$meshLODSymbol] = true; const element = this.tryGetCurrentModelViewer(scene); const url = this.getUrl(element); if (!url) { return; } // modelviewer has all the information we need in the userData (associations + gltfExtensions) const ext = object.userData?.["gltfExtensions"]?.[EXTENSION_NAME]; if (ext && url) { const lodKey = object.uuid; NEEDLE_progressive.registerMesh(url, lodKey, object, 0, ext.lods.length, ext); } } }