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;