Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b98222e
Pass 'opacity' and 'visible' parameters to createScene()
mkkellogg Jun 13, 2024
2acac62
Fix bug with scene scale affecting spherical harmonics rotation
mkkellogg Jun 21, 2024
648fe67
2D mode working
mkkellogg Jun 21, 2024
e659c49
Use eigen-vector aligned quad for rendering 2D gaussians
mkkellogg Jun 22, 2024
4d040a7
Revert to reference implementation when quad is too small
mkkellogg Jun 22, 2024
5e34524
Cleanup
mkkellogg Jun 22, 2024
18dd17b
Fix hang that occurs during removal of splat scene when splat scene c…
mkkellogg Jun 24, 2024
e54c00a
Refactor SplatMaterial
mkkellogg Jun 24, 2024
55fcedb
Fix bug with loading spherical harmonics from .ply files containing f…
mkkellogg Jun 26, 2024
3a2005d
Fix raycasting for 2D mode
mkkellogg Jun 28, 2024
5140534
Raycaster bug fix
mkkellogg Jun 29, 2024
64f8e95
Prevent double dispose
mkkellogg Jul 3, 2024
409c13d
Fix spherical harmonics mapping
mkkellogg Jul 8, 2024
fa8bf42
Fix DropInViewer bug & add removeSplatScenes()
mkkellogg Jul 8, 2024
c621377
Better packing of half-precision covariances into data texture
mkkellogg Jul 9, 2024
30e0523
Support updated covariance packing for progressive load
mkkellogg Jul 9, 2024
7ae3b8d
Clear onSplatTreeReadyCallback after calling
mkkellogg Jul 10, 2024
970bab7
Fix scale access bug
mkkellogg Jul 10, 2024
0e8b6bf
Force half-precision covariances on GPU when splat count is too high
mkkellogg Jul 10, 2024
e891002
Update README with note on splat count limits
mkkellogg Jul 10, 2024
8fdca1e
Fix progressive loading bug with 2DGS scenes
mkkellogg Jul 11, 2024
a6a39c8
Merge pull request #264 from mkkellogg/2dgs
mkkellogg Jul 15, 2024
8936fd3
Update version to 0.4.3
mkkellogg Jul 15, 2024
bbb3b86
Merge remote-tracking branch 'origin/main' into dev
mkkellogg Jul 15, 2024
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
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,23 @@ When I started, web-based viewers were already available -- A WebGL-based viewer
- Custom `.ksplat` file format still needs work, especially around compression
- The default, integer based splat sort does not work well for larger scenes. In that case a value of `false` for the `integerBasedSort` viewer parameter can force a slower, floating-point based sort

## Limitations

Currently there are limits on the number of splats that can be rendered, and those limits depend mainly on the degree of spherical harmonics desired. Those limits are:

| Spherical harmonics degree | Max splat count
| --- | ---
| `0` | ~ 16,000,000
| `1` | ~ 11,000,000
| `2` | ~ 8,000,000

Future work will include optimizing how splat data is packed into data textures, which will help increase these limits.

## Future work
This is still very much a work in progress! There are several things that still need to be done:
- Improve the method by which splat data is stored in textures
- Improve the way splat data is packed into data textures
- Continue optimizing CPU-based splat sort - maybe try an incremental sort of some kind?
- Add editing mode, allowing users to modify scene and export changes
- Support very large scenes
- Support very large scenes (streaming sections & LOD)

