-
Notifications
You must be signed in to change notification settings - Fork 330
Initial editor #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Initial editor #49
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| dist | ||
| build | ||
| node_modules | ||
| .vscode/**/* | ||
| demo/assets/data/**/* | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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', | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should make this an enum.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure! |
||
| }; | ||
| 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; | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this variable used anywhere?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well no, not really. This would be a non-issue if we were to go with the inheritance route. |
||
| if (editMode) { | ||
| const editorOptions = {}; | ||
| editor = new GaussianSplats3D.Editor(viewer, editorOptions); | ||
| } | ||
|
|
||
| viewer.loadSplatBuffer(splatBuffer, sceneOptions) | ||
| .then(() => { | ||
| viewer.start(); | ||
|
|
@@ -574,6 +584,8 @@ | |
| <br> | ||
| <span class="button" onclick="window.viewSplat()">View</span> | ||
| | ||
| <span class="button" onclick="window.viewSplat({editMode: true})">Edit</span> | ||
| | ||
| <span class="button" onclick="reset()">Reset</span> | ||
| <br> | ||
| <br> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 = | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see this code in multiple places, maybe it's worth making an
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, there are a few places where we could refactor to separate functions |
||
| `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); | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I definitely think we need a less intrusive way of overriding behavior in
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm mainly a React developer and am not all that familiar with best practices in vanilla and OOP.This could be a case of lack of knowledge on my part. The effect I was going for was to have a default global state that is the same as just the viewer. I couldn't really figure out how to do it with inheritance + overriding without loosing the default state. In my mind we had to store those handlers away anyhow and I figured that we might aswell keep the class itself intact as that stored default state.
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So I was thinking something along the lines of: class Viewer {
constructor() {
window.addEventListener("mousedown", this.onMouseDown.bind(this));
}
onMouseDown() {
// default behavior
}
}
const PointerMode = {
Default: 0,
CameraUpSelection: 1
};
class Editor extends Viewer {
constructor() {
super();
this.pointerMode = PointerMode.Default;
this.setupEditPanel();
}
setupEditPanel() {
const planeFinderButton = document.createElement('button');
planeFinderButton.innerHTML = 'Find ground plane';
planeFinderButton.addEventListener('click', () => {
this.pointerMode = PointerMode.CameraUpSelection;
});
}
// override Viewer.onMouseDown()
onMouseDown() {
if (this.pointerMode === PointerMode.CameraUpSelection) {
// Do custom stuff for camera up selection
} else {
super.onMouseDown();
}
}
}This would retain default behavior when not selecting the camera-up vector. (This code probably contains errors, but hopefully it conveys the idea :) )
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, this is absolutely more clear, I like it. With this structure I would prefer to inherit the Viewer class. I also think a user should be able to jack into the events and extend the functionality in a straight forward manner. I'm not really sure how one would do that right now, maybe something like how I bound the invalidate() method to the controllers change event. Is there something with this approach that would hinder that?
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think an inheritance based approach will hinder that; we can still extend functions in
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 on making |
||
| 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; | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we need to change
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed |
||
|
|
||
| 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(); | ||
| } | ||
| } | ||
| } | ||
| }; | ||
| }(); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we call this flag
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, I was just kind of going for the "viewMatrixNeedsUpdate"-feel, but I don't have any hard stance on either |
||
| 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; | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any particular reason for this change? If the editor needs higher damping, maybe we can make it a parameter to the
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When the damping is a little lower the "demand" rendering has a much quicker effect. I little of a personal preference, but I also think it's easier to work with in an edit mode, when you want more control of the angles. With that said, this change should maybe be inside the Editor class if we decide to keep it.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Regarding if we should make it a parameter to the Viewer class I think we can keep it as it is and let the users reach into the viewer to modify the controls and camera. In my experience it's pretty common practice when working with three. Maybe we could mark them as public for clarity.
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed that this should be set from within the |
||
| this.controls.zoomToCursor = true; | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm also curious why this setting was changed.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same thing as previous, a little more control when trying to get specific angles when editing. Also this one should be inside the Editor class though.
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we make a toggle in the Editor UI for this?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Absolutely |
||
| 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() { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,15 @@ | ||
| 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 { | ||
| PlyParser, | ||
| PlyLoader, | ||
| SplatLoader, | ||
| SplatBuffer, | ||
| Viewer | ||
| Viewer, | ||
| Editor, | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to add this? The
demodirectory is really meant to be treated as a source code directory (which means maybe it should actually be moved under/src). Would it be possible to usebuild/demo/assets/datafor the same purpose?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, not really, I just didn't want to accidentally commit unnecessary files.
Not sure if it's appropriate with the
build/demo/assets/data-route. I would say it depends on what the goal/vision is, which I can't say I see clearly.