From 9074a4a1cf2f4855659d97eb9daf60d22d8bc1f4 Mon Sep 17 00:00:00 2001 From: James Smith Date: Fri, 20 Dec 2024 13:01:11 +0000 Subject: [PATCH] Add basic framework for loading and testing MANYFOLD_mesh_progressive --- README.md | 1 + .../assets/gltf/Progressive/progressive.glb | Bin 0 -> 55480 bytes .../MANYFOLD_mesh_progressive.js | 19 + loaders/MANYFOLD_mesh_progressive/README.md | 5 + test/MANYFOLD_mesh_progressive.js | 43 ++ test/build/unit.js | 456 +++++++++--------- test/index.js | 3 +- 7 files changed, 308 insertions(+), 219 deletions(-) create mode 100644 examples/assets/gltf/Progressive/progressive.glb create mode 100644 loaders/MANYFOLD_mesh_progressive/MANYFOLD_mesh_progressive.js create mode 100644 loaders/MANYFOLD_mesh_progressive/README.md create mode 100644 test/MANYFOLD_mesh_progressive.js diff --git a/README.md b/README.md index a80f84b..01addce 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ This project provides you Three.js glTF loader/extension plugins even for such e * [EXT_texture_video](https://github.com/takahirox/EXT_texture_video) (Loader only) * [MSFT_lod](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/MSFT_lod) (Loader only, in progress) * [MSFT_texture_dds](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/MSFT_texture_dds) (Loader only) +* [MANYFOLD_mesh_progressive](https://github.com/manyfold3d/glTF/tree/MANYFOLD_mesh_progressive/extensions/2.0/Vendor/MANYFOLD_mesh_progressive#readme) (Loader only) ## Compatible Three.js revision diff --git a/examples/assets/gltf/Progressive/progressive.glb b/examples/assets/gltf/Progressive/progressive.glb new file mode 100644 index 0000000000000000000000000000000000000000..1ddd348570d3bc35ed43eb4a512e064dfa34f748 GIT binary patch literal 55480 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 hZUs5*24%<3S$At@$Cy!}h!1I6!o^ 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 literal 0 HcmV?d00001 diff --git a/loaders/MANYFOLD_mesh_progressive/MANYFOLD_mesh_progressive.js b/loaders/MANYFOLD_mesh_progressive/MANYFOLD_mesh_progressive.js new file mode 100644 index 0000000..b73f2d3 --- /dev/null +++ b/loaders/MANYFOLD_mesh_progressive/MANYFOLD_mesh_progressive.js @@ -0,0 +1,19 @@ +import { + FileLoader, + LoaderUtils +} from 'three'; + +export default class GLTFManyfoldMeshProgressiveExtension { + constructor(parser, url, binChunkOffset) { + this.name = 'MANYFOLD_mesh_progressive'; + this.parser = parser; + this.url = url; + this.binChunkOffset = binChunkOffset; + console.log(`${this.name} loader created`) + } + + static load(url, loader, onLoad, onProgress, onError, fileLoader = null) { + console.log(`${this.name} falling back to original loader`) + loader.load(url, onLoad, onProgress, onError); + } +} diff --git a/loaders/MANYFOLD_mesh_progressive/README.md b/loaders/MANYFOLD_mesh_progressive/README.md new file mode 100644 index 0000000..1e45e6c --- /dev/null +++ b/loaders/MANYFOLD_mesh_progressive/README.md @@ -0,0 +1,5 @@ +# [Three.js](https://threejs.org) [GLTFLoader](https://threejs.org/docs/#examples/en/loaders/GLTFLoader) [EXT_texture_video](https://github.com/manyfold3d/glTF/tree/MANYFOLD_mesh_progressive/extensions/2.0/Vendor/MANYFOLD_mesh_progressive#readme) extension + +This extension implements a loader for [MANYFOLD_mesh_progressive](https://github.com/manyfold3d/glTF/tree/MANYFOLD_mesh_progressive/extensions/2.0/Vendor/MANYFOLD_mesh_progressive#readme) streams. + +Such streams load an initial base mesh in the usual way, then provide a stream of refinements which can be displayed incrementally. diff --git a/test/MANYFOLD_mesh_progressive.js b/test/MANYFOLD_mesh_progressive.js new file mode 100644 index 0000000..044d53b --- /dev/null +++ b/test/MANYFOLD_mesh_progressive.js @@ -0,0 +1,43 @@ +/* global QUnit */ + +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; +import GLTFManyfoldMeshProgressiveExtension from '../loaders/MANYFOLD_mesh_progressive/MANYFOLD_mesh_progressive.js'; + +export default QUnit.module('MANYFOLD_mesh_progressive', () => { + QUnit.module('GLTFManyfoldMeshProgressiveExtension', () => { + QUnit.test('register', assert => { + const done = assert.async(); + new GLTFLoader() + .register(parser => new GLTFManyfoldMeshProgressiveExtension(parser)) + .parse('{"asset": {"version": "2.0"}}', null, result => { + assert.ok(true, 'can register'); + done(); + }, error => { + assert.ok(false, 'can register'); + done(); + }); + }); + }); + + QUnit.module('GLTFManyfoldMeshProgressiveExtension-webonly', () => { + QUnit.test('parse', assert => { + const done = assert.async(); + const assetPath = '../examples/assets/gltf/Progressive/progressive.glb'; + new GLTFLoader() + .register(parser => new GLTFManyfoldMeshProgressiveExtension(parser)) + .load(assetPath, gltf => { + let hasBaseMesh = false; + gltf.scene.traverse(object => { + if (object.isMesh) { + hasBaseMesh = true; + } + }); + assert.ok(hasBaseMesh, 'can parse base mesh'); + done(); + }, undefined, error => { + assert.ok(false, 'can load base mesh'); + done(); + }); + }); + }); +}); diff --git a/test/build/unit.js b/test/build/unit.js index b051391..ad58678 100644 --- a/test/build/unit.js +++ b/test/build/unit.js @@ -68488,215 +68488,231 @@ }); }); + const EXTENSION_NAME = 'MSFT_lod'; + const SCREENCOVERAGE_NAME = 'MSFT_screencoverage'; + const LOADING_MODES = { All: 'all', // Default Any: 'any', Progressive: 'progressive' }; - const removeLevel = (lod, obj) => { - const levels = lod.levels; - let readIndex = 0; + // LOD.clone() and copy() copies its children + // in the order of first objects not in the levels + // and then LOD objects in the levels order. + // This function ensures the children order follows + // that because it should be less problematic in case + // where lod object is cloned or copied and compared + // between source and cloned graph. + const _map = new Map(); + const sortChildrenOrder = (lod) => { + for (let i = 0; i < lod.levels.length; i++) { + _map.set(lod.levels[i].object); + } let writeIndex = 0; - for (readIndex = 0; readIndex < levels.length; readIndex++) { - if (levels[readIndex].object !== obj) { - levels[writeIndex++] = levels[readIndex]; + for (let readIndex = 0; readIndex < lod.children.length; readIndex++) { + if (!_map.has(lod.children[readIndex])) { + lod.children[writeIndex++] = lod.children[readIndex]; } } - if (writeIndex < readIndex) { - levels.length = writeIndex; - lod.remove(obj); + _map.clear(); + lod.children.length = writeIndex; + for (let i = 0; i < lod.levels.length; i++) { + lod.children.push(lod.levels[i].object); } }; const loadScreenCoverages = (def) => { const extras = def.extras; + const extensionsDef = def.extensions; - if (!extras) { + if (!extras || !extras[SCREENCOVERAGE_NAME] || + !extensionsDef || !extensionsDef[EXTENSION_NAME]) { return []; } - const screenCoverages = extras['MSFT_screencoverage']; + const screenCoverages = extras[SCREENCOVERAGE_NAME]; + const levelsLength = extensionsDef[EXTENSION_NAME].ids.length + 1; // extra field is free structure so validate more carefully - if (!screenCoverages || !Array.isArray(screenCoverages)) { + if (!screenCoverages || !Array.isArray(screenCoverages) || + screenCoverages.length !== levelsLength) { return []; } return screenCoverages; }; - /** - * LOD Extension - * - * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/MSFT_lod - */ - class GLTFLodExtension { - constructor(parser, options={}) { - this.name = 'MSFT_lod'; - this.parser = parser; - this.options = options; + // level: 0 is the highest level + const calculateDistance = (level, lowestLevel, def, options) => { + const coverages = loadScreenCoverages(def); + + // Use the distance set by users if calculateDistance callback is set + if (options.calculateDistance) { + return options.calculateDistance(level, lowestLevel, coverages); } - _hasLODMaterial(meshIndex) { - const parser = this.parser; - const json = parser.json; - const meshDef = json.meshes[meshIndex]; + if (level === 0) return 0; - // Ignore LOD + multiple primitives so far because - // it might be a bit too complicated. - // @TODO: Fix me - if (meshDef.primitives.length > 1) { - return null; - } + const levelsLength = def.extensions[EXTENSION_NAME].ids.length + 1; - for (const primitiveDef of meshDef.primitives) { - if (primitiveDef.material === undefined) { - continue; - } - const materialDef = json.materials[primitiveDef.material]; - if (materialDef.extensions && materialDef.extensions[this.name]) { - return true; - } - } - return false; - } - - _hasLODMaterialInNode(nodeIndex) { - const parser = this.parser; - const json = parser.json; - const nodeDef = json.nodes[nodeIndex]; + // 1.0 / Math.pow(2, level * 2) is just a number that seems to be heuristically good so far. + const c = levelsLength === coverages.length ? coverages[level - 1] : 1.0 / Math.pow(2, level * 2); - const nodeIndices = (nodeDef.extensions && nodeDef.extensions[this.name]) - ? nodeDef.extensions[this.name].ids.slice() : []; - nodeIndices.unshift(nodeIndex); + // This is just an easy approximation because it is not so easy to calculate the screen coverage + // (in Three.js) since it requires camera info, geometry info (boundary box/sphere), world scale. + // And also it needs to observe the change of them. + // If users want more accurate value, they are expected to use calculateDistance hook. + return 1.0 / c; + }; - for (const nodeIndex of nodeIndices) { - if (json.nodes[nodeIndex].mesh !== undefined && - this._hasLODMaterial(json.nodes[nodeIndex].mesh)) { - return true; - } - } + const getLODNodeDependency = (level, nodeIndices, parser) => { + const nodeIndex = nodeIndices[level]; - return false; + if (level > 0) { + return parser.getDependency('node', nodeIndex); } - // level: 0 is the highest level - _calculateDistance(level, lowestLevel, def) { - const coverages = loadScreenCoverages(def); - - // Use the distance set by users if calculateDistance callback is set - if (this.options.calculateDistance) { - return this.options.calculateDistance(level, lowestLevel, coverages); + // For the highest LOD GLTFLodExtension.loadNode() needs to be avoided + // to be called again + const extensions = Object.values(parser.plugins); + extensions.push(parser); + for (let i = 0; i < extensions.length; i++) { + const ext = extensions[i]; + if (ext.constructor === GLTFLodExtension) { + continue; + } + const result = ext.loadNode && ext.loadNode(nodeIndex); + if (result) { + return result; } - - if (level === 0) return 0; - - const levelsLength = def.extensions[this.name].ids.length + 1; - - // 1.0 / Math.pow(2, level * 2) is just a number that seems to be heuristically good so far. - const c = levelsLength === coverages.length ? coverages[level - 1] : 1.0 / Math.pow(2, level * 2); - - // This is just an easy approximation. If users want more accurate value, they are expected - // to use calculateDistance hook. - return 1.0 / c; } + throw new Error('Unreachable'); + }; - _assignOnBeforeRender(meshPending, clonedMesh, level, lowestLevel, def) { - const _this = this; - const currentOnBeforeRender = clonedMesh.onBeforeRender; - clonedMesh.onBeforeRender = function () { - const clonedMesh = this; - const lod = clonedMesh.parent; - meshPending.then(mesh => { - if (_this.options.onLoadMesh) { - mesh = _this.options.onLoadMesh(lod, mesh, level, lowestLevel); - } - removeLevel(lod, clonedMesh); - lod.addLevel(mesh, _this._calculateDistance(level, lowestLevel, def)); - if (_this.options.onUpdate) { - _this.options.onUpdate(lod, mesh, level, lowestLevel); - } - }); - clonedMesh.onBeforeRender = currentOnBeforeRender; - }; - } + const LOADING_STATES = { + NotStarted: 0, + Loading: 1, + Complete: 2 + }; - // For LOD in materials - loadMesh(meshIndex) { - if (!this._hasLODMaterial(meshIndex)) { - return null; + class GLTFProgressiveLOD extends LOD { + constructor(nodeIndices, parser, options) { + super(); + this._parser = parser; + this._options = options; + this._nodeIndices = nodeIndices; + this._lowestLevel = nodeIndices.length - 1; + this._states = []; + this._objectLevels = []; // Current object level set to this level + for (let i = 0; i < nodeIndices.length; i++) { + this._states[i] = LOADING_STATES.NotStarted; + this._objectLevels[i] = nodeIndices.length; } + } - const parser = this.parser; - const json = parser.json; - const meshDef = json.meshes[meshIndex]; - - const primitiveDef = meshDef.primitives[0]; - const materialIndex = primitiveDef.material; - const materialDef = json.materials[materialIndex]; - const extensionDef = materialDef.extensions[this.name]; - - const meshIndices = [meshIndex]; - // Very hacky solution. - // Clone the mesh def, replace the material index with a lower level one, - // add to json.meshes. - // @TODO: Fix me. Polluting json is a bad idea. - for (const materialIndex of extensionDef.ids) { - const clonedMeshDef = Object.assign({}, meshDef); - clonedMeshDef.primitives = [Object.assign({}, clonedMeshDef.primitives[0])]; - clonedMeshDef.primitives[0].material = materialIndex; - meshIndices.push(json.meshes.push(clonedMeshDef) - 1); - } + initialize() { + // Load only the lowest level as initialization. + // Progressively load the higher levels on demand. + // Assuming the lowest level LOD node has meshes for now. + // @TODO: Load the lowest visible node as initialization. + return this._loadLevel(this._lowestLevel).then(node => { + const nodeDef = this._parser.json.nodes[this._nodeIndices[0]]; + for (let level = 0; level < this._nodeIndices.length - 1; level++) { + this.addLevel(node.clone(), calculateDistance(level, this._lowestLevel, nodeDef, this._options)); + this._objectLevels[level] = this._lowestLevel; + } + this.addLevel(node, calculateDistance(this._lowestLevel, this._lowestLevel, nodeDef, this._options)); + this._objectLevels[this._lowestLevel] = this._lowestLevel; + if (this._options.onUpdate) { + this._options.onUpdate(this, node, this._lowestLevel, this._lowestLevel); + } + return this; + }); + } - const lod = new LOD(); - const lowestLevel = meshIndices.length - 1; + _loadLevel(level) { + this._states[level] = LOADING_STATES.Loading; + return getLODNodeDependency(level, this._nodeIndices, this._parser).then(node => { + this._states[level] = LOADING_STATES.Complete; + if (this._options.onLoadNode) { + node = this._options.onLoadNode(this, node, level, this._lowestLevel); + } + return node; + }); + } - if (this.options.loadingMode === LOADING_MODES.Progressive) { - const firstLoadLevel = meshIndices.length - 1; - return parser.loadMesh(meshIndices[firstLoadLevel]).then(mesh => { - if (this.options.onLoadMesh) { - mesh = this.options.onLoadMesh(lod, mesh, firstLoadLevel, lowestLevel); - } + _replaceLevelObject(object, levelNum) { + const level = this.levels[levelNum]; + const oldObject = level.object; + level.object = object; + this.remove(oldObject); + this.add(object); + sortChildrenOrder(this); + } - for (let level = 0; level < meshIndices.length - 1; level++) { - const clonedMesh = mesh.clone(); - this._assignOnBeforeRender(parser.loadMesh(meshIndices[level]), - clonedMesh, level, lowestLevel, materialDef); - lod.addLevel(clonedMesh, this._calculateDistance(level, lowestLevel, materialDef)); + update(camera) { + super.update(camera); + if (this._states[this._currentLevel] === LOADING_STATES.NotStarted) { + const level = this._currentLevel; + this._loadLevel(level).then(node => { + this._replaceLevelObject(node, level); + this._objectLevels[level] = level; + + // Replace the higher level objects with this level object + // if they are not loaded yet or if they are set lower level object + // than this level. + for (let i = 0; i < level; i++) { + if (this._states[i] !== LOADING_STATES.Complete && + this._objectLevels[i] > level) { + this._replaceLevelObject(node.clone(), i); + this._objectLevels[i] = level; + } } - lod.addLevel(mesh, this._calculateDistance(firstLoadLevel, lowestLevel, materialDef)); - if (this.options.onUpdate) { - this.options.onUpdate(lod, mesh, firstLoadLevel, lowestLevel); + if (this._options.onUpdate) { + this._options.onUpdate(this, node, level, this._lowestLevel); } - - return lod; }); - } else { - const pending = []; + } + } - for (let level = 0; level < meshIndices.length; level++) { - pending.push(parser.loadMesh(meshIndices[level]).then(mesh => { - if (this.options.onLoadMesh) { - mesh = this.options.onLoadMesh(lod, mesh, level, lowestLevel); - } - lod.addLevel(mesh, this._calculateDistance(level, lowestLevel, materialDef)); - if (this.options.onUpdate) { - this.options.onUpdate(lod, mesh, level, lowestLevel); - } - })); - } + clone(recursive) { + return new this.constructor(this._nodeIndices, this._parser, this._options).copy(this, recursive); + } - return (this.options.loadingMode === LOADING_MODES.Any - ? Promise.any(pending) - : Promise.all(pending) - ).then(() => lod); + copy(source, recursive = true) { + super.copy(source, recursive); + this._parser = source._parser; + this._options = source._options; + this._lowestLevel = source._lowestLevel; + for (let i = 0; i < source._nodeIndices.length; i++) { + this._nodeIndices[i] = source._nodeIndices[i]; + this._states[i] = source._states[i] === LOADING_STATES.Complete + ? LOADING_STATES.Complete : LOADING_STATES.NotStarted; + this._objectLevels[i] = source._objectLevels[i]; } + this._nodeIndices.length = source._nodeIndices.length; + this._states.length = source._states.length; + this._objectLevels.length = source._objectLevels.length; + return this; + } + } + + /** + * LOD Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/MSFT_lod + */ + // Note: This plugin doesn't support Material LOD for simplicity + class GLTFLodExtension { + constructor(parser, options={}) { + this.name = EXTENSION_NAME; + this.parser = parser; + this.options = options; } - // For LOD in nodes - createNodeMesh(nodeIndex) { + loadNode(nodeIndex) { const parser = this.parser; const json = parser.json; const nodeDef = json.nodes[nodeIndex]; @@ -68705,85 +68721,33 @@ return null; } - // If LODs are defined in both nodes and materials, - // ignore the ones in the nodes and use the ones in the materials. - // @TODO: Process correctly - // Refer to https://github.com/KhronosGroup/glTF/issues/1952 - if (this._hasLODMaterialInNode(nodeIndex)) { - return null; - } - const extensionDef = nodeDef.extensions[this.name]; // Node indices from high to low levels const nodeIndices = extensionDef.ids.slice(); nodeIndices.unshift(nodeIndex); - const lod = new LOD(); const lowestLevel = nodeIndices.length - 1; - for (let level = 0; level < nodeIndices.length; level++) { - const nodeDef = json.nodes[nodeIndices[level]]; - if (nodeDef.mesh === undefined) { - lod.addLevel(new Object3D(), this._calculateDistance(level, lowestLevel, nodeDef)); - } - } - if (this.options.loadingMode === LOADING_MODES.Progressive) { - let firstLoadLevel = null; - for (let level = nodeIndices.length - 1; level >= 0; level--) { - if (json.nodes[nodeIndices[level]].mesh !== undefined) { - firstLoadLevel = level; - break; - } - } - - if (firstLoadLevel === null) { - return Promise.resolve(lod); - } - - return parser.createNodeMesh(nodeIndices[firstLoadLevel]).then(mesh => { - if (this.options.onLoadMesh) { - mesh = this.options.onLoadMesh(lod, mesh, firstLoadLevel, lowestLevel); - } - - for (let level = 0; level < nodeIndices.length - 1; level++) { - if (json.nodes[nodeIndices[level]].mesh === undefined) { - continue; - } - const clonedMesh = mesh.clone(); - this._assignOnBeforeRender(parser.createNodeMesh(nodeIndices[level]), - clonedMesh, level, lowestLevel, nodeDef); - lod.addLevel(clonedMesh, this._calculateDistance(level, lowestLevel, nodeDef)); - } - lod.addLevel(mesh, this._calculateDistance(firstLoadLevel, lowestLevel, nodeDef)); - - if (this.options.onUpdate) { - this.options.onUpdate(lod, mesh, firstLoadLevel, lowestLevel); - } - - return lod; - }); + return new GLTFProgressiveLOD(nodeIndices, this.parser, this.options).initialize(); } else { + const lod = new LOD(); const pending = []; for (let level = 0; level < nodeIndices.length; level++) { - if (json.nodes[nodeIndices[level]].mesh === undefined) { - continue; - } - - pending.push(parser.createNodeMesh(nodeIndices[level]).then(mesh => { - lod.addLevel(mesh, this._calculateDistance(level, lowestLevel, nodeDef)); + pending.push(getLODNodeDependency(level, nodeIndices, parser).then(node => { + if (this.options.onLoadNode) { + node = this.options.onLoadNode(lod, node, level, lowestLevel); + } + lod.addLevel(node, calculateDistance(level, lowestLevel, nodeDef, this.options)); + sortChildrenOrder(lod); if (this.options.onUpdate) { - this.options.onUpdate(lod, mesh, level, lowestLevel); + this.options.onUpdate(lod, node, level, lowestLevel); } })); } - if (pending.length === 0) { - return Promise.resolve(lod); - } - return (this.options.loadingMode === LOADING_MODES.Any ? Promise.any(pending) : Promise.all(pending) @@ -69173,10 +69137,66 @@ }); }); + class GLTFManyfoldMeshProgressiveExtension { + constructor(parser, url, binChunkOffset) { + this.name = 'MANYFOLD_mesh_progressive'; + this.parser = parser; + this.url = url; + this.binChunkOffset = binChunkOffset; + console.log(`${this.name} loader created`); + } + + static load(url, loader, onLoad, onProgress, onError, fileLoader = null) { + console.log(`${this.name} falling back to original loader`); + loader.load(url, onLoad, onProgress, onError); + } + } + + /* global QUnit */ + + QUnit.module('MANYFOLD_mesh_progressive', () => { + QUnit.module('GLTFManyfoldMeshProgressiveExtension', () => { + QUnit.test('register', assert => { + const done = assert.async(); + new GLTFLoader() + .register(parser => new GLTFManyfoldMeshProgressiveExtension(parser)) + .parse('{"asset": {"version": "2.0"}}', null, result => { + assert.ok(true, 'can register'); + done(); + }, error => { + assert.ok(false, 'can register'); + done(); + }); + }); + }); + + QUnit.module('GLTFManyfoldMeshProgressiveExtension-webonly', () => { + QUnit.test('parse', assert => { + const done = assert.async(); + const assetPath = '../examples/assets/gltf/Progressive/progressive.glb'; + new GLTFLoader() + .register(parser => new GLTFManyfoldMeshProgressiveExtension(parser)) + .load(assetPath, gltf => { + let hasBaseMesh = false; + gltf.scene.traverse(object => { + if (object.isMesh) { + hasBaseMesh = true; + } + }); + assert.ok(hasBaseMesh, 'can parse base mesh'); + done(); + }, undefined, error => { + assert.ok(false, 'can load base mesh'); + done(); + }); + }); + }); + }); + // GLTFLoader accesses navigator.userAgent // but Node.js doesn't seems to have it so // defining it here as workaround. // @TODO: Fix the root issue in Three.js - global.navigator = {userAgent: ''}; + global.navigator = { userAgent: '' }; })); diff --git a/test/index.js b/test/index.js index cb614e9..f40cdbc 100644 --- a/test/index.js +++ b/test/index.js @@ -2,7 +2,7 @@ // but Node.js doesn't seems to have it so // defining it here as workaround. // @TODO: Fix the root issue in Three.js -global.navigator = {userAgent: ''}; +global.navigator = { userAgent: '' }; import './KHR_materials_variants.js' import './EXT_mesh_gpu_instancing.js' @@ -10,3 +10,4 @@ import './EXT_text.js' import './EXT_texture_video.js' import './MSFT_lod.js' import './MSFT_texture_dds.js' +import './MANYFOLD_mesh_progressive.js'