diff --git a/demo/index.html b/demo/index.html index f04c1e44..b88c1166 100644 --- a/demo/index.html +++ b/demo/index.html @@ -782,7 +782,7 @@ -
+

Mouse input @@ -813,7 +813,7 @@ -
P
+
U
Toggle controls orientation marker @@ -832,6 +832,38 @@
Rotate camera-up clockwise + + + + + +
P
+ Toggle point-cloud mode + + + + + + +
O
+ Toggle orthographic mode + + + + + + +
=
+ Increase splat scale + + + + + + +
-
+ Decrease splat scale +
diff --git a/package-lock.json b/package-lock.json index 66e754f2..8d4a337b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@mkkellogg/gaussian-splats-3d", - "version": "0.3.7", + "version": "0.3.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@mkkellogg/gaussian-splats-3d", - "version": "0.3.7", + "version": "0.3.8", "license": "MIT", "devDependencies": { "@babel/core": "7.22.0", diff --git a/package.json b/package.json index b4f78848..c43b04a8 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "type": "git", "url": "https://github.com/mkkellogg/GaussianSplats3D" }, - "version": "0.3.7", + "version": "0.3.8", "description": "Three.js-based 3D Gaussian splat viewer", "module": "build/gaussian-splats-3d.module.js", "main": "build/gaussian-splats-3d.umd.cjs", diff --git a/src/DropInViewer.js b/src/DropInViewer.js index 4bc471f1..cbe15187 100644 --- a/src/DropInViewer.js +++ b/src/DropInViewer.js @@ -19,6 +19,7 @@ export class DropInViewer extends THREE.Group { options.renderer = undefined; this.viewer = new Viewer(options); + this.splatMesh = null; this.callbackMesh = DropInViewer.createCallbackMesh(); this.add(this.callbackMesh); @@ -49,11 +50,7 @@ export class DropInViewer extends THREE.Group { */ addSplatScene(path, options = {}) { if (options.showLoadingUI !== false) options.showLoadingUI = true; - const loadPromise = this.viewer.addSplatScene(path, options); - loadPromise.then(() => { - this.add(this.viewer.splatMesh); - }); - return loadPromise; + return this.viewer.addSplatScene(path, options); } /** @@ -76,11 +73,7 @@ export class DropInViewer extends THREE.Group { */ addSplatScenes(sceneOptions, showLoadingUI) { if (showLoadingUI !== false) showLoadingUI = true; - const loadPromise = this.viewer.addSplatScenes(sceneOptions, showLoadingUI); - loadPromise.then(() => { - this.add(this.viewer.splatMesh); - }); - return loadPromise; + return this.viewer.addSplatScenes(sceneOptions, showLoadingUI); } /** @@ -92,11 +85,22 @@ export class DropInViewer extends THREE.Group { return this.viewer.getSplatScene(sceneIndex); } + removeSplatScene(index) { + return this.viewer.removeSplatScene(index); + } + dispose() { return this.viewer.dispose(); } static onBeforeRender(viewer, renderer, threeScene, camera) { + if (this.splatMesh !== this.viewer.splatMesh) { + if (this.splatMesh) { + this.remove(this.splatMesh); + } + this.splatMesh = this.viewer.splatMesh; + this.add(this.viewer.splatMesh); + } viewer.update(renderer, camera); } diff --git a/src/SplatMesh.js b/src/SplatMesh.js index 2abccc61..71f2bf25 100644 --- a/src/SplatMesh.js +++ b/src/SplatMesh.js @@ -96,6 +96,7 @@ export class SplatMesh extends THREE.Mesh { this.disposed = false; this.lastRenderer = null; + this.visible = false; } /** @@ -672,16 +673,28 @@ export class SplatMesh extends THREE.Mesh { } this.scenes = newScenes; + let splatBuffersChanged = false; + if (splatBuffers.length !== this.lastBuildScenes.length) { + splatBuffersChanged = true; + } else { + for (let i = 0; i < splatBuffers.length; i++) { + const splatBuffer = splatBuffers[i]; + if (splatBuffer !== this.lastBuildScenes[i].splatBuffer) { + splatBuffersChanged = true; + break; + } + } + } + let isUpdateBuild = true; - if (this.scenes.length > 1 || + if (this.scenes.length !== 1 || this.lastBuildSceneCount !== this.scenes.length || this.lastBuildMaxSplatCount !== maxSplatCount || - this.scenes[0].splatBuffer !== this.lastBuildScenes[0].splatBuffer) { + splatBuffersChanged) { isUpdateBuild = false; } if (!isUpdateBuild) { - isUpdateBuild = false; this.boundingBox = new THREE.Box3(); this.maxSplatDistanceFromSceneCenter = 0; this.visibleRegionBufferRadius = 0; @@ -723,7 +736,7 @@ export class SplatMesh extends THREE.Mesh { this.lastBuildMaxSplatCount = this.getMaxSplatCount(); this.lastBuildSceneCount = this.scenes.length; - if (finalBuild) { + if (finalBuild && this.scenes.length > 0) { this.buildSplatTree(sceneOptions.map(options => options.splatAlphaRemovalThreshold || 1), onSplatTreeIndexesUpload, onSplatTreeConstruction) .then(() => { @@ -731,6 +744,8 @@ export class SplatMesh extends THREE.Mesh { }); } + this.visible = (this.scenes.length > 0); + return buildResults; } @@ -789,6 +804,7 @@ export class SplatMesh extends THREE.Mesh { this.disposed = true; this.lastRenderer = null; + this.visible = false; } /** @@ -819,10 +835,13 @@ export class SplatMesh extends THREE.Mesh { } disposeSplatTree() { - if (this.splatTree) this.splatTree.dispose(); - this.splatTree = null; - if (this.baseSplatTree) this.baseSplatTree.dispose(); - this.baseSplatTree = null; + if (this.splatTree) { + this.splatTree.dispose(); + this.splatTree = null; + } else if (this.baseSplatTree) { + this.baseSplatTree.dispose(); + this.baseSplatTree = null; + } } getSplatTree() { diff --git a/src/Viewer.js b/src/Viewer.js index 8871b497..40bfc4ed 100644 --- a/src/Viewer.js +++ b/src/Viewer.js @@ -191,6 +191,7 @@ export class Viewer { this.sortPromiseResolver = null; this.downloadPromisesToAbort = {}; this.splatSceneLoadPromise = null; + this.splatSceneRemovalPromise = null; this.loadingSpinner = new LoadingSpinner(null, this.rootElement || document.body); this.loadingSpinner.hide(); @@ -545,8 +546,9 @@ export class Viewer { }(); - isLoading() { - return Object.keys(this.downloadPromisesToAbort) > 0 || this.splatSceneLoadPromise !== null; + isLoadingOrUnloading() { + return Object.keys(this.downloadPromisesToAbort) > 0 || this.splatSceneLoadPromise !== null || + this.splatSceneRemovalPromise !== null; } isDisposingOrDisposed() { @@ -585,8 +587,8 @@ export class Viewer { */ addSplatScene(path, options = {}) { - if (this.isLoading()) { - throw new Error('Cannot add splat scene while another load is already in progress.'); + if (this.isLoadingOrUnloading()) { + throw new Error('Cannot add splat scene while another load or unload is already in progress.'); } if (this.isDisposingOrDisposed()) { @@ -610,7 +612,10 @@ export class Viewer { if (showLoadingUI !== false) showLoadingUI = true; let loadingTaskId = null; - if (showLoadingUI) loadingTaskId = this.loadingSpinner.addTask('Downloading...'); + if (showLoadingUI) { + this.loadingSpinner.removeAllTasks(); + loadingTaskId = this.loadingSpinner.addTask('Downloading...'); + } let downloadDone = false; @@ -693,8 +698,8 @@ export class Viewer { */ addSplatScenes(sceneOptions, showLoadingUI = true, onProgress = undefined) { - if (this.isLoading()) { - throw new Error('Cannot add splat scene while another load is already in progress.'); + if (this.isLoadingOrUnloading()) { + throw new Error('Cannot add splat scene while another load or unload is already in progress.'); } if (this.isDisposingOrDisposed()) { @@ -703,7 +708,10 @@ export class Viewer { const fileCount = sceneOptions.length; const percentComplete = []; - if (showLoadingUI) this.loadingSpinner.show(); + if (showLoadingUI) { + this.loadingSpinner.removeAllTasks(); + this.loadingSpinner.show(); + } const onLoadProgress = (fileIndex, percent, percentLabel) => { percentComplete[fileIndex] = percent; let totalPercent = 0; @@ -947,7 +955,7 @@ export class Viewer { // If we aren't calculating the splat distances from the center on the GPU, the sorting worker needs splat centers and // transform indexes so that it can calculate those distance values. - if (!this.gpuAcceleratedSort) { + if (!this.gpuAcceleratedSort && this.sortWorker) { this.sortWorker.postMessage({ 'centers': buildResults.centers.buffer, 'transformIndexes': buildResults.transformIndexes.buffer, @@ -979,7 +987,7 @@ export class Viewer { if (this.sortWorker && this.sortWorker.maxSplatCount !== maxSplatCount) { this.disposeSortWorker(); } - if (!this.sortWorker) { + if (!this.sortWorker && maxSplatCount > 0) { this.setupSortWorker(this.splatMesh).then(() => { finish(buildResults, resolve); }); @@ -1115,15 +1123,30 @@ export class Viewer { disposeSortWorker() { if (this.sortWorker) this.sortWorker.terminate(); this.sortWorker = null; + this.sortPromise = null; + if (this.sortPromiseResolver) { + this.sortPromiseResolver(); + this.sortPromiseResolver = null; + } this.sortRunning = false; } removeSplatScene(index, showLoadingUI = true) { - if (this.isDisposingOrDisposed()) return Promise.resolve(); - return new Promise((resolve, reject) => { + if (this.isLoadingOrUnloading()) { + throw new Error('Cannot remove splat scene while another load or unload is already in progress.'); + } + + if (this.isDisposingOrDisposed()) { + throw new Error('Cannot remove splat scene after dispose() is called.'); + } + + let sortPromise; + + this.splatSceneRemovalPromise = new Promise((resolve, reject) => { let revmovalTaskId; if (showLoadingUI) { + this.loadingSpinner.removeAllTasks(); this.loadingSpinner.show(); revmovalTaskId = this.loadingSpinner.addTask('Removing splat scene...'); } @@ -1135,9 +1158,11 @@ export class Viewer { } }; - const onDone = () => { + const onDone = (error) => { checkAndHideLoadingUI(); - resolve(); + this.splatSceneRemovalPromise = null; + if (!error) resolve(); + else reject(error); }; const checkForEarlyExit = () => { @@ -1148,59 +1173,60 @@ export class Viewer { return false; }; - delayedExecute(() => { - this.sortPromise.then(() => { - if (checkForEarlyExit()) return; - const savedSplatBuffers = []; - const savedSceneOptions = []; - const savedSceneTransformComponents = []; - const savedVisibleRegionFadeStartRadius = this.splatMesh.visibleRegionFadeStartRadius; - for (let i = 0; i < this.splatMesh.scenes.length; i++) { - if (i !== index) { - const scene = this.splatMesh.scenes[i]; - savedSplatBuffers.push(scene.splatBuffer); - savedSceneOptions.push(this.splatMesh.sceneOptions[i]); - savedSceneTransformComponents.push({ - 'position': scene.position.clone(), - 'quaternion': scene.quaternion.clone(), - 'scale': scene.scale.clone() - }); - } + sortPromise = this.sortPromise || Promise.resolve(); + sortPromise.then(() => { + if (checkForEarlyExit()) return; + const savedSplatBuffers = []; + const savedSceneOptions = []; + const savedSceneTransformComponents = []; + const savedVisibleRegionFadeStartRadius = this.splatMesh.visibleRegionFadeStartRadius; + for (let i = 0; i < this.splatMesh.scenes.length; i++) { + if (i !== index) { + const scene = this.splatMesh.scenes[i]; + savedSplatBuffers.push(scene.splatBuffer); + savedSceneOptions.push(this.splatMesh.sceneOptions[i]); + savedSceneTransformComponents.push({ + 'position': scene.position.clone(), + 'quaternion': scene.quaternion.clone(), + 'scale': scene.scale.clone() + }); } - this.splatMesh.dispose(); - this.createSplatMesh(); - - this.addSplatBuffers(savedSplatBuffers, savedSceneOptions, true, false, true) + } + this.disposeSortWorker(); + this.splatMesh.dispose(); + this.createSplatMesh(); + this.addSplatBuffers(savedSplatBuffers, savedSceneOptions, true, false, true) + .then(() => { + if (checkForEarlyExit()) return; + checkAndHideLoadingUI(); + this.splatMesh.visibleRegionFadeStartRadius = savedVisibleRegionFadeStartRadius; + this.splatMesh.scenes.forEach((scene, index) => { + scene.position.copy(savedSceneTransformComponents[index].position); + scene.quaternion.copy(savedSceneTransformComponents[index].quaternion); + scene.scale.copy(savedSceneTransformComponents[index].scale); + }); + this.splatMesh.updateTransforms(); + this.splatRenderReady = false; + this.updateSplatSort(true) .then(() => { - if (checkForEarlyExit()) return; - checkAndHideLoadingUI(); - this.splatMesh.visibleRegionFadeStartRadius = savedVisibleRegionFadeStartRadius; - this.splatMesh.scenes.forEach((scene, index) => { - scene.position.copy(savedSceneTransformComponents[index].position); - scene.quaternion.copy(savedSceneTransformComponents[index].quaternion); - scene.scale.copy(savedSceneTransformComponents[index].scale); - }); - this.splatMesh.updateTransforms(); - - this.splatRenderReady = false; - this.updateSplatSort(true) - .then(() => { - if (checkForEarlyExit()) { - this.splatRenderReady = true; - return; - } - this.sortPromise.then(() => { - this.splatRenderReady = true; - onDone(); - }); + if (checkForEarlyExit()) { + this.splatRenderReady = true; + return; + } + sortPromise = this.sortPromise || Promise.resolve(); + sortPromise.then(() => { + this.splatRenderReady = true; + onDone(); }); - }) - .catch((e) => { - reject(e); }); + }) + .catch((e) => { + onDone(e); }); }); }); + + return this.splatSceneRemovalPromise; } /** @@ -1572,7 +1598,7 @@ export class Viewer { this.getRenderDimensions(renderDimensions); const cameraLookAtPosition = this.controls ? this.controls.target : null; const meshCursorPosition = this.showMeshCursor ? this.sceneHelper.meshCursor.position : null; - const splatRenderCountPct = this.splatRenderCount / splatCount * 100; + const splatRenderCountPct = splatCount > 0 ? this.splatRenderCount / splatCount * 100 : 0; this.infoPanel.update(renderDimensions, this.camera.position, cameraLookAtPosition, this.camera.up, this.camera.isOrthographicCamera, meshCursorPosition, this.currentFPS || 'N/A', splatCount, this.splatRenderCount, splatRenderCountPct, @@ -1618,6 +1644,7 @@ export class Viewer { return async function(force = false) { if (this.sortRunning) return; + if (this.splatMesh.getSplatCount() <= 0) return; let angleDiff = 0; let positionDiff = 0; @@ -1639,9 +1666,6 @@ export class Viewer { this.sortRunning = true; const { splatRenderCount, shouldSortAll } = this.gatherSceneNodesForSort(); this.splatRenderCount = splatRenderCount; - this.sortPromise = new Promise((resolve) => { - this.sortPromiseResolver = resolve; - }); mvpMatrix.copy(this.camera.matrixWorld).invert(); mvpMatrix.premultiply(this.camera.projectionMatrix); @@ -1689,6 +1713,11 @@ export class Viewer { sortMessage.precomputedDistances = this.sortWorkerPrecomputedDistances; } } + + this.sortPromise = new Promise((resolve) => { + this.sortPromiseResolver = resolve; + }); + this.sortWorker.postMessage({ 'sort': sortMessage }); diff --git a/src/splattree/SplatTree.js b/src/splattree/SplatTree.js index c008656a..8562fba9 100644 --- a/src/splattree/SplatTree.js +++ b/src/splattree/SplatTree.js @@ -78,7 +78,6 @@ class SplatSubTree { } } -let splatTreeWorker; function createSplatTreeWorker(self) { let WorkerSplatTreeNodeIDGen = 0; @@ -278,7 +277,7 @@ function createSplatTreeWorker(self) { }; } -function workerProcessCenters(centers, transferBuffers, maxDepth, maxCentersPerNode) { +function workerProcessCenters(splatTreeWorker, centers, transferBuffers, maxDepth, maxCentersPerNode) { splatTreeWorker.postMessage({ 'process': { 'centers': centers, @@ -289,15 +288,14 @@ function workerProcessCenters(centers, transferBuffers, maxDepth, maxCentersPerN } function checkAndCreateWorker() { - if (!splatTreeWorker) { - splatTreeWorker = new Worker( - URL.createObjectURL( - new Blob(['(', createSplatTreeWorker.toString(), ')(self)'], { - type: 'application/javascript', - }), - ), - ); - } + const splatTreeWorker = new Worker( + URL.createObjectURL( + new Blob(['(', createSplatTreeWorker.toString(), ')(self)'], { + type: 'application/javascript', + }), + ), + ); + return splatTreeWorker; } /** @@ -319,8 +317,8 @@ export class SplatTree { } diposeSplatTreeWorker() { - if (splatTreeWorker) splatTreeWorker.terminate(); - splatTreeWorker = null; + if (this.splatTreeWorker) this.splatTreeWorker.terminate(); + this.splatTreeWorker = null; }; /** @@ -335,7 +333,7 @@ export class SplatTree { * @return {undefined} */ processSplatMesh = function(splatMesh, filterFunc = () => true, onIndexesUpload, onSplatTreeConstruction) { - checkAndCreateWorker(); + if (!this.splatTreeWorker) this.splatTreeWorker = checkAndCreateWorker(); this.splatMesh = splatMesh; this.subTrees = []; @@ -391,7 +389,7 @@ export class SplatTree { allCenters.push(sceneCenters); } - splatTreeWorker.onmessage = (e) => { + this.splatTreeWorker.onmessage = (e) => { if (checkForEarlyExit()) return; @@ -423,7 +421,7 @@ export class SplatTree { if (checkForEarlyExit()) return; if (onIndexesUpload) onIndexesUpload(true); const transferBuffers = allCenters.map((array) => array.buffer); - workerProcessCenters(allCenters, transferBuffers, this.maxDepth, this.maxCentersPerNode); + workerProcessCenters(this.splatTreeWorker, allCenters, transferBuffers, this.maxDepth, this.maxCentersPerNode); }); }); diff --git a/util/create-ksplat.js b/util/create-ksplat.js index 65c30bcf..2d90baa5 100644 --- a/util/create-ksplat.js +++ b/util/create-ksplat.js @@ -1,4 +1,5 @@ import * as GaussianSplats3D from '../build/gaussian-splats-3d.module.js'; +import * as THREE from '../build/demo/lib/three.module.js'; import * as fs from 'fs'; if (process.argv.length < 4) { @@ -21,7 +22,7 @@ const path = intputFile.toLowerCase().trim(); const format = GaussianSplats3D.LoaderUtils.sceneFormatFromPath(path); const splatBuffer = fileBufferToSplatBuffer(fileData.buffer, format, compressionLevel, splatAlphaRemovalThreshold); -fs.writeFileSync(outputFile, splatBuffer.bufferData); +fs.writeFileSync(outputFile, Buffer.from(splatBuffer.bufferData)); function fileBufferToSplatBuffer(fileBufferData, format, compressionLevel, alphaRemovalThreshold) { let splatBuffer;