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, };