diff --git a/.gitignore b/.gitignore
index 0f5f50e0..cd54965b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
dist
build
node_modules
+.vscode/**/*
+demo/assets/data/**/*
\ No newline at end of file
diff --git a/demo/index.html b/demo/index.html
index 7889e928..a8180947 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -335,7 +335,7 @@
document.getElementById('check-icon').style.display = visible ? 'block' : 'none';
}
- window.viewSplat = function() {
+ window.viewSplat = function(options = {}) {
const viewFile = document.getElementById("viewFile");
const alphaRemovalThreshold = parseInt(document.getElementById("alphaRemovalThresholdView").value);
@@ -399,11 +399,12 @@
currentCameraUpArray = cameraUpArray;
currentCameraPositionArray = cameraPositionArray;
currentCameraLookAtArray = cameraLookAtArray;
+
try {
const fileReader = new FileReader();
fileReader.onload = function(){
try {
- runViewer(fileReader.result, isPly, alphaRemovalThreshold, cameraUpArray, cameraPositionArray, cameraLookAtArray);
+ runViewer(fileReader.result, isPly, alphaRemovalThreshold, cameraUpArray, cameraPositionArray, cameraLookAtArray, options.editMode);
} catch (e) {
setViewError("Could not view scene.");
}
@@ -440,11 +441,12 @@
}
});
- function runViewer(splatBufferData, isPly, alphaRemovalThreshold, cameraUpArray, cameraPositionArray, cameraLookAtArray) {
+ function runViewer(splatBufferData, isPly, alphaRemovalThreshold, cameraUpArray, cameraPositionArray, cameraLookAtArray, editMode) {
const viewerOptions = {
'cameraUp': cameraUpArray,
'initialCameraPosition': cameraPositionArray,
- 'initialCameraLookAt': cameraLookAtArray
+ 'initialCameraLookAt': cameraLookAtArray,
+ 'frameloop': 'demand',
};
const sceneOptions = {
'halfPrecisionCovariancesOnGPU': true,
@@ -461,7 +463,15 @@
document.getElementById("demo-content").style.display = 'none';
document.body.style.backgroundColor = "#000000";
history.pushState("ViewSplat", null);
+
const viewer = new GaussianSplats3D.Viewer(viewerOptions);
+
+ let editor;
+ if (editMode) {
+ const editorOptions = {};
+ editor = new GaussianSplats3D.Editor(viewer, editorOptions);
+ }
+
viewer.loadSplatBuffer(splatBuffer, sceneOptions)
.then(() => {
viewer.start();
@@ -574,6 +584,8 @@
View
+ Edit
+
Reset
diff --git a/src/Editor.js b/src/Editor.js
new file mode 100644
index 00000000..02f06e70
--- /dev/null
+++ b/src/Editor.js
@@ -0,0 +1,127 @@
+import * as THREE from 'three';
+import { getCurrentTime } from './Util.js';
+
+export class Editor {
+ constructor(viewer, options = {}) {
+ this.viewer = viewer;
+
+ this.editPanel = null;
+
+ this.pointerUpHandlerPlaneFinder = this.onPointerUpPlaneFinder.bind(this);
+
+ this.initialized = false;
+ this.init();
+ }
+
+ init() {
+ if (this.initialized) return;
+
+ this.setupEditPanel();
+
+ this.initialized = true;
+ }
+
+ setupEditPanel() {
+ this.editPanel = document.createElement('div');
+ this.editPanel.style.position = 'absolute';
+ this.editPanel.style.padding = '10px';
+ this.editPanel.style.backgroundColor = '#cccccc';
+ this.editPanel.style.border = '#aaaaaa 1px solid';
+ this.editPanel.style.zIndex = 90;
+ this.editPanel.style.width = '375px';
+ this.editPanel.style.fontFamily = 'arial';
+ this.editPanel.style.fontSize = '10pt';
+ this.editPanel.style.textAlign = 'left';
+
+
+ const editTable = document.createElement('div');
+ editTable.style.width = '100%';
+
+
+ // Plane Finder
+ const planeFinder = document.createElement('div');
+ planeFinder.style.width = '100%';
+ planeFinder.style.display = 'flex';
+ planeFinder.style.flexDirection = 'row';
+ planeFinder.style.justifyContent = 'space-between';
+
+ const planeFinderLabel = document.createElement('p');
+ planeFinderLabel.id = 'planeFinderLabel';
+ planeFinderLabel.innerHTML =
+ `Up: ${this.viewer.camera.up.x.toFixed(3)}, ${this.viewer.camera.up.y.toFixed(3)}, ${this.viewer.camera.up.z.toFixed(3)}`;
+
+ const planeFinderButton = document.createElement('button');
+ planeFinderButton.innerHTML = 'Find ground plane';
+ planeFinderButton.addEventListener('click', () => {
+ this.setupPlaneFinder();
+ });
+
+ planeFinder.appendChild(planeFinderLabel);
+ planeFinder.appendChild(planeFinderButton);
+
+ editTable.appendChild(planeFinder);
+
+ this.editPanel.appendChild(editTable);
+ this.editPanel.style.display = 'block';
+ this.viewer.renderer.domElement.parentElement.prepend(this.editPanel);
+ }
+
+ setupPlaneFinder() {
+ if (this.viewer.useBuiltInControls) {
+ this.viewer.rootElement.removeEventListener('pointerup', this.viewer.pointerUpHandler);
+ this.viewer.rootElement.addEventListener('pointerup', this.pointerUpHandlerPlaneFinder);
+ }
+ }
+
+ teardownPlaneFinder() {
+ if (this.viewer.useBuiltInControls) {
+ this.viewer.rootElement.removeEventListener('pointerup', this.pointerUpHandlerPlaneFinder);
+ this.viewer.rootElement.addEventListener('pointerup', this.viewer.pointerUpHandler);
+ }
+ }
+
+ onPointerUpPlaneFinder = function() {
+ const renderDimensions = new THREE.Vector2();
+ const clickOffset = new THREE.Vector2();
+ // const toNewFocalPoint = new THREE.Vector3();
+ const outHits = [];
+ let points = [];
+
+ return function(mouse) {
+ clickOffset.copy(this.viewer.mousePosition).sub(this.viewer.mouseDownPosition);
+ const mouseUpTime = getCurrentTime();
+ const wasClick = mouseUpTime - this.viewer.mouseDownTime < 0.5 && clickOffset.length() < 2;
+
+ if (!this.transitioningCameraTarget && wasClick) {
+ this.viewer.getRenderDimensions(renderDimensions);
+ outHits.length = 0;
+ this.viewer.raycaster.setFromCameraAndScreenPosition(this.viewer.camera, this.viewer.mousePosition, renderDimensions);
+ this.viewer.mousePosition.set(mouse.offsetX, mouse.offsetY);
+ this.viewer.raycaster.intersectSplatMesh(this.viewer.splatMesh, outHits);
+
+ if (outHits.length > 0) {
+ const intersectionPoint = outHits[0].origin;
+
+ points.push(intersectionPoint);
+
+ if (points.length === 3) {
+ const plane = new THREE.Plane();
+
+ plane.setFromCoplanarPoints(points[0], points[1], points[2]);
+
+ this.viewer.camera.up = plane.normal;
+ this.viewer.invalidate();
+
+ // Update label
+ const planeFinderLabel = document.getElementById('planeFinderLabel');
+ planeFinderLabel.innerHTML =
+ `${this.viewer.camera.up.x.toFixed(3)},${this.viewer.camera.up.y.toFixed(3)},${this.viewer.camera.up.z.toFixed(3)}`;
+
+ points = [];
+ this.teardownPlaneFinder();
+ }
+ }
+ }
+ };
+ }();
+}
diff --git a/src/Viewer.js b/src/Viewer.js
index 533f7b74..ad476cfc 100644
--- a/src/Viewer.js
+++ b/src/Viewer.js
@@ -1,14 +1,14 @@
import * as THREE from 'three';
+import { Constants } from './Constants.js';
+import { LoadingSpinner } from './LoadingSpinner.js';
import { OrbitControls } from './OrbitControls.js';
import { PlyLoader } from './PlyLoader.js';
-import { SplatLoader } from './SplatLoader.js';
-import { LoadingSpinner } from './LoadingSpinner.js';
-import { SceneHelper } from './SceneHelper.js';
import { Raycaster } from './raycaster/Raycaster.js';
+import { SceneHelper } from './SceneHelper.js';
+import { SplatLoader } from './SplatLoader.js';
import { SplatMesh } from './SplatMesh.js';
-import { createSortWorker } from './worker/SortWorker.js';
-import { Constants } from './Constants.js';
import { getCurrentTime } from './Util.js';
+import { createSortWorker } from './worker/SortWorker.js';
const THREE_CAMERA_FOV = 50;
const MINIMUM_DISTANCE_TO_NEW_FOCAL_POINT = .75;
@@ -22,6 +22,7 @@ export class Viewer {
if (!params.initialCameraLookAt) params.initialCameraLookAt = [0, 0, 0];
if (params.selfDrivenMode === undefined) params.selfDrivenMode = true;
if (params.useBuiltInControls === undefined) params.useBuiltInControls = true;
+ if (!params.frameloop) params.frameloop = 'always';
this.rootElement = params.rootElement;
this.usingExternalCamera = params.camera ? true : false;
@@ -76,6 +77,14 @@ export class Viewer {
this.mouseDownPosition = new THREE.Vector2();
this.mouseDownTime = null;
+ this.pointerUpHandler = this.onMouseUp.bind(this);
+
+ this.loadingSpinner = new LoadingSpinner();
+ this.loadingSpinner.hide();
+
+ this.viewerNeedsUpdate = true;
+ this.frameloop = params.frameloop; // 'demand' | 'always'
+
this.initialized = false;
this.init();
}
@@ -86,8 +95,8 @@ export class Viewer {
if (!this.rootElement && !this.usingExternalRenderer) {
this.rootElement = document.createElement('div');
- this.rootElement.style.width = '100%';
- this.rootElement.style.height = '100%';
+ this.rootElement.style.width = '100vw';
+ this.rootElement.style.height = '100vh';
document.body.appendChild(this.rootElement);
}
@@ -125,11 +134,13 @@ export class Viewer {
this.controls.maxPolarAngle = Math.PI * .75;
this.controls.minPolarAngle = 0.1;
this.controls.enableDamping = true;
- this.controls.dampingFactor = 0.05;
+ this.controls.dampingFactor = 0.25;
+ this.controls.zoomToCursor = true;
this.controls.target.copy(this.initialCameraLookAt);
this.rootElement.addEventListener('pointermove', this.onMouseMove.bind(this), false);
this.rootElement.addEventListener('pointerdown', this.onMouseDown.bind(this), false);
- this.rootElement.addEventListener('pointerup', this.onMouseUp.bind(this), false);
+ this.rootElement.addEventListener('pointerup', this.pointerUpHandler, false);
+ this.controls.addEventListener('change', this.onControlsChange.bind(this), false);
window.addEventListener('keydown', this.onKeyDown.bind(this), false);
}
@@ -183,12 +194,17 @@ export class Viewer {
}
break;
}
+
+ this.invalidate();
};
}();
onMouseMove(mouse) {
this.mousePosition.set(mouse.offsetX, mouse.offsetY);
+ if (this.showMeshCursor) {
+ this.invalidate();
+ }
}
onMouseDown() {
@@ -225,9 +241,16 @@ export class Viewer {
}
}
};
-
}();
+ onControlsChange() {
+ this.invalidate();
+ }
+
+ invalidate() {
+ this.viewerNeedsUpdate = true;
+ }
+
getRenderDimensions(outDimensions) {
if (this.rootElement) {
outDimensions.x = this.rootElement.offsetWidth;
@@ -240,6 +263,7 @@ export class Viewer {
setupInfoPanel() {
this.infoPanel = document.createElement('div');
this.infoPanel.style.position = 'absolute';
+ this.infoPanel.style.right = '0px';
this.infoPanel.style.padding = '10px';
this.infoPanel.style.backgroundColor = '#cccccc';
this.infoPanel.style.border = '#aaaaaa 1px solid';
@@ -558,6 +582,7 @@ export class Viewer {
this.updateSplatMeshUniforms();
}
lastRendererSize.copy(currentRendererSize);
+ this.invalidate();
}
};
@@ -568,7 +593,11 @@ export class Viewer {
requestAnimationFrame(this.selfDrivenUpdateFunc);
}
this.update();
- this.render();
+
+ if (this.frameloop === 'always' || this.viewerNeedsUpdate) {
+ this.render();
+ this.viewerNeedsUpdate = false;
+ }
}
update() {
diff --git a/src/index.js b/src/index.js
index 769fe29e..4b589043 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,7 +1,8 @@
-import { PlyParser } from './PlyParser.js';
+import { Editor } from './Editor.js';
import { PlyLoader } from './PlyLoader.js';
-import { SplatLoader } from './SplatLoader.js';
+import { PlyParser } from './PlyParser.js';
import { SplatBuffer } from './SplatBuffer.js';
+import { SplatLoader } from './SplatLoader.js';
import { Viewer } from './Viewer.js';
export {
@@ -9,5 +10,6 @@ export {
PlyLoader,
SplatLoader,
SplatBuffer,
- Viewer
+ Viewer,
+ Editor,
};