diff --git a/README.md b/README.md
index d9ec2980..d3540c5e 100644
--- a/README.md
+++ b/README.md
@@ -59,12 +59,20 @@ Keyboard
- Ratio of rendered splats to total splats
- Last splat sort duration
-- `P` Toggles a debug object that shows the orientation of the camera controls. It includes a green arrow representing the camera's orbital axis and a white square representing the plane at which the camera's elevation angle is 0.
+- `U` Toggles a debug object that shows the orientation of the camera controls. It includes a green arrow representing the camera's orbital axis and a white square representing the plane at which the camera's elevation angle is 0.
- `Left arrow` Rotate the camera's up vector counter-clockwise
- `Right arrow` Rotate the camera's up vector clockwise
+- `P` Toggle point-cloud mode, where each splat is rendered as a filled circle
+
+- `=` Increase splat scale
+
+- `-` Decrease splat scale
+
+- `O` Toggle orthographic mode
+
## Building from source and running locally
@@ -262,8 +270,8 @@ const viewer = new GaussianSplats3D.Viewer({
'webXRMode': GaussianSplats3D.WebXRMode.None,
'renderMode': GaussianSplats3D.RenderMode.OnChange,
'sceneRevealMode': GaussianSplats3D.SceneRevealMode.Instant,
- `antialiased`: false,
- `focalAdjustment`: 1.0
+ 'antialiased': false,
+ 'focalAdjustment': 1.0
});
viewer.addSplatScene('')
.then(() => {
diff --git a/package-lock.json b/package-lock.json
index bf7e41a8..66e754f2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@mkkellogg/gaussian-splats-3d",
- "version": "0.3.6",
+ "version": "0.3.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@mkkellogg/gaussian-splats-3d",
- "version": "0.3.6",
+ "version": "0.3.7",
"license": "MIT",
"devDependencies": {
"@babel/core": "7.22.0",
diff --git a/package.json b/package.json
index 14d1f4de..b4f78848 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"type": "git",
"url": "https://github.com/mkkellogg/GaussianSplats3D"
},
- "version": "0.3.6",
+ "version": "0.3.7",
"description": "Three.js-based 3D Gaussian splat viewer",
"module": "build/gaussian-splats-3d.module.js",
"main": "build/gaussian-splats-3d.umd.cjs",
diff --git a/src/OrbitControls.js b/src/OrbitControls.js
index de214ce3..31ed235f 100644
--- a/src/OrbitControls.js
+++ b/src/OrbitControls.js
@@ -437,6 +437,11 @@ class OrbitControls extends EventDispatcher {
};
+ this.clearDampedRotation = function() {
+ sphericalDelta.theta = 0.0;
+ sphericalDelta.phi = 0.0;
+ };
+
//
// internals
//
diff --git a/src/SceneHelper.js b/src/SceneHelper.js
index c9a4f0cf..c2c483f9 100644
--- a/src/SceneHelper.js
+++ b/src/SceneHelper.js
@@ -165,13 +165,17 @@ export class SceneHelper {
const tempPosition = new THREE.Vector3();
const tempMatrix = new THREE.Matrix4();
+ const toCamera = new THREE.Vector3();
return function(position, camera, viewport) {
tempMatrix.copy(camera.matrixWorld).invert();
tempPosition.copy(position).applyMatrix4(tempMatrix);
tempPosition.normalize().multiplyScalar(10);
tempPosition.applyMatrix4(camera.matrixWorld);
- this.focusMarker.position.copy(tempPosition);
+ toCamera.copy(camera.position).sub(position);
+ const toCameraDistance = toCamera.length();
+ this.focusMarker.position.copy(position);
+ this.focusMarker.scale.set(toCameraDistance, toCameraDistance, toCameraDistance);
this.focusMarker.material.uniforms.realFocusPosition.value.copy(position);
this.focusMarker.material.uniforms.viewport.value.copy(viewport);
this.focusMarker.material.uniformsNeedUpdate = true;
diff --git a/src/SplatMesh.js b/src/SplatMesh.js
index 0b863659..2abccc61 100644
--- a/src/SplatMesh.js
+++ b/src/SplatMesh.js
@@ -55,6 +55,7 @@ export class SplatMesh extends THREE.Mesh {
this.scenes = [];
// Special octree tailored to SplatMesh instances
this.splatTree = null;
+ this.baseSplatTree = null;
// Textures in which splat data will be stored for rendering
this.splatDataTextures = {};
this.distancesTransformFeedback = {
@@ -90,7 +91,11 @@ export class SplatMesh extends THREE.Mesh {
this.visibleRegionFadeStartRadius = 0;
this.visibleRegionChanging = false;
+ this.splatScale = 1.0;
+ this.pointCloudModeEnabled = false;
+
this.disposed = false;
+ this.lastRenderer = null;
}
/**
@@ -100,9 +105,12 @@ export class SplatMesh extends THREE.Mesh {
* @param {boolean} antialiased If true, calculate compensation factor to deal with gaussians being rendered at a significantly
* different resolution than that of their training
* @param {number} maxScreenSpaceSplatSize The maximum clip space splat size
+ * @param {number} splatScale Value by which all splats are scaled in screen-space (default is 1.0)
+ * @param {number} pointCloudModeEnabled Render all splats as screen-space circles
* @return {THREE.ShaderMaterial}
*/
- static buildMaterial(dynamicMode = false, antialiased = false, maxScreenSpaceSplatSize = 2048) {
+ static buildMaterial(dynamicMode = false, antialiased = false,
+ maxScreenSpaceSplatSize = 2048, splatScale = 1.0, pointCloudModeEnabled = false) {
// Contains the code to project 3D covariance to 2D and from there calculate the quad (using the eigen vectors of the
// 2D covariance) that is ultimately rasterized
@@ -125,6 +133,9 @@ export class SplatMesh extends THREE.Mesh {
vertexShaderSource += `
uniform vec2 focal;
+ uniform float orthoZoom;
+ uniform int orthographicMode;
+ uniform int pointCloudModeEnabled;
uniform float inverseFocalAdjustment;
uniform vec2 viewport;
uniform vec2 basisViewport;
@@ -136,6 +147,7 @@ export class SplatMesh extends THREE.Mesh {
uniform float currentTime;
uniform int fadeInComplete;
uniform vec3 sceneCenter;
+ uniform float splatScale;
varying vec4 vColor;
varying vec2 vUv;
@@ -211,12 +223,19 @@ export class SplatMesh extends THREE.Mesh {
// require a non-linear component (perspective division) which would yield a non-gaussian result. (This assumes
// the current projection is a perspective projection).
- float s = 1.0 / (viewCenter.z * viewCenter.z);
- mat3 J = mat3(
- focal.x / viewCenter.z, 0., -(focal.x * viewCenter.x) * s,
- 0., focal.y / viewCenter.z, -(focal.y * viewCenter.y) * s,
- 0., 0., 0.
- );
+ mat3 J;
+ if (orthographicMode == 1) {
+ J = transpose(mat3(orthoZoom, 0.0, 0.0,
+ 0.0, orthoZoom, 0.0,
+ 0.0, 0.0, 0.0));
+ } else {
+ float s = 1.0 / (viewCenter.z * viewCenter.z);
+ J = mat3(
+ focal.x / viewCenter.z, 0., -(focal.x * viewCenter.x) * s,
+ 0., focal.y / viewCenter.z, -(focal.y * viewCenter.y) * s,
+ 0., 0., 0.
+ );
+ }
// Concatenate the projection approximation with the model-view transformation
mat3 W = transpose(mat3(transformModelViewMatrix));
@@ -279,6 +298,10 @@ export class SplatMesh extends THREE.Mesh {
float eigenValue1 = traceOver2 + term2;
float eigenValue2 = traceOver2 - term2;
+ if (pointCloudModeEnabled == 1) {
+ eigenValue1 = eigenValue2 = 0.2;
+ }
+
if (eigenValue2 <= 0.0) return;
vec2 eigenVector1 = normalize(vec2(b, eigenValue1 - a));
@@ -286,8 +309,8 @@ export class SplatMesh extends THREE.Mesh {
vec2 eigenVector2 = vec2(eigenVector1.y, -eigenVector1.x);
// We use sqrt(8) standard deviations instead of 3 to eliminate more of the splat with a very low opacity.
- vec2 basisVector1 = eigenVector1 * min(sqrt8 * sqrt(eigenValue1), ${parseInt(maxScreenSpaceSplatSize)}.0);
- vec2 basisVector2 = eigenVector2 * min(sqrt8 * sqrt(eigenValue2), ${parseInt(maxScreenSpaceSplatSize)}.0);
+ vec2 basisVector1 = eigenVector1 * splatScale * min(sqrt8 * sqrt(eigenValue1), ${parseInt(maxScreenSpaceSplatSize)}.0);
+ vec2 basisVector2 = eigenVector2 * splatScale * min(sqrt8 * sqrt(eigenValue2), ${parseInt(maxScreenSpaceSplatSize)}.0);
if (fadeInComplete == 0) {
float opacityAdjust = 1.0;
@@ -305,7 +328,9 @@ export class SplatMesh extends THREE.Mesh {
vec2 ndcOffset = vec2(vPosition.x * basisVector1 + vPosition.y * basisVector2) *
basisViewport * 2.0 * inverseFocalAdjustment;
- gl_Position = vec4(ndcCenter.xy + ndcOffset, ndcCenter.z, 1.0);
+
+ vec4 quadPos = vec4(ndcCenter.xy + ndcOffset, ndcCenter.z, 1.0);
+ gl_Position = quadPos;
// Scale the position data we send to the fragment shader
vPosition *= sqrt8;
@@ -349,6 +374,10 @@ export class SplatMesh extends THREE.Mesh {
'type': 'i',
'value': 0
},
+ 'orthographicMode': {
+ 'type': 'i',
+ 'value': 0
+ },
'visibleRegionFadeStartRadius': {
'type': 'f',
'value': 0.0
@@ -377,6 +406,10 @@ export class SplatMesh extends THREE.Mesh {
'type': 'v2',
'value': new THREE.Vector2()
},
+ 'orthoZoom': {
+ 'type': 'f',
+ 'value': 1.0
+ },
'inverseFocalAdjustment': {
'type': 'f',
'value': 1.0
@@ -400,6 +433,14 @@ export class SplatMesh extends THREE.Mesh {
'centersColorsTextureSize': {
'type': 'v2',
'value': new THREE.Vector2(1024, 1024)
+ },
+ 'splatScale': {
+ 'type': 'f',
+ 'value': splatScale
+ },
+ 'pointCloudModeEnabled': {
+ 'type': 'i',
+ 'value': pointCloudModeEnabled ? 1 : 0
}
};
@@ -533,7 +574,6 @@ export class SplatMesh extends THREE.Mesh {
/**
* Build an instance of SplatTree (a specialized octree) for the given splat mesh.
- * @param {SplatMesh} splatMesh SplatMesh instance for which the splat tree will be built
* @param {Array} minAlphas Array of minimum splat slphas for each scene
* @param {function} onSplatTreeIndexesUpload Function to be called when the upload of splat centers to the splat tree
* builder worker starts and finishes.
@@ -541,42 +581,51 @@ export class SplatMesh extends THREE.Mesh {
* the format produced by the splat tree builder worker starts and ends.
* @return {SplatTree}
*/
- static buildSplatTree = function(splatMesh, minAlphas = [], onSplatTreeIndexesUpload, onSplatTreeConstruction) {
+ buildSplatTree = function(minAlphas = [], onSplatTreeIndexesUpload, onSplatTreeConstruction) {
return new Promise((resolve) => {
+ this.disposeSplatTree();
// TODO: expose SplatTree constructor parameters (maximumDepth and maxCentersPerNode) so that they can
// be configured on a per-scene basis
- const splatTree = new SplatTree(8, 1000);
- console.time('SplatTree build');
+ this.baseSplatTree = new SplatTree(8, 1000);
+ const buildStartTime = performance.now();
const splatColor = new THREE.Vector4();
- splatTree.processSplatMesh(splatMesh, (splatIndex) => {
- splatMesh.getSplatColor(splatIndex, splatColor);
- const sceneIndex = splatMesh.getSceneIndexForSplat(splatIndex);
+ this.baseSplatTree.processSplatMesh(this, (splatIndex) => {
+ this.getSplatColor(splatIndex, splatColor);
+ const sceneIndex = this.getSceneIndexForSplat(splatIndex);
const minAlpha = minAlphas[sceneIndex] || 1;
return splatColor.w >= minAlpha;
}, onSplatTreeIndexesUpload, onSplatTreeConstruction)
.then(() => {
- console.timeEnd('SplatTree build');
-
- let leavesWithVertices = 0;
- let avgSplatCount = 0;
- let maxSplatCount = 0;
- let nodeCount = 0;
-
- splatTree.visitLeaves((node) => {
- const nodeSplatCount = node.data.indexes.length;
- if (nodeSplatCount > 0) {
- avgSplatCount += nodeSplatCount;
- maxSplatCount = Math.max(maxSplatCount, nodeSplatCount);
- nodeCount++;
- leavesWithVertices++;
- }
- });
- console.log(`SplatTree leaves: ${splatTree.countLeaves()}`);
- console.log(`SplatTree leaves with splats:${leavesWithVertices}`);
- avgSplatCount = avgSplatCount / nodeCount;
- console.log(`Avg splat count per node: ${avgSplatCount}`);
- console.log(`Total splat count: ${splatMesh.getSplatCount()}`);
- resolve(splatTree);
+ const buildTime = performance.now() - buildStartTime;
+ console.log('SplatTree build: ' + buildTime + ' ms');
+ if (this.disposed) {
+ resolve();
+ } else {
+
+ this.splatTree = this.baseSplatTree;
+ this.baseSplatTree = null;
+
+ let leavesWithVertices = 0;
+ let avgSplatCount = 0;
+ let maxSplatCount = 0;
+ let nodeCount = 0;
+
+ this.splatTree.visitLeaves((node) => {
+ const nodeSplatCount = node.data.indexes.length;
+ if (nodeSplatCount > 0) {
+ avgSplatCount += nodeSplatCount;
+ maxSplatCount = Math.max(maxSplatCount, nodeSplatCount);
+ nodeCount++;
+ leavesWithVertices++;
+ }
+ });
+ console.log(`SplatTree leaves: ${this.splatTree.countLeaves()}`);
+ console.log(`SplatTree leaves with splats:${leavesWithVertices}`);
+ avgSplatCount = avgSplatCount / nodeCount;
+ console.log(`Avg splat count per node: ${avgSplatCount}`);
+ console.log(`Total splat count: ${this.getSplatCount()}`);
+ resolve();
+ }
});
});
};
@@ -603,10 +652,12 @@ export class SplatMesh extends THREE.Mesh {
* builder worker starts and finishes.
* @param {function} onSplatTreeConstruction Function to be called when the conversion of the local splat tree from
* the format produced by the splat tree builder worker starts and ends.
+ * @return {object} Object containing info about the splats that are updated
*/
build(splatBuffers, sceneOptions, keepSceneTransforms = true, finalBuild = false,
onSplatTreeIndexesUpload, onSplatTreeConstruction) {
+ this.sceneOptions = sceneOptions;
this.finalBuild = finalBuild;
const maxSplatCount = SplatMesh.getTotalMaxSplatCountForSplatBuffers(splatBuffers);
@@ -628,6 +679,7 @@ export class SplatMesh extends THREE.Mesh {
this.scenes[0].splatBuffer !== this.lastBuildScenes[0].splatBuffer) {
isUpdateBuild = false;
}
+
if (!isUpdateBuild) {
isUpdateBuild = false;
this.boundingBox = new THREE.Box3();
@@ -641,7 +693,8 @@ export class SplatMesh extends THREE.Mesh {
this.lastBuildMaxSplatCount = 0;
this.disposeMeshData();
this.geometry = SplatMesh.buildGeomtery(maxSplatCount);
- this.material = SplatMesh.buildMaterial(this.dynamicMode, this.antialiased, this.maxScreenSpaceSplatSize);
+ this.material = SplatMesh.buildMaterial(this.dynamicMode, this.antialiased,
+ this.maxScreenSpaceSplatSize, this.splatScale, this.pointCloudModeEnabled);
const indexMaps = SplatMesh.buildSplatIndexMaps(splatBuffers);
this.globalSplatIndexToLocalSplatIndexMap = indexMaps.localSplatIndexMap;
this.globalSplatIndexToSceneIndexMap = indexMaps.sceneIndexMap;
@@ -653,19 +706,32 @@ export class SplatMesh extends THREE.Mesh {
for (let i = 0; i < this.scenes.length; i++) {
this.lastBuildScenes[i] = this.scenes[i];
}
+
+ const buildResults = {
+ 'from': this.lastBuildSplatCount,
+ 'to': this.getSplatCount() - 1,
+ 'count': this.getSplatCount() - this.lastBuildSplatCount
+ };
+ if (!this.enableDistancesComputationOnGPU) {
+ buildResults.centers = this.integerBasedDistancesComputation ?
+ this.getIntegerCenters(true, isUpdateBuild) :
+ this.getFloatCenters(true, isUpdateBuild);
+ buildResults.transformIndexes = this.getTransformIndexes(isUpdateBuild);
+ }
+
this.lastBuildSplatCount = this.getSplatCount();
this.lastBuildMaxSplatCount = this.getMaxSplatCount();
this.lastBuildSceneCount = this.scenes.length;
if (finalBuild) {
- this.disposeSplatTree();
- SplatMesh.buildSplatTree(this, sceneOptions.map(options => options.splatAlphaRemovalThreshold || 1),
- onSplatTreeIndexesUpload, onSplatTreeConstruction)
- .then((splatTree) => {
- this.splatTree = splatTree;
+ this.buildSplatTree(sceneOptions.map(options => options.splatAlphaRemovalThreshold || 1),
+ onSplatTreeIndexesUpload, onSplatTreeConstruction)
+ .then(() => {
if (this.onSplatTreeReadyCallback) this.onSplatTreeReadyCallback(this.splatTree);
});
}
+
+ return buildResults;
}
/**
@@ -676,9 +742,53 @@ export class SplatMesh extends THREE.Mesh {
this.disposeTextures();
this.disposeSplatTree();
if (this.enableDistancesComputationOnGPU) {
+ if (this.computeDistancesOnGPUSyncTimeout) {
+ clearTimeout(this.computeDistancesOnGPUSyncTimeout);
+ this.computeDistancesOnGPUSyncTimeout = null;
+ }
this.disposeDistancesComputationGPUResources();
}
+ this.scenes = [];
+ this.distancesTransformFeedback = {
+ 'id': null,
+ 'vertexShader': null,
+ 'fragmentShader': null,
+ 'program': null,
+ 'centersBuffer': null,
+ 'transformIndexesBuffer': null,
+ 'outDistancesBuffer': null,
+ 'centersLoc': -1,
+ 'modelViewProjLoc': -1,
+ 'transformIndexesLoc': -1,
+ 'transformsLocs': []
+ };
+ this.renderer = null;
+
+ this.globalSplatIndexToLocalSplatIndexMap = [];
+ this.globalSplatIndexToSceneIndexMap = [];
+
+ this.lastBuildSplatCount = 0;
+ this.lastBuildScenes = [];
+ this.lastBuildMaxSplatCount = 0;
+ this.lastBuildSceneCount = 0;
+ this.firstRenderTime = -1;
+ this.finalBuild = false;
+
+ this.webGLUtils = null;
+
+ this.boundingBox = new THREE.Box3();
+ this.calculatedSceneCenter = new THREE.Vector3();
+ this.maxSplatDistanceFromSceneCenter = 0;
+ this.visibleRegionBufferRadius = 0;
+ this.visibleRegionRadius = 0;
+ this.visibleRegionFadeStartRadius = 0;
+ this.visibleRegionChanging = false;
+
+ this.splatScale = 1.0;
+ this.pointCloudModeEnabled = false;
+
this.disposed = true;
+ this.lastRenderer = null;
}
/**
@@ -709,7 +819,10 @@ export class SplatMesh extends THREE.Mesh {
}
disposeSplatTree() {
+ if (this.splatTree) this.splatTree.dispose();
this.splatTree = null;
+ if (this.baseSplatTree) this.baseSplatTree.dispose();
+ this.baseSplatTree = null;
}
getSplatTree() {
@@ -727,7 +840,7 @@ export class SplatMesh extends THREE.Mesh {
this.uploadSplatDataToTextures(isUpdateBuild);
if (this.enableDistancesComputationOnGPU) {
this.updateGPUCentersBufferForDistancesComputation(isUpdateBuild);
- this.updateGPUTransformIndexesBufferForDistancesComputation();
+ this.updateGPUTransformIndexesBufferForDistancesComputation(isUpdateBuild);
}
}
@@ -1006,7 +1119,8 @@ export class SplatMesh extends THREE.Mesh {
const viewport = new THREE.Vector2();
- return function(renderDimensions, cameraFocalLengthX, cameraFocalLengthY, inverseFocalAdjustment) {
+ return function(renderDimensions, cameraFocalLengthX, cameraFocalLengthY,
+ orthographicMode, orthographicZoom, inverseFocalAdjustment) {
const splatCount = this.getSplatCount();
if (splatCount > 0) {
viewport.set(renderDimensions.x * this.devicePixelRatio,
@@ -1014,6 +1128,8 @@ export class SplatMesh extends THREE.Mesh {
this.material.uniforms.viewport.value.copy(viewport);
this.material.uniforms.basisViewport.value.set(1.0 / viewport.x, 1.0 / viewport.y);
this.material.uniforms.focal.value.set(cameraFocalLengthX, cameraFocalLengthY);
+ this.material.uniforms.orthographicMode.value = orthographicMode ? 1 : 0;
+ this.material.uniforms.orthoZoom.value = orthographicZoom;
this.material.uniforms.inverseFocalAdjustment.value = inverseFocalAdjustment;
if (this.dynamicMode) {
for (let i = 0; i < this.scenes.length; i++) {
@@ -1026,6 +1142,26 @@ export class SplatMesh extends THREE.Mesh {
}();
+ setSplatScale(splatScale = 1) {
+ this.splatScale = splatScale;
+ this.material.uniforms.splatScale.value = splatScale;
+ this.material.uniformsNeedUpdate = true;
+ }
+
+ getSplatScale() {
+ return this.splatScale;
+ }
+
+ setPointCloudModeEnabled(enabled) {
+ this.pointCloudModeEnabled = enabled;
+ this.material.uniforms.pointCloudModeEnabled.value = enabled ? 1 : 0;
+ this.material.uniformsNeedUpdate = true;
+ }
+
+ getPointCloudModeEnabled() {
+ return this.pointCloudModeEnabled;
+ }
+
getSplatDataTextures() {
return this.splatDataTextures;
}
@@ -1129,7 +1265,6 @@ export class SplatMesh extends THREE.Mesh {
setupDistancesComputationTransformFeedback = function() {
- let currentRenderer;
let currentMaxSplatCount;
return function() {
@@ -1137,7 +1272,7 @@ export class SplatMesh extends THREE.Mesh {
if (!this.renderer) return;
- const rebuildGPUObjects = (currentRenderer !== this.renderer);
+ const rebuildGPUObjects = (this.lastRenderer !== this.renderer);
const rebuildBuffers = currentMaxSplatCount !== maxSplatCount;
if (!rebuildGPUObjects && !rebuildBuffers) return;
@@ -1230,6 +1365,7 @@ export class SplatMesh extends THREE.Mesh {
const currentVao = gl.getParameter(gl.VERTEX_ARRAY_BINDING);
const currentProgram = gl.getParameter(gl.CURRENT_PROGRAM);
+ const currentProgramDeleted = currentProgram ? gl.getProgramParameter(currentProgram, gl.DELETE_STATUS) : false;
if (rebuildGPUObjects) {
this.distancesTransformFeedback.vao = gl.createVertexArray();
@@ -1310,10 +1446,10 @@ export class SplatMesh extends THREE.Mesh {
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, this.distancesTransformFeedback.id);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, this.distancesTransformFeedback.outDistancesBuffer);
- if (currentProgram) gl.useProgram(currentProgram);
+ if (currentProgram && currentProgramDeleted !== true) gl.useProgram(currentProgram);
if (currentVao) gl.bindVertexArray(currentVao);
- currentRenderer = this.renderer;
+ this.lastRenderer = this.renderer;
currentMaxSplatCount = maxSplatCount;
};
@@ -1346,7 +1482,7 @@ export class SplatMesh extends THREE.Mesh {
if (isUpdateBuild) {
gl.bufferSubData(gl.ARRAY_BUFFER, subBufferOffset, srcCenters);
} else {
- const maxArray = new ArrayType(this.getMaxSplatCount() * 16);
+ const maxArray = new ArrayType(this.getMaxSplatCount() * attributeBytesPerCenter);
maxArray.set(srcCenters);
gl.bufferData(gl.ARRAY_BUFFER, maxArray, gl.STATIC_DRAW);
}
@@ -1358,8 +1494,9 @@ export class SplatMesh extends THREE.Mesh {
/**
* Refresh GPU buffers used for pre-computing splat distances with centers data from the scenes for this mesh.
+ * @param {boolean} isUpdateBuild Specify whether or not to only update for splats that have been added since the last build.
*/
- updateGPUTransformIndexesBufferForDistancesComputation() {
+ updateGPUTransformIndexesBufferForDistancesComputation(isUpdateBuild) {
if (!this.renderer || !this.dynamicMode) return;
@@ -1368,8 +1505,18 @@ export class SplatMesh extends THREE.Mesh {
const currentVao = gl.getParameter(gl.VERTEX_ARRAY_BINDING);
gl.bindVertexArray(this.distancesTransformFeedback.vao);
+ const subBufferOffset = isUpdateBuild ? this.lastBuildSplatCount * 4 : 0;
+ const transformIndexes = this.getTransformIndexes(isUpdateBuild);
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.distancesTransformFeedback.transformIndexesBuffer);
- gl.bufferData(gl.ARRAY_BUFFER, this.getTransformIndexes(), gl.STATIC_DRAW);
+
+ if (isUpdateBuild) {
+ gl.bufferSubData(gl.ARRAY_BUFFER, subBufferOffset, transformIndexes);
+ } else {
+ const maxArray = new Uint32Array(this.getMaxSplatCount() * 4);
+ maxArray.set(transformIndexes);
+ gl.bufferData(gl.ARRAY_BUFFER, maxArray, gl.STATIC_DRAW);
+ }
gl.bindBuffer(gl.ARRAY_BUFFER, null);
if (currentVao) gl.bindVertexArray(currentVao);
@@ -1377,11 +1524,24 @@ export class SplatMesh extends THREE.Mesh {
/**
* Get a typed array containing a mapping from global splat indexes to their scene index.
+ * @param {boolean} isUpdateBuild Specify whether or not to only update for splats that have been added since the last build.
* @return {Uint32Array}
*/
- getTransformIndexes() {
- const transformIndexes = new Uint32Array(this.globalSplatIndexToSceneIndexMap.length);
- transformIndexes.set(this.globalSplatIndexToSceneIndexMap);
+ getTransformIndexes(isUpdateBuild) {
+
+ let transformIndexes;
+ if (isUpdateBuild) {
+ const splatCount = this.getSplatCount();
+ const fillCount = splatCount - this.lastBuildSplatCount;
+ transformIndexes = new Uint32Array(fillCount);
+ for (let i = this.lastBuildSplatCount; i < splatCount; i++) {
+ transformIndexes[i] = this.globalSplatIndexToSceneIndexMap[i];
+ }
+ } else {
+ transformIndexes = new Uint32Array(this.globalSplatIndexToSceneIndexMap.length);
+ transformIndexes.set(this.globalSplatIndexToSceneIndexMap);
+ }
+
return transformIndexes;
}
@@ -1419,6 +1579,7 @@ export class SplatMesh extends THREE.Mesh {
const currentVao = gl.getParameter(gl.VERTEX_ARRAY_BINDING);
const currentProgram = gl.getParameter(gl.CURRENT_PROGRAM);
+ const currentProgramDeleted = currentProgram ? gl.getProgramParameter(currentProgram, gl.DELETE_STATUS) : false;
gl.bindVertexArray(this.distancesTransformFeedback.vao);
gl.useProgram(this.distancesTransformFeedback.program);
@@ -1481,33 +1642,39 @@ export class SplatMesh extends THREE.Mesh {
const promise = new Promise((resolve) => {
const checkSync = () => {
- const timeout = 0;
- const bitflags = 0;
- const status = gl.clientWaitSync(sync, bitflags, timeout);
- switch (status) {
- case gl.TIMEOUT_EXPIRED:
- return setTimeout(checkSync);
- case gl.WAIT_FAILED:
- throw new Error('should never get here');
- default:
- gl.deleteSync(sync);
- const currentVao = gl.getParameter(gl.VERTEX_ARRAY_BINDING);
- gl.bindVertexArray(this.distancesTransformFeedback.vao);
- gl.bindBuffer(gl.ARRAY_BUFFER, this.distancesTransformFeedback.outDistancesBuffer);
- gl.getBufferSubData(gl.ARRAY_BUFFER, 0, outComputedDistances);
- gl.bindBuffer(gl.ARRAY_BUFFER, null);
-
- if (currentVao) gl.bindVertexArray(currentVao);
-
- // console.timeEnd("gpu_compute_distances");
-
- resolve();
+ if (this.disposed) {
+ resolve();
+ } else {
+ const timeout = 0;
+ const bitflags = 0;
+ const status = gl.clientWaitSync(sync, bitflags, timeout);
+ switch (status) {
+ case gl.TIMEOUT_EXPIRED:
+ this.computeDistancesOnGPUSyncTimeout = setTimeout(checkSync);
+ return this.computeDistancesOnGPUSyncTimeout;
+ case gl.WAIT_FAILED:
+ throw new Error('should never get here');
+ default:
+ this.computeDistancesOnGPUSyncTimeout = null;
+ gl.deleteSync(sync);
+ const currentVao = gl.getParameter(gl.VERTEX_ARRAY_BINDING);
+ gl.bindVertexArray(this.distancesTransformFeedback.vao);
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.distancesTransformFeedback.outDistancesBuffer);
+ gl.getBufferSubData(gl.ARRAY_BUFFER, 0, outComputedDistances);
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
+
+ if (currentVao) gl.bindVertexArray(currentVao);
+
+ // console.timeEnd("gpu_compute_distances");
+
+ resolve();
+ }
}
};
- setTimeout(checkSync);
+ this.computeDistancesOnGPUSyncTimeout = setTimeout(checkSync);
});
- if (currentProgram) gl.useProgram(currentProgram);
+ if (currentProgram && currentProgramDeleted !== true) gl.useProgram(currentProgram);
if (currentVao) gl.bindVertexArray(currentVao);
return promise;
@@ -1604,7 +1771,6 @@ export class SplatMesh extends THREE.Mesh {
return intCenters;
}
-
/**
* Returns an array of splat centers, transformed as appropriate, optionally padded.
* @param {number} padFour Enforce alignement of 4 by inserting a 1 after every 3 values
diff --git a/src/Util.js b/src/Util.js
index 7ec9f184..917170f4 100644
--- a/src/Util.js
+++ b/src/Util.js
@@ -138,10 +138,10 @@ export const disposeAllMeshes = (object3D) => {
}
};
-export const delayedExecute = (func) => {
+export const delayedExecute = (func, fast) => {
return new Promise((resolve) => {
window.setTimeout(() => {
resolve(func());
- }, 1);
+ }, fast ? 1 : 50);
});
};
diff --git a/src/Viewer.js b/src/Viewer.js
index c6e252be..8871b497 100644
--- a/src/Viewer.js
+++ b/src/Viewer.js
@@ -83,11 +83,7 @@ export class Viewer {
// If 'gpuAcceleratedSort' is true, a partially GPU-accelerated approach to sorting splats will be used.
// Currently this means pre-computing splat distances from the camera on the GPU
- this.gpuAcceleratedSort = options.gpuAcceleratedSort;
- if (this.gpuAcceleratedSort !== true && this.gpuAcceleratedSort !== false) {
- if (this.isMobile()) this.gpuAcceleratedSort = false;
- else this.gpuAcceleratedSort = true;
- }
+ this.gpuAcceleratedSort = options.gpuAcceleratedSort || false;
// if 'integerBasedSort' is true, the integer version of splat centers as well as other values used to calculate
// splat distances are used instead of the float version. This speeds up computation, but introduces the possibility of
@@ -109,14 +105,14 @@ export class Viewer {
// scene may change. This prevents optimizations that depend on a static scene from being made. Additionally, if 'dynamicScene' is
// true it tells the splat mesh to not apply scene tranforms to splat data that is returned by functions like
// SplatMesh.getSplatCenter() by default.
- const dynamicScene = !!options.dynamicScene;
+ this.dynamicScene = !!options.dynamicScene;
// When true, will perform additional steps during rendering to address artifacts caused by the rendering of gaussians at a
// substantially different resolution than that at which they were rendered during training. This will only work correctly
// for models that were trained using a process that utilizes this compensation calculation. For more details:
// https://github.com/nerfstudio-project/gsplat/pull/117
// https://github.com/graphdeco-inria/gaussian-splatting/issues/294#issuecomment-1772688093
- const antialiased = options.antialiased || false;
+ this.antialiased = options.antialiased || false;
this.webXRMode = options.webXRMode || WebXRMode.None;
@@ -141,10 +137,14 @@ export class Viewer {
// Specify the maximum screen-space splat size, can help deal with large splats that get too unwieldy
this.maxScreenSpaceSplatSize = options.maxScreenSpaceSplatSize || 2048;
- this.splatMesh = new SplatMesh(dynamicScene, this.halfPrecisionCovariancesOnGPU, this.devicePixelRatio,
- this.gpuAcceleratedSort, this.integerBasedSort, antialiased, this.maxScreenSpaceSplatSize);
+ this.createSplatMesh();
this.controls = null;
+ this.perspectiveControls = null;
+ this.orthographicControls = null;
+
+ this.orthographicCamera = null;
+ this.perspectiveCamera = null;
this.showMeshCursor = false;
this.showControlPlane = false;
@@ -162,12 +162,14 @@ export class Viewer {
this.runAfterFirstSort = [];
this.selfDrivenModeRunning = false;
- this.splatRenderingInitialized = false;
+ this.splatRenderReady = false;
this.raycaster = new Raycaster();
this.infoPanel = null;
+ this.startInOrthographicMode = false;
+
this.currentFPS = 0;
this.lastSortTime = 0;
this.consecutiveRenderFrames = 0;
@@ -206,6 +208,12 @@ export class Viewer {
if (!this.dropInMode) this.init();
}
+ createSplatMesh() {
+ this.splatMesh = new SplatMesh(this.dynamicScene, this.halfPrecisionCovariancesOnGPU, this.devicePixelRatio,
+ this.gpuAcceleratedSort, this.integerBasedSort, this.antialiased, this.maxScreenSpaceSplatSize);
+ this.splatMesh.frustumCulled = false;
+ }
+
init() {
if (this.initialized) return;
@@ -226,7 +234,10 @@ export class Viewer {
this.getRenderDimensions(renderDimensions);
if (!this.usingExternalCamera) {
- this.camera = new THREE.PerspectiveCamera(THREE_CAMERA_FOV, renderDimensions.x / renderDimensions.y, 0.1, 500);
+ this.perspectiveCamera = new THREE.PerspectiveCamera(THREE_CAMERA_FOV, renderDimensions.x / renderDimensions.y, 0.1, 1000);
+ this.orthographicCamera = new THREE.OrthographicCamera(renderDimensions.x / -2, renderDimensions.x / 2,
+ renderDimensions.y / 2, renderDimensions.y / -2, 0.1, 1000 );
+ this.camera = this.startInOrthographicMode ? this.orthographicCamera : this.perspectiveCamera;
this.camera.position.copy(this.initialCameraPosition);
this.camera.up.copy(this.cameraUp).normalize();
this.camera.lookAt(this.initialCameraLookAt);
@@ -270,14 +281,28 @@ export class Viewer {
this.sceneHelper.setupControlPlane();
if (this.useBuiltInControls && this.webXRMode === WebXRMode.None) {
- this.controls = new OrbitControls(this.camera, this.renderer.domElement);
- this.controls.listenToKeyEvents(window);
- this.controls.rotateSpeed = 0.5;
- this.controls.maxPolarAngle = Math.PI * .75;
- this.controls.minPolarAngle = 0.1;
- this.controls.enableDamping = true;
- this.controls.dampingFactor = 0.05;
- this.controls.target.copy(this.initialCameraLookAt);
+ if (!this.usingExternalCamera) {
+ this.perspectiveControls = new OrbitControls(this.perspectiveCamera, this.renderer.domElement);
+ this.orthographicControls = new OrbitControls(this.orthographicCamera, this.renderer.domElement);
+ } else {
+ if (this.camera.isOrthographicCamera) {
+ this.orthographicControls = new OrbitControls(this.camera, this.renderer.domElement);
+ } else {
+ this.perspectiveControls = new OrbitControls(this.camera, this.renderer.domElement);
+ }
+ }
+ for (let controls of [this.perspectiveControls, this.orthographicControls]) {
+ if (controls) {
+ controls.listenToKeyEvents(window);
+ controls.rotateSpeed = 0.5;
+ controls.maxPolarAngle = Math.PI * .75;
+ controls.minPolarAngle = 0.1;
+ controls.enableDamping = true;
+ controls.dampingFactor = 0.05;
+ controls.target.copy(this.initialCameraLookAt);
+ }
+ }
+ this.controls = this.camera.isOrthographicCamera ? this.orthographicControls : this.perspectiveControls;
this.mouseMoveListener = this.onMouseMove.bind(this);
this.renderer.domElement.addEventListener('pointermove', this.mouseMoveListener, false);
this.mouseDownListener = this.onMouseDown.bind(this);
@@ -341,7 +366,7 @@ export class Viewer {
case 'KeyC':
this.showMeshCursor = !this.showMeshCursor;
break;
- case 'KeyP':
+ case 'KeyU':
this.showControlPlane = !this.showControlPlane;
break;
case 'KeyI':
@@ -352,6 +377,26 @@ export class Viewer {
this.infoPanel.hide();
}
break;
+ case 'KeyO':
+ if (!this.usingExternalCamera) {
+ this.setOrthographicMode(!this.camera.isOrthographicCamera);
+ }
+ break;
+ case 'KeyP':
+ if (!this.usingExternalCamera) {
+ this.splatMesh.setPointCloudModeEnabled(!this.splatMesh.getPointCloudModeEnabled());
+ }
+ break;
+ case 'Equal':
+ if (!this.usingExternalCamera) {
+ this.splatMesh.setSplatScale(this.splatMesh.getSplatScale() + 0.05);
+ }
+ break;
+ case 'Minus':
+ if (!this.usingExternalCamera) {
+ this.splatMesh.setSplatScale(Math.max(this.splatMesh.getSplatScale() - 0.05, 0.0));
+ }
+ break;
}
};
@@ -423,6 +468,57 @@ export class Viewer {
}
}
+ setOrthographicMode(orthographicMode) {
+ if (orthographicMode === this.camera.isOrthographicCamera) return;
+ const fromCamera = this.camera;
+ const toCamera = orthographicMode ? this.orthographicCamera : this.perspectiveCamera;
+ toCamera.position.copy(fromCamera.position);
+ toCamera.up.copy(fromCamera.up);
+ toCamera.rotation.copy(fromCamera.rotation);
+ toCamera.quaternion.copy(fromCamera.quaternion);
+ toCamera.matrix.copy(fromCamera.matrix);
+ this.camera = toCamera;
+
+ if (this.controls) {
+ const fromControls = this.controls;
+ const toControls = orthographicMode ? this.orthographicControls : this.perspectiveControls;
+ toControls.target.copy(fromControls.target);
+ toControls.clearDampedRotation();
+ fromControls.clearDampedRotation();
+ if (orthographicMode) {
+ Viewer.setCameraZoomFromPosition(toCamera, fromCamera, fromControls);
+ } else {
+ Viewer.setCameraPositionFromZoom(toCamera, fromCamera, toControls);
+ }
+ this.controls = toControls;
+ this.camera.lookAt(this.controls.target);
+ }
+ }
+
+ static setCameraPositionFromZoom = function() {
+
+ const tempVector = new THREE.Vector3();
+
+ return function(positionCamera, zoomedCamera, controls) {
+ const toLookAtDistance = 1 / (zoomedCamera.zoom * 0.001);
+ tempVector.copy(controls.target).sub(positionCamera.position).normalize().multiplyScalar(toLookAtDistance).negate();
+ positionCamera.position.copy(controls.target).add(tempVector);
+ };
+
+ }();
+
+
+ static setCameraZoomFromPosition = function() {
+
+ const tempVector = new THREE.Vector3();
+
+ return function(zoomCamera, positionZamera, controls) {
+ const toLookAtDistance = tempVector.copy(controls.target).sub(positionZamera.position).length();
+ zoomCamera.zoom = 1 / (toLookAtDistance * .001);
+ };
+
+ }();
+
updateSplatMesh = function() {
const renderDimensions = new THREE.Vector2();
@@ -438,11 +534,12 @@ export class Viewer {
const focalLengthY = this.camera.projectionMatrix.elements[5] * 0.5 *
this.devicePixelRatio * renderDimensions.y;
- const focalAdjustment = this.focalAdjustment;
+ const focalMultiplier = this.camera.isOrthographicCamera ? (1.0 / this.devicePixelRatio) : 1.0;
+ const focalAdjustment = this.focalAdjustment * focalMultiplier;
const inverseFocalAdjustment = 1.0 / focalAdjustment;
- this.splatMesh.updateUniforms(renderDimensions, focalLengthX * focalAdjustment,
- focalLengthY * focalAdjustment, inverseFocalAdjustment);
+ this.splatMesh.updateUniforms(renderDimensions, focalLengthX * focalAdjustment, focalLengthY * focalAdjustment,
+ this.camera.isOrthographicCamera, this.camera.zoom || 1.0, inverseFocalAdjustment);
}
};
@@ -832,40 +929,38 @@ export class Viewer {
*/
addSplatBuffers = function() {
- let loadCount = 0;
- let splatProcessingTaskId = null;
-
return function(splatBuffers, splatBufferOptions = [], finalBuild = true,
showLoadingUI = true, showLoadingSpinnerForSplatTreeBuild = true) {
if (this.isDisposingOrDisposed()) return Promise.resolve();
- this.splatRenderingInitialized = false;
- loadCount++;
+ this.splatRenderReady = false;
+ let splatProcessingTaskId = null;
- const finish = (resolver) => {
+ const finish = (buildResults, resolver) => {
if (this.isDisposingOrDisposed()) return;
- loadCount--;
- if (loadCount === 0) {
- if (splatProcessingTaskId !== null) {
- this.loadingSpinner.removeTask(splatProcessingTaskId);
- splatProcessingTaskId = null;
- }
- this.splatRenderingInitialized = true;
+ if (splatProcessingTaskId !== null) {
+ this.loadingSpinner.removeTask(splatProcessingTaskId);
+ splatProcessingTaskId = null;
}
// If we aren't calculating the splat distances from the center on the GPU, the sorting worker needs splat centers and
// transform indexes so that it can calculate those distance values.
if (!this.gpuAcceleratedSort) {
- const centers = this.integerBasedSort ? this.splatMesh.getIntegerCenters(true) : this.splatMesh.getFloatCenters(true);
- const transformIndexes = this.splatMesh.getTransformIndexes();
this.sortWorker.postMessage({
- 'centers': centers.buffer,
- 'transformIndexes': transformIndexes.buffer
+ 'centers': buildResults.centers.buffer,
+ 'transformIndexes': buildResults.transformIndexes.buffer,
+ 'range': {
+ 'from': buildResults.from,
+ 'to': buildResults.to,
+ 'count': buildResults.count
+ }
});
}
- this.forceSort = true;
+
+ this.splatRenderReady = true;
+ this.sortNeededForSceneChange = true;
resolver();
};
@@ -878,20 +973,21 @@ export class Viewer {
if (this.isDisposingOrDisposed()) {
resolve();
} else {
- this.addSplatBuffersToMesh(splatBuffers, splatBufferOptions, finalBuild, showLoadingSpinnerForSplatTreeBuild);
+ const buildResults = this.addSplatBuffersToMesh(splatBuffers, splatBufferOptions,
+ finalBuild, showLoadingSpinnerForSplatTreeBuild);
const maxSplatCount = this.splatMesh.getMaxSplatCount();
if (this.sortWorker && this.sortWorker.maxSplatCount !== maxSplatCount) {
this.disposeSortWorker();
}
if (!this.sortWorker) {
this.setupSortWorker(this.splatMesh).then(() => {
- finish(resolve);
+ finish(buildResults, resolve);
});
} else {
- finish(resolve);
+ finish(buildResults, resolve);
}
}
- });
+ }, true);
});
};
@@ -900,12 +996,6 @@ export class Viewer {
}();
- disposeSortWorker() {
- if (this.sortWorker) this.sortWorker.terminate();
- this.sortWorker = null;
- this.sortRunning = false;
- }
-
/**
* Add one or more instances of SplatBuffer to the SplatMesh instance managed by the viewer. This function is additive; all splat
* buffers contained by the viewer's splat mesh before calling this function will be preserved.
@@ -924,6 +1014,7 @@ export class Viewer {
* @param {boolean} finalBuild Will the splat mesh be in its final state after this build?
* @param {boolean} showLoadingSpinnerForSplatTreeBuild Whether or not to show the loading spinner during
* construction of the splat tree.
+ * @return {object} Object containing info about the splats that are updated
*/
addSplatBuffersToMesh(splatBuffers, splatBufferOptions, finalBuild = true, showLoadingSpinnerForSplatTreeBuild = false) {
if (this.isDisposingOrDisposed()) return;
@@ -943,15 +1034,13 @@ export class Viewer {
}
}
};
- const onSplatTreeConstructed = (finished) => {
+ const onSplatTreeReady = (finished) => {
if (this.isDisposingOrDisposed()) return;
if (finished && splatOptimizingTaskId) {
this.loadingSpinner.removeTask(splatOptimizingTaskId);
}
};
- this.splatMesh.build(allSplatBuffers, allSplatBufferOptions, true, finalBuild,
- onSplatTreeIndexesUpload, onSplatTreeConstructed);
- this.splatMesh.frustumCulled = false;
+ return this.splatMesh.build(allSplatBuffers, allSplatBufferOptions, true, finalBuild, onSplatTreeIndexesUpload, onSplatTreeReady);
}
/**
@@ -979,7 +1068,6 @@ export class Viewer {
}
this.lastSortTime = e.data.sortTime;
this.sortPromiseResolver();
- this.sortPromise = null;
this.sortPromiseResolver = null;
this.forceRenderNextFrame();
if (sortCount === 0) {
@@ -1010,19 +1098,111 @@ export class Viewer {
}
for (let i = 0; i < splatCount; i++) this.sortWorkerIndexesToSort[i] = i;
this.sortWorker.maxSplatCount = maxSplatCount;
- resolve();
- } else if (e.data.sortSetupComplete) {
+
console.log('Sorting web worker ready.');
const splatDataTextures = this.splatMesh.getSplatDataTextures();
const covariancesTextureSize = splatDataTextures.covariances.size;
const centersColorsTextureSize = splatDataTextures.centerColors.size;
console.log('Covariances texture size: ' + covariancesTextureSize.x + ' x ' + covariancesTextureSize.y);
console.log('Centers/colors texture size: ' + centersColorsTextureSize.x + ' x ' + centersColorsTextureSize.y);
+
+ resolve();
}
};
});
}
+ disposeSortWorker() {
+ if (this.sortWorker) this.sortWorker.terminate();
+ this.sortWorker = null;
+ this.sortRunning = false;
+ }
+
+ removeSplatScene(index, showLoadingUI = true) {
+ if (this.isDisposingOrDisposed()) return Promise.resolve();
+ return new Promise((resolve, reject) => {
+ let revmovalTaskId;
+
+ if (showLoadingUI) {
+ this.loadingSpinner.show();
+ revmovalTaskId = this.loadingSpinner.addTask('Removing splat scene...');
+ }
+
+ const checkAndHideLoadingUI = () => {
+ if (showLoadingUI) {
+ this.loadingSpinner.hide();
+ this.loadingSpinner.removeTask(revmovalTaskId);
+ }
+ };
+
+ const onDone = () => {
+ checkAndHideLoadingUI();
+ resolve();
+ };
+
+ const checkForEarlyExit = () => {
+ if (this.isDisposingOrDisposed()) {
+ onDone();
+ return true;
+ }
+ return false;
+ };
+
+ delayedExecute(() => {
+ this.sortPromise.then(() => {
+ if (checkForEarlyExit()) return;
+ const savedSplatBuffers = [];
+ const savedSceneOptions = [];
+ const savedSceneTransformComponents = [];
+ const savedVisibleRegionFadeStartRadius = this.splatMesh.visibleRegionFadeStartRadius;
+ for (let i = 0; i < this.splatMesh.scenes.length; i++) {
+ if (i !== index) {
+ const scene = this.splatMesh.scenes[i];
+ savedSplatBuffers.push(scene.splatBuffer);
+ savedSceneOptions.push(this.splatMesh.sceneOptions[i]);
+ savedSceneTransformComponents.push({
+ 'position': scene.position.clone(),
+ 'quaternion': scene.quaternion.clone(),
+ 'scale': scene.scale.clone()
+ });
+ }
+ }
+ this.splatMesh.dispose();
+ this.createSplatMesh();
+
+ this.addSplatBuffers(savedSplatBuffers, savedSceneOptions, true, false, true)
+ .then(() => {
+ if (checkForEarlyExit()) return;
+ checkAndHideLoadingUI();
+ this.splatMesh.visibleRegionFadeStartRadius = savedVisibleRegionFadeStartRadius;
+ this.splatMesh.scenes.forEach((scene, index) => {
+ scene.position.copy(savedSceneTransformComponents[index].position);
+ scene.quaternion.copy(savedSceneTransformComponents[index].quaternion);
+ scene.scale.copy(savedSceneTransformComponents[index].scale);
+ });
+ this.splatMesh.updateTransforms();
+
+ this.splatRenderReady = false;
+ this.updateSplatSort(true)
+ .then(() => {
+ if (checkForEarlyExit()) {
+ this.splatRenderReady = true;
+ return;
+ }
+ this.sortPromise.then(() => {
+ this.splatRenderReady = true;
+ onDone();
+ });
+ });
+ })
+ .catch((e) => {
+ reject(e);
+ });
+ });
+ });
+ });
+ }
+
/**
* Start self-driven mode
*/
@@ -1097,7 +1277,7 @@ export class Viewer {
this.camera = null;
this.threeScene = null;
- this.splatRenderingInitialized = false;
+ this.splatRenderReady = false;
this.initialized = false;
if (this.renderer) {
if (!this.usingExternalRenderer) {
@@ -1181,7 +1361,7 @@ export class Viewer {
render = function() {
return function() {
- if (!this.initialized || !this.splatRenderingInitialized) return;
+ if (!this.initialized || !this.splatRenderReady) return;
const hasRenderables = (threeScene) => {
for (let child of threeScene.children) {
@@ -1189,10 +1369,14 @@ export class Viewer {
}
return false;
};
+
const savedAuoClear = this.renderer.autoClear;
- this.renderer.autoClear = false;
- if (hasRenderables(this.threeScene)) this.renderer.render(this.threeScene, this.camera);
+ if (hasRenderables(this.threeScene)) {
+ this.renderer.render(this.threeScene, this.camera);
+ this.renderer.autoClear = false;
+ }
this.renderer.render(this.splatMesh, this.camera);
+ this.renderer.autoClear = false;
if (this.sceneHelper.getFocusMarkerOpacity() > 0.0) this.renderer.render(this.sceneHelper.focusMarker, this.camera);
if (this.showControlPlane) this.renderer.render(this.sceneHelper.controlPlane, this.camera);
this.renderer.autoClear = savedAuoClear;
@@ -1202,8 +1386,13 @@ export class Viewer {
update(renderer, camera) {
if (this.dropInMode) this.updateForDropInMode(renderer, camera);
- if (!this.initialized || !this.splatRenderingInitialized) return;
- if (this.controls) this.controls.update();
+ if (!this.initialized || !this.splatRenderReady) return;
+ if (this.controls) {
+ this.controls.update();
+ if (this.camera.isOrthographicCamera && !this.usingExternalCamera) {
+ Viewer.setCameraPositionFromZoom(this.camera, this.camera, this.controls);
+ }
+ }
this.splatMesh.updateVisibleRegionFadeDistance(this.sceneRevealMode);
this.updateSplatSort();
this.updateForRendererSizeChanges();
@@ -1250,15 +1439,25 @@ export class Viewer {
const lastRendererSize = new THREE.Vector2();
const currentRendererSize = new THREE.Vector2();
+ let lastCameraOrthographic;
return function() {
- this.renderer.getSize(currentRendererSize);
- if (currentRendererSize.x !== lastRendererSize.x || currentRendererSize.y !== lastRendererSize.y) {
- if (!this.usingExternalCamera) {
- this.camera.aspect = currentRendererSize.x / currentRendererSize.y;
+ if (!this.usingExternalCamera) {
+ this.renderer.getSize(currentRendererSize);
+ if (lastCameraOrthographic === undefined || lastCameraOrthographic !== this.camera.isOrthographicCamera ||
+ currentRendererSize.x !== lastRendererSize.x || currentRendererSize.y !== lastRendererSize.y) {
+ if (this.camera.isOrthographicCamera) {
+ this.camera.left = -currentRendererSize.x / 2.0;
+ this.camera.right = currentRendererSize.x / 2.0;
+ this.camera.top = currentRendererSize.y / 2.0;
+ this.camera.bottom = -currentRendererSize.y / 2.0;
+ } else {
+ this.camera.aspect = currentRendererSize.x / currentRendererSize.y;
+ }
this.camera.updateProjectionMatrix();
+ lastRendererSize.copy(currentRendererSize);
+ lastCameraOrthographic = this.camera.isOrthographicCamera;
}
- lastRendererSize.copy(currentRendererSize);
}
};
@@ -1375,8 +1574,10 @@ export class Viewer {
const meshCursorPosition = this.showMeshCursor ? this.sceneHelper.meshCursor.position : null;
const splatRenderCountPct = this.splatRenderCount / splatCount * 100;
this.infoPanel.update(renderDimensions, this.camera.position, cameraLookAtPosition,
- this.camera.up, meshCursorPosition, this.currentFPS || 'N/A', splatCount,
- this.splatRenderCount, splatRenderCountPct, this.lastSortTime, this.focalAdjustment);
+ this.camera.up, this.camera.isOrthographicCamera, meshCursorPosition,
+ this.currentFPS || 'N/A', splatCount, this.splatRenderCount, splatRenderCountPct,
+ this.lastSortTime, this.focalAdjustment, this.splatMesh.getSplatScale(),
+ this.splatMesh.getPointCloudModeEnabled());
};
}();
@@ -1415,9 +1616,8 @@ export class Viewer {
}
];
- return async function() {
+ return async function(force = false) {
if (this.sortRunning) return;
- if (!this.initialized || !this.splatRenderingInitialized) return;
let angleDiff = 0;
let positionDiff = 0;
@@ -1428,10 +1628,12 @@ export class Viewer {
angleDiff = sortViewDir.dot(lastSortViewDir);
positionDiff = sortViewOffset.copy(this.camera.position).sub(lastSortViewPos).length();
- if (!this.forceSort && !this.splatMesh.dynamicMode && queuedSorts.length === 0) {
- if (angleDiff <= 0.99) needsRefreshForRotation = true;
- if (positionDiff >= 1.0) needsRefreshForPosition = true;
- if (!needsRefreshForRotation && !needsRefreshForPosition) return;
+ if (!force) {
+ if (!this.sortNeededForSceneChange && !this.splatMesh.dynamicMode && queuedSorts.length === 0) {
+ if (angleDiff <= 0.99) needsRefreshForRotation = true;
+ if (positionDiff >= 1.0) needsRefreshForPosition = true;
+ if (!needsRefreshForRotation && !needsRefreshForPosition) return;
+ }
}
this.sortRunning = true;
@@ -1496,7 +1698,7 @@ export class Viewer {
lastSortViewDir.copy(sortViewDir);
}
- this.forceSort = false;
+ this.sortNeededForSceneChange = false;
};
}();
diff --git a/src/raycaster/Raycaster.js b/src/raycaster/Raycaster.js
index 08120888..c7d22273 100644
--- a/src/raycaster/Raycaster.js
+++ b/src/raycaster/Raycaster.js
@@ -21,7 +21,7 @@ export class Raycaster {
this.ray.direction.set(ndcCoords.x, ndcCoords.y, 0.5 ).unproject(camera).sub(this.ray.origin).normalize();
this.camera = camera;
} else if (camera.isOrthographicCamera) {
- this.ray.origin.set(screenPosition.x, screenPosition.y,
+ this.ray.origin.set(ndcCoords.x, ndcCoords.y,
(camera.near + camera.far) / (camera.near - camera.far)).unproject(camera);
this.ray.direction.set(0, 0, -1).transformDirection(camera.matrixWorld);
this.camera = camera;
diff --git a/src/splattree/SplatTree.js b/src/splattree/SplatTree.js
index a4151339..c008656a 100644
--- a/src/splattree/SplatTree.js
+++ b/src/splattree/SplatTree.js
@@ -109,6 +109,7 @@ function createSplatTreeWorker(self) {
this.addedIndexes = {};
this.nodesWithIndexes = [];
this.splatMesh = null;
+ this.disposed = false;
}
}
@@ -311,15 +312,26 @@ export class SplatTree {
this.splatMesh = null;
}
+
+ dispose() {
+ this.diposeSplatTreeWorker();
+ this.disposed = true;
+ }
+
+ diposeSplatTreeWorker() {
+ if (splatTreeWorker) splatTreeWorker.terminate();
+ splatTreeWorker = null;
+ };
+
/**
* Construct this instance of SplatTree from an instance of SplatMesh.
*
* @param {SplatMesh} splatMesh The instance of SplatMesh from which to construct this splat tree.
* @param {function} filterFunc Optional function to filter out unwanted splats.
* @param {function} onIndexesUpload Function to be called when the upload of splat centers to the splat tree
- * builder worker starts and finishes.
+ * builder worker starts and finishes.
* @param {function} onSplatTreeConstruction Function to be called when the conversion of the local splat tree from
- * the format produced by the splat tree builder worker starts and ends.
+ * the format produced by the splat tree builder worker starts and ends.
* @return {undefined}
*/
processSplatMesh = function(splatMesh, filterFunc = () => true, onIndexesUpload, onSplatTreeConstruction) {
@@ -347,27 +359,22 @@ export class SplatTree {
return sceneCenters;
};
- const diposeSplatTreeWorker = () => {
- splatTreeWorker.terminate();
- splatTreeWorker = null;
- };
-
- const checkForEarlyExit = (resolve) => {
- if (splatMesh.disposed) {
- diposeSplatTreeWorker();
- resolve();
- return true;
- }
- return false;
- };
-
return new Promise((resolve) => {
+ const checkForEarlyExit = () => {
+ if (this.disposed) {
+ this.diposeSplatTreeWorker();
+ resolve();
+ return true;
+ }
+ return false;
+ };
+
if (onIndexesUpload) onIndexesUpload(false);
delayedExecute(() => {
- if (checkForEarlyExit(resolve)) return;
+ if (checkForEarlyExit()) return;
const allCenters = [];
if (splatMesh.dynamicMode) {
@@ -386,7 +393,7 @@ export class SplatTree {
splatTreeWorker.onmessage = (e) => {
- if (checkForEarlyExit(resolve)) return;
+ if (checkForEarlyExit()) return;
if (e.data.subTrees) {
@@ -394,13 +401,13 @@ export class SplatTree {
delayedExecute(() => {
- if (checkForEarlyExit(resolve)) return;
+ if (checkForEarlyExit()) return;
for (let workerSubTree of e.data.subTrees) {
const convertedSubTree = SplatSubTree.convertWorkerSubTree(workerSubTree, splatMesh);
this.subTrees.push(convertedSubTree);
}
- diposeSplatTreeWorker();
+ this.diposeSplatTreeWorker();
if (onSplatTreeConstruction) onSplatTreeConstruction(true);
@@ -413,7 +420,7 @@ export class SplatTree {
};
delayedExecute(() => {
- if (checkForEarlyExit(resolve)) return;
+ if (checkForEarlyExit()) return;
if (onIndexesUpload) onIndexesUpload(true);
const transferBuffers = allCenters.map((array) => array.buffer);
workerProcessCenters(allCenters, transferBuffers, this.maxDepth, this.maxCentersPerNode);
diff --git a/src/ui/InfoPanel.js b/src/ui/InfoPanel.js
index d995389d..4c367c54 100644
--- a/src/ui/InfoPanel.js
+++ b/src/ui/InfoPanel.js
@@ -10,12 +10,15 @@ export class InfoPanel {
['Camera position', 'cameraPosition'],
['Camera look-at', 'cameraLookAt'],
['Camera up', 'cameraUp'],
+ ['Camera mode', 'orthographicCamera'],
['Cursor position', 'cursorPosition'],
['FPS', 'fps'],
['Rendering:', 'renderSplatCount'],
['Sort time', 'sortTime'],
['Render window', 'renderWindow'],
- ['Focal adjustment', 'focalAdjustment']
+ ['Focal adjustment', 'focalAdjustment'],
+ ['Splat scale', 'splatScale'],
+ ['Point cloud mode', 'pointCloudMode']
];
this.infoPanelContainer = document.createElement('div');
@@ -98,8 +101,9 @@ export class InfoPanel {
this.visible = false;
}
- update = function(renderDimensions, cameraPosition, cameraLookAtPosition, cameraUp,
- meshCursorPosition, currentFPS, splatCount, splatRenderCount, splatRenderCountPct, lastSortTime, focalAdjustment) {
+ update = function(renderDimensions, cameraPosition, cameraLookAtPosition, cameraUp, orthographicCamera,
+ meshCursorPosition, currentFPS, splatCount, splatRenderCount,
+ splatRenderCountPct, lastSortTime, focalAdjustment, splatScale, pointCloudMode) {
const cameraPosString = `${cameraPosition.x.toFixed(5)}, ${cameraPosition.y.toFixed(5)}, ${cameraPosition.z.toFixed(5)}`;
if (this.infoCells.cameraPosition.innerHTML !== cameraPosString) {
@@ -119,6 +123,8 @@ export class InfoPanel {
this.infoCells.cameraUp.innerHTML = cameraUpString;
}
+ this.infoCells.orthographicCamera.innerHTML = orthographicCamera ? 'Orthographic' : 'Perspective';
+
if (meshCursorPosition) {
const cursPos = meshCursorPosition;
const cursorPosString = `${cursPos.x.toFixed(5)}, ${cursPos.y.toFixed(5)}, ${cursPos.z.toFixed(5)}`;
@@ -134,8 +140,9 @@ export class InfoPanel {
`${splatRenderCount} splats out of ${splatCount} (${splatRenderCountPct.toFixed(2)}%)`;
this.infoCells.sortTime.innerHTML = `${lastSortTime.toFixed(3)} ms`;
-
this.infoCells.focalAdjustment.innerHTML = `${focalAdjustment.toFixed(3)}`;
+ this.infoCells.splatScale.innerHTML = `${splatScale.toFixed(3)}`;
+ this.infoCells.pointCloudMode.innerHTML = `${pointCloudMode}`;
};
setContainer(container) {
diff --git a/src/worker/SortWorker.js b/src/worker/SortWorker.js
index 011c8b44..1e030153 100644
--- a/src/worker/SortWorker.js
+++ b/src/worker/SortWorker.js
@@ -80,15 +80,18 @@ function sortWorker(self) {
centers = e.data.centers;
transformIndexes = e.data.transformIndexes;
if (integerBasedSort) {
- new Int32Array(wasmMemory, centersOffset, splatCount * 4).set(new Int32Array(centers));
+ new Int32Array(wasmMemory, centersOffset + e.data.range.from * Constants.BytesPerInt * 4,
+ e.data.range.count * 4).set(new Int32Array(centers));
} else {
- new Float32Array(wasmMemory, centersOffset, splatCount * 4).set(new Float32Array(centers));
+ new Float32Array(wasmMemory, centersOffset + e.data.range.from * Constants.BytesPerFloat * 4,
+ e.data.range.count * 4).set(new Float32Array(centers));
}
if (dynamicMode) {
- new Uint32Array(wasmMemory, transformIndexesOffset, splatCount).set(new Uint32Array(transformIndexes));
+ new Uint32Array(wasmMemory, transformIndexesOffset + e.data.range.from * 4,
+ e.data.range.count).set(new Uint32Array(transformIndexes));
}
self.postMessage({
- 'sortSetupComplete': true,
+ 'centerDataSet': true,
});
} else if (e.data.sort) {
const renderCount = e.data.sort.splatRenderCount || 0;