Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
dist
build
node_modules
.vscode/**/*
demo/assets/data/**/*
Copy link
Owner

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 demo directory 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 use build/demo/assets/data for the same purpose?

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.

20 changes: 16 additions & 4 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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.");
}
Expand Down Expand Up @@ -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',
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should make this an enum.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure!

};
const sceneOptions = {
'halfPrecisionCovariancesOnGPU': true,
Expand All @@ -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;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this variable used anywhere?

Choose a reason for hiding this comment

The 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();
Expand Down Expand Up @@ -574,6 +584,8 @@
<br>
<span class="button" onclick="window.viewSplat()">View</span>
&nbsp;&nbsp;
<span class="button" onclick="window.viewSplat({editMode: true})">Edit</span>
&nbsp;&nbsp;
<span class="button" onclick="reset()">Reset</span>
<br>
<br>
Expand Down
127 changes: 127 additions & 0 deletions src/Editor.js
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 =
Copy link
Owner

Choose a reason for hiding this comment

The 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 updateCameraUpLabel() function (or something like that).

Choose a reason for hiding this comment

The 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);
Copy link
Owner

Choose a reason for hiding this comment

The 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 Viewer. I'm still not sure we should simply have Editor inherit from Viewer and just override the onMouseDown() etc. functions, or if we should add functions to Viewer to allow them to be set by an external source.

Choose a reason for hiding this comment

The 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.

Copy link
Owner

Choose a reason for hiding this comment

The 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 :) )

Choose a reason for hiding this comment

The 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?

Copy link
Owner

Choose a reason for hiding this comment

The 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 Viewer by overriding them with custom functionality and making sure to call the base function along with it. We have more options that way because we can override any function and it's cleaner because it doesn't require changes to Viewer. I do agree that it would be nice for Viewer to have a general way to hook into events so that anyone developing with the library could hook into them. Maybe we need to make Viewer extend an EventDispatcher class and emit events at the appropriate places.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 on making Viewer extend EventDispatcher

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;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to change Viewer.onMouseUp() to only do these calculations to determine if there was a click, and define a new function Viewer.onClick() and call that from Viewer.onMouseUp(). Then we could just override Viewer.onClick() and not have to replicate the click detection logic.

Choose a reason for hiding this comment

The 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();
}
}
}
};
}();
}
51 changes: 40 additions & 11 deletions src/Viewer.js
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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we call this flag viewNeedsRefresh ? Just so it's clear that just the view/screen needs to be updated.

Choose a reason for hiding this comment

The 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();
}
Expand All @@ -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);
}

Expand Down Expand Up @@ -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;
Copy link
Owner

Choose a reason for hiding this comment

The 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 Viewer.

Choose a reason for hiding this comment

The 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.

Choose a reason for hiding this comment

The 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.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that this should be set from within the Editor class. Maybe a parameter to the Viewer class doesn't quite make sense. Also, we should be careful about creating too many parameters for Viewer

this.controls.zoomToCursor = true;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also curious why this setting was changed.

Choose a reason for hiding this comment

The 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.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we make a toggle in the Editor UI for this?

Choose a reason for hiding this comment

The 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);
}

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -225,9 +241,16 @@ export class Viewer {
}
}
};

}();

onControlsChange() {
this.invalidate();
}

invalidate() {
this.viewerNeedsUpdate = true;
}

getRenderDimensions(outDimensions) {
if (this.rootElement) {
outDimensions.x = this.rootElement.offsetWidth;
Expand All @@ -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';
Expand Down Expand Up @@ -558,6 +582,7 @@ export class Viewer {
this.updateSplatMeshUniforms();
}
lastRendererSize.copy(currentRendererSize);
this.invalidate();
}
};

Expand All @@ -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() {
Expand Down
8 changes: 5 additions & 3 deletions src/index.js
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,
};