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;