## Online demo
[https://projects.markkellogg.org/threejs/demo_gaussian_splats_3d.php](https://projects.markkellogg.org/threejs/demo_gaussian_splats_3d.php)
Expand Down Expand Up @@ -318,6 +329,7 @@ Advanced `Viewer` parameters
| `enableOptionalEffects` | When true, allows for usage of extra properties and attributes during rendering for effects such as opacity adjustment. Default is `false` for performance reasons. These properties are separate from transform properties (scale, rotation, position) that are enabled by the `dynamicScene` parameter.
| `plyInMemoryCompressionLevel` | Level to compress `.ply` files when loading them for direct rendering (not exporting to `.ksplat`). Valid values are the same as `.ksplat` compression levels (0, 1, or 2). Default is 2.
| `freeIntermediateSplatData` | When true, the intermediate splat data that is the result of decompressing splat bufffer(s) and used to populate data textures will be freed. This will reduces memory usage, but if that data needs to be modified it will need to be re-populated from the splat buffer(s). Defaults to `false`.
| `splatRenderMode` | Determine which splat rendering mode to enable. Valid values are defined in the `SplatRenderMode` enum: `ThreeD` and `TwoD`. `ThreeD` is the original/traditional mode and `TwoD` is the new mode described here: https://surfsplatting.github.io/
<br>

### Creating KSPLAT files
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"type": "git",
"url": "https://github.com/mkkellogg/GaussianSplats3D"
},
"version": "0.4.2",
"version": "0.4.3",
"description": "Three.js-based 3D Gaussian splat viewer",
"module": "build/gaussian-splats-3d.module.js",
"main": "build/gaussian-splats-3d.umd.cjs",
Expand Down
30 changes: 21 additions & 9 deletions src/DropInViewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,26 @@ export class DropInViewer extends THREE.Group {

this.viewer = new Viewer(options);
this.splatMesh = null;
this.updateSplatMesh();

this.callbackMesh = DropInViewer.createCallbackMesh();
this.add(this.callbackMesh);
this.callbackMesh.onBeforeRender = DropInViewer.onBeforeRender.bind(this, this.viewer);

this.viewer.onSplatMeshChanged(() => {
this.updateSplatMesh();
});

}

updateSplatMesh() {
if (this.splatMesh !== this.viewer.splatMesh) {
if (this.splatMesh) {
this.remove(this.splatMesh);
}
this.splatMesh = this.viewer.splatMesh;
this.add(this.viewer.splatMesh);
}
}

/**
Expand Down Expand Up @@ -85,22 +100,19 @@ export class DropInViewer extends THREE.Group {
return this.viewer.getSplatScene(sceneIndex);
}

removeSplatScene(index) {
return this.viewer.removeSplatScene(index);
removeSplatScene(index, showLoadingUI = true) {
return this.viewer.removeSplatScene(index, showLoadingUI);
}

removeSplatScenes(indexes, showLoadingUI = true) {
return this.viewer.removeSplatScenes(indexes, showLoadingUI);
}

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

Expand Down
4 changes: 4 additions & 0 deletions src/SplatRenderMode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const SplatRenderMode = {
ThreeD: 0,
TwoD: 1
};
89 changes: 67 additions & 22 deletions src/Viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { LoaderStatus } from './loaders/LoaderStatus.js';
import { RenderMode } from './RenderMode.js';
import { LogLevel } from './LogLevel.js';
import { SceneRevealMode } from './SceneRevealMode.js';
import { SplatRenderMode } from './SplatRenderMode.js';

const THREE_CAMERA_FOV = 50;
const MINIMUM_DISTANCE_TO_NEW_FOCAL_POINT = .75;
Expand Down Expand Up @@ -142,7 +143,7 @@ export class Viewer {
this.logLevel = options.logLevel || LogLevel.None;

// Degree of spherical harmonics to utilize in rendering splats (assuming the data is present in the splat scene).
// Valid values are 0 - 3. Default value is 0.
// Valid values are 0 - 2. Default value is 0.
this.sphericalHarmonicsDegree = options.sphericalHarmonicsDegree || 0;

// When true, allows for usage of extra properties and attributes during rendering for effects such as opacity adjustment.
Expand Down Expand Up @@ -180,6 +181,13 @@ export class Viewer {
}
}

// Tell the viewer how to render the splats
if (options.splatRenderMode === undefined || options.splatRenderMode === null) {
options.splatRenderMode = SplatRenderMode.ThreeD;
}
this.splatRenderMode = options.splatRenderMode;

this.onSplatMeshChangedCallback = null;
this.createSplatMesh();

this.controls = null;
Expand Down Expand Up @@ -249,14 +257,17 @@ export class Viewer {
this.initialized = false;
this.disposing = false;
this.disposed = false;
this.disposePromise = null;
if (!this.dropInMode) this.init();
}

createSplatMesh() {
this.splatMesh = new SplatMesh(this.dynamicScene, this.enableOptionalEffects, this.halfPrecisionCovariancesOnGPU,
this.devicePixelRatio, this.gpuAcceleratedSort, this.integerBasedSort, this.antialiased,
this.maxScreenSpaceSplatSize, this.logLevel, this.sphericalHarmonicsDegree);
this.splatMesh = new SplatMesh(this.splatRenderMode, this.dynamicScene, this.enableOptionalEffects,
this.halfPrecisionCovariancesOnGPU, this.devicePixelRatio, this.gpuAcceleratedSort,
this.integerBasedSort, this.antialiased, this.maxScreenSpaceSplatSize, this.logLevel,
this.sphericalHarmonicsDegree);
this.splatMesh.frustumCulled = false;
if (this.onSplatMeshChangedCallback) this.onSplatMeshChangedCallback();
}

init() {
Expand Down Expand Up @@ -366,7 +377,7 @@ export class Viewer {
this.perspectiveControls = new OrbitControls(this.camera, this.renderer.domElement);
}
}
for (let controls of [this.perspectiveControls, this.orthographicControls]) {
for (let controls of [this.orthographicControls, this.perspectiveControls,]) {
if (controls) {
controls.listenToKeyEvents(window);
controls.rotateSpeed = 0.5;
Expand All @@ -375,9 +386,11 @@ export class Viewer {
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.target.copy(this.initialCameraLookAt);
controls.update();
}
}
this.controls = this.camera.isOrthographicCamera ? this.orthographicControls : this.perspectiveControls;
this.controls.update();
}
}

Expand Down Expand Up @@ -411,6 +424,10 @@ export class Viewer {
this.renderMode = renderMode;
}

onSplatMeshChanged(callback) {
this.onSplatMeshChangedCallback = callback;
}

onKeyDown = function() {

const forward = new THREE.Vector3();
Expand Down Expand Up @@ -1021,7 +1038,8 @@ export class Viewer {
return PlyLoader.loadFromURL(path, onProgress, progressiveBuild, onSectionBuilt,
splatAlphaRemovalThreshold, this.plyInMemoryCompressionLevel, this.sphericalHarmonicsDegree);
}
return AbortablePromise.reject(new Error(`Viewer::downloadSplatSceneToSplatBuffer -> File format not supported: ${path}`));

throw new Error(`Viewer::downloadSplatSceneToSplatBuffer -> File format not supported: ${path}`);
}

static isProgressivelyLoadable(format) {
Expand All @@ -1043,6 +1061,13 @@ export class Viewer {
this.splatRenderReady = false;
let splatProcessingTaskId = null;

const removeSplatProcessingTask = () => {
if (splatProcessingTaskId !== null) {
this.loadingSpinner.removeTask(splatProcessingTaskId);
splatProcessingTaskId = null;
}
};

const finish = (buildResults, resolver) => {
if (this.isDisposingOrDisposed()) return;

Expand All @@ -1062,21 +1087,23 @@ export class Viewer {

this.updateSplatSort(true);

if (enableRenderBeforeFirstSort) {
if (!this.sortWorker) {
this.splatRenderReady = true;
removeSplatProcessingTask();
resolver();
} else {
this.runAfterNextSort.push(() => {
if (enableRenderBeforeFirstSort) {
this.splatRenderReady = true;
} else {
this.runAfterNextSort.push(() => {
this.splatRenderReady = true;
});
}
this.runAfterNextSort.push(() => {
removeSplatProcessingTask();
resolver();
});
}

this.runAfterNextSort.push(() => {
if (splatProcessingTaskId !== null) {
this.loadingSpinner.removeTask(splatProcessingTaskId);
splatProcessingTaskId = null;
}
resolver();
});
};

return new Promise((resolve) => {
Expand Down Expand Up @@ -1245,7 +1272,11 @@ export class Viewer {
this.sortRunning = false;
}

removeSplatScene(index, showLoadingUI = true) {
removeSplatScene(indexToRemove, showLoadingUI = true) {
return this.removeSplatScenes([indexToRemove], showLoadingUI);
}

removeSplatScenes(indexesToRemove, showLoadingUI = true) {
if (this.isLoadingOrUnloading()) {
throw new Error('Cannot remove splat scene while another load or unload is already in progress.');
}
Expand Down Expand Up @@ -1294,7 +1325,14 @@ export class Viewer {
const savedSceneOptions = [];
const savedSceneTransformComponents = [];
for (let i = 0; i < this.splatMesh.scenes.length; i++) {
if (i !== index) {
let shouldRemove = false;
for (let indexToRemove of indexesToRemove) {
if (indexToRemove === i) {
shouldRemove = true;
break;
}
}
if (!shouldRemove) {
const scene = this.splatMesh.scenes[i];
savedSplatBuffers.push(scene.splatBuffer);
savedSceneOptions.push(this.splatMesh.sceneOptions[i]);
Expand All @@ -1307,6 +1345,7 @@ export class Viewer {
}
this.disposeSortWorker();
this.splatMesh.dispose();
this.sceneRevealMode = SceneRevealMode.Instant;
this.createSplatMesh();
this.addSplatBuffers(savedSplatBuffers, savedSceneOptions, true, false, true)
.then(() => {
Expand Down Expand Up @@ -1373,7 +1412,8 @@ export class Viewer {
* Dispose of all resources held directly and indirectly by this viewer.
*/
async dispose() {
this.disposing = true;
if (this.isDisposingOrDisposed()) return this.disposePromise;

let waitPromises = [];
let promisesToAbort = [];
for (let promiseKey in this.splatSceneDownloadPromises) {
Expand All @@ -1386,7 +1426,9 @@ export class Viewer {
if (this.sortPromise) {
waitPromises.push(this.sortPromise);
}
const disposePromise = Promise.all(waitPromises).finally(() => {

this.disposing = true;
this.disposePromise = Promise.all(waitPromises).finally(() => {
this.stop();
if (this.controls) {
this.controls.dispose();
Expand Down Expand Up @@ -1439,7 +1481,7 @@ export class Viewer {
promisesToAbort.forEach((toAbort) => {
toAbort.abort('Scene disposed');
});
return disposePromise;
return this.disposePromise;
}

selfDrivenUpdate() {
Expand Down Expand Up @@ -1756,7 +1798,10 @@ export class Viewer {

return async function(force = false) {
if (this.sortRunning) return;
if (this.splatMesh.getSplatCount() <= 0) return;
if (this.splatMesh.getSplatCount() <= 0) {
this.splatRenderCount = 0;
return;
}

let angleDiff = 0;
let positionDiff = 0;
Expand Down
4 changes: 3 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { WebXRMode } from './webxr/WebXRMode.js';
import { RenderMode } from './RenderMode.js';
import { LogLevel } from './LogLevel.js';
import { SceneRevealMode } from './SceneRevealMode.js';
import { SplatRenderMode } from './SplatRenderMode.js';

export {
PlyParser,
Expand All @@ -37,5 +38,6 @@ export {
WebXRMode,
RenderMode,
LogLevel,
SceneRevealMode
SceneRevealMode,
SplatRenderMode
};
Loading