diff --git a/README.md b/README.md index 9c3fc31..a99f7d7 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,10 @@ When I started, web-based viewers were already available -- A WebGL-based viewer - Built-in viewer is self-contained so very little code is necessary to load and view a scene - Allows user to import `.ply` files for conversion to custom compressed `.splat` file format - Allows a Three.js scene or object group to be rendered along with the splats +- Focus on optimization: + - Splats culled prior to sorting & rendering using a custom octree + - WASM splat sort: Implemented in C++ using WASM SIMD instructions + - Partially GPU accelerated splat sort: Uses transform feedback to pre-calculate splat distances ## Known issues @@ -86,6 +90,7 @@ The demo scene data is available here: [https://projects.markkellogg.org/downloa

+ ## Basic Usage To run the built-in viewer: @@ -95,102 +100,97 @@ const viewer = new GaussianSplats3D.Viewer({ 'cameraUp': [0, -1, -0.6], 'initialCameraPosition': [-1, -4, 6], 'initialCameraLookAt': [0, 4, 0], - 'ignoreDevicePixelRatio': false, - 'gpuAcceleratedSort': true + 'halfPrecisionCovariancesOnGPU': true, }); viewer.loadFile('', { - 'splatAlphaRemovalThreshold': 5, // out of 255 - 'halfPrecisionCovariancesOnGPU': true + 'splatAlphaRemovalThreshold': 5, + 'showLoadingSpinner': true, + 'position': [0, 1, 0], + 'rotation': [0, 0, 0, 1], + 'scale': [1.5, 1.5, 1.5] }) .then(() => { viewer.start(); }); + ``` +Viewer parameters +
+ | Parameter | Purpose | --- | --- -| `cameraUp` | The natural 'up' vector for viewing the scene. Determines the scene's orientation relative to the camera and serves as the axis around which the camera will orbit. -| `initialCameraPosition` | The camera's initial position. -| `initialCameraLookAt` | The initial focal point of the camera and center of the camera's orbit. -| `ignoreDevicePixelRatio` | Tells the viewer to pretend the device pixel ratio is 1, which can boost performance on devices where it is larger, at a small cost to visual quality. Defaults to `false`. -| `gpuAcceleratedSort` | Tells the viewer to use a partially GPU-accelerated approach to sorting splats. Currently this means pre-computing splat distances is done on the GPU. Defaults to `true`. -| `splatAlphaRemovalThreshold` | Tells `loadFile()` to ignore any splats with an alpha less than the specified value. Defaults to `1`. +| `cameraUp` | The natural 'up' vector for viewing the scene (only has an effect when used with orbit controls and when the viewer uses its own camera). Serves as the axis around which the camera will orbit, and is used to determine the scene's orientation relative to the camera. +| `initialCameraPosition` | The camera's initial position (only used when the viewer uses its own camera). +| `initialCameraLookAt` | The initial focal point of the camera and center of the camera's orbit (only used when the viewer uses its own camera). | `halfPrecisionCovariancesOnGPU` | Tells the viewer to use 16-bit floating point values for each element of a splat's 3D covariance matrix, instead of 32-bit. Defaults to `true`. -
- -As an alternative to using `cameraUp` to adjust to the scene's natural orientation, you can pass an orientation (and/or position) to the `loadFile()` method to transform the entire scene: -```javascript -const viewer = new GaussianSplats3D.Viewer({ - 'initialCameraPosition': [-1, -4, 6], - 'initialCameraLookAt': [0, 4, 0] -}); -const orientation = new THREE.Quaternion(); -orientation.setFromUnitVectors(new THREE.Vector3(0, -1, -0.6).normalize(), new THREE.Vector3(0, 1, 0)); -viewer.loadFile('', { - 'splatAlphaRemovalThreshold': 5, // out of 255 - 'halfPrecisionCovariancesOnGPU': true, - 'position': [0, 0, 0], - 'orientation': orientation.toArray(), -}) -.then(() => { - viewer.start(); -}); -``` - -The `loadFile()` method will accept the original `.ply` files as well as my custom `.splat` files. +Parameters for `loadFile()`
-
-### Creating SPLAT files -To convert a `.ply` file into the stripped-down `.splat` format (currently only compatible with this viewer), there are several options. The easiest method is to use the UI in the main demo page at [http://127.0.0.1:8080/index.html](http://127.0.0.1:8080/index.html). If you want to run the conversion programatically, run the following in a browser: -```javascript -const compressionLevel = 1; -const splatAlphaRemovalThreshold = 5; // out of 255 -const plyLoader = new GaussianSplats3D.PlyLoader(); -plyLoader.loadFromURL('', compressionLevel, splatAlphaRemovalThreshold) -.then((splatBuffer) => { - new GaussianSplats3D.SplatLoader(splatBuffer).downloadFile('converted_file.splat'); -}); -``` -Both of the above methods will prompt your browser to automatically start downloading the converted `.splat` file. +| Parameter | Purpose +| --- | --- +| `splatAlphaRemovalThreshold` | Tells `loadFile()` to ignore any splats with an alpha less than the specified value (valid range: 0 - 255). Defaults to `1`. +| `showLoadingSpinner` | Displays a loading spinner while the scene is loading. Defaults to `true`. +| `position` | Position of the scene, acts as an offset from its default position. Defaults to `[0, 0, 0]`. +| `rotation` | Rotation of the scene represented as a quaternion, defaults to `[0, 0, 0, 1]` (identity quaternion). +| `scale` | Scene's scale, defaults to `[1, 1, 1]`. -The third option is to use the included nodejs script: +
-``` -node util/create-splat.js [path to .PLY] [output file] [compression level = 0] [alpha removal threshold = 1] -``` +The `loadFile()` method will accept the original `.ply` files as well as my custom `.splat` files. -Currently supported values for `compressionLevel` are `0` or `1`. `0` means no compression, `1` means compression of scale, rotation, and position values from 32-bit to 16-bit. -

+ ### Integrating THREE.js scenes -You can integrate your own Three.js scene into the viewer if you want rendering to be handled for you. Just pass a Three.js scene object as the 'scene' parameter to the constructor: +You can integrate your own Three.js scene into the viewer if you want rendering to be handled for you. Just pass a Three.js scene object as the `scene` parameter to the constructor: ```javascript const scene = new THREE.Scene(); - const boxColor = 0xBBBBBB; const boxGeometry = new THREE.BoxGeometry(2, 2, 2); const boxMesh = new THREE.Mesh(boxGeometry, new THREE.MeshBasicMaterial({'color': boxColor})); -scene.add(boxMesh); boxMesh.position.set(3, 2, 2); +scene.add(boxMesh); const viewer = new GaussianSplats3D.Viewer({ 'scene': scene, - 'cameraUp': [0, -1, -0.6], - 'initialCameraPosition': [-1, -4, 6], - 'initialCameraLookAt': [0, 4, -0] }); viewer.loadFile('') .then(() => { viewer.start(); }); ``` + Currently this will only work for objects that write to the depth buffer (e.g. standard opaque objects). Supporting transparent objects will be more challenging :)
+ +A "drop-in" mode for the viewer is also supported. The `RenderableViewer` class encapsulates `Viewer` and can be added to a Three.js scene like any other renderable: +```javascript +const scene = new THREE.Scene(); +const renderableViewer = new GaussianSplats3D.RenderableViewer({ + 'gpuAcceleratedSort': true +}); +renderableViewer.addScenesFromFiles([ + { + 'path': '', + 'splatAlphaRemovalThreshold': 5, + }, + { + 'path': '', + 'rotation': [0, -0.857, -0.514495, 6.123233995736766e-17], + 'scale': [1.5, 1.5, 1.5], + 'position': [0, -2, -1.2], + 'splatAlphaRemovalThreshold': 5, + } + ], + true); +scene.add(renderableViewer); + +```
-### Custom options -The viewer allows for various levels of customization via constructor parameters. You can control when its `update()` and `render()` methods are called by passing `false` for the `selfDrivenMode` parameter and then calling those methods whenever/wherever you decide is appropriate. You can tell the viewer to not use its built-in camera controls by passing `false` for the `useBuiltInControls` parameter. You can also use your own Three.js renderer and camera by passing those values to the viewer's constructor. The sample below shows all of these options: + +### Advanced options +The viewer allows for various levels of customization via constructor parameters. You can control when its `update()` and `render()` methods are called by passing `false` for the `selfDrivenMode` parameter and then calling those methods whenever/wherever you decide is appropriate. You can also use your own camera controls, as well as an your own instance of a Three.js `Renderer` or `Camera` The sample below shows all of these options: ```javascript const renderWidth = 800; @@ -209,14 +209,16 @@ rootElement.appendChild(renderer.domElement); const camera = new THREE.PerspectiveCamera(65, renderWidth / renderHeight, 0.1, 500); camera.position.copy(new THREE.Vector3().fromArray([-1, -4, 6])); -camera.lookAt(new THREE.Vector3().fromArray([0, 4, -0])); camera.up = new THREE.Vector3().fromArray([0, -1, -0.6]).normalize(); +camera.lookAt(new THREE.Vector3().fromArray([0, 4, -0])); const viewer = new GaussianSplats3D.Viewer({ 'selfDrivenMode': false, 'renderer': renderer, 'camera': camera, - 'useBuiltInControls': false + 'useBuiltInControls': false, + 'ignoreDevicePixelRatio': false, + 'gpuAcceleratedSort': true }); viewer.loadFile('') .then(() => { @@ -231,6 +233,40 @@ function update() { viewer.render(); } ``` +Advanced `Viewer` parameters +
+| Parameter | Purpose +| --- | --- +| `selfDrivenMode` | If `false`, tells the viewer that you will manually call its `update()` and `render()` methods. Defaults to `true`. +| `useBuiltInControls` | Tells the viewer to use its own camera controls. Defaults to `true`. +| `renderer` | Pass an instance of a Three.js `Renderer` to the viewer, otherwise it will create its own. Defaults to `undefined`. +| `camera` | Pass an instance of a Three.js `Camera` to the viewer, otherwise it will create its own. Defaults to `undefined`. +| `ignoreDevicePixelRatio` | Tells the viewer to pretend the device pixel ratio is 1, which can boost performance on devices where it is larger, at a small cost to visual quality. Defaults to `false`. +| `gpuAcceleratedSort` | Tells the viewer to use a partially GPU-accelerated approach to sorting splats. Currently this means pre-computing splat distances is done on the GPU. Defaults to `true`. +
+ +### Creating SPLAT files +To convert a `.ply` file into the stripped-down `.splat` format (currently only compatible with this viewer), there are several options. The easiest method is to use the UI in the main demo page at [http://127.0.0.1:8080/index.html](http://127.0.0.1:8080/index.html). If you want to run the conversion programatically, run the following in a browser: + +```javascript +const compressionLevel = 1; +const splatAlphaRemovalThreshold = 5; // out of 255 +const plyLoader = new GaussianSplats3D.PlyLoader(); +plyLoader.loadFromURL('', compressionLevel, splatAlphaRemovalThreshold) +.then((splatBuffer) => { + new GaussianSplats3D.SplatLoader(splatBuffer).downloadFile('converted_file.splat'); +}); +``` +Both of the above methods will prompt your browser to automatically start downloading the converted `.splat` file. + +The third option is to use the included nodejs script: + +``` +node util/create-splat.js [path to .PLY] [output file] [compression level = 0] [alpha removal threshold = 1] +``` + +Currently supported values for `compressionLevel` are `0` or `1`. `0` means no compression, `1` means compression of scale, rotation, and position values from 32-bit to 16-bit. +
### CORS issues and SharedArrayBuffer diff --git a/demo/dropin.html b/demo/dropin.html new file mode 100644 index 0000000..e58e6b9 --- /dev/null +++ b/demo/dropin.html @@ -0,0 +1,120 @@ + + + + + + + + 3D Gaussian Splats - Drop-in example + + + + + + + + + + + \ No newline at end of file diff --git a/demo/garden.html b/demo/garden.html index e20b197..a078bee 100644 --- a/demo/garden.html +++ b/demo/garden.html @@ -34,13 +34,12 @@ const viewer = new GaussianSplats3D.Viewer({ 'cameraUp': [0, -1, -0.54], 'initialCameraPosition': [-3.15634, -0.16946, -0.51552], - 'initialCameraLookAt': [1.52976, 2.27776, 1.65898] + 'initialCameraLookAt': [1.52976, 2.27776, 1.65898], + 'halfPrecisionCovariancesOnGPU': true }); let path = 'assets/data/garden/garden'; path += isMobile() ? '.splat' : '_high.splat'; - viewer.loadFile(path, { - 'halfPrecisionCovariancesOnGPU': true - }) + viewer.loadFile(path) .then(() => { viewer.start(); }); diff --git a/demo/index.html b/demo/index.html index 7889e92..3438ef1 100644 --- a/demo/index.html +++ b/demo/index.html @@ -444,10 +444,10 @@ const viewerOptions = { 'cameraUp': cameraUpArray, 'initialCameraPosition': cameraPositionArray, - 'initialCameraLookAt': cameraLookAtArray - }; - const sceneOptions = { + 'initialCameraLookAt': cameraLookAtArray, 'halfPrecisionCovariancesOnGPU': true, + }; + const splatBufferOptions = { 'splatAlphaRemovalThreshold': alphaRemovalThreshold }; @@ -462,7 +462,7 @@ document.body.style.backgroundColor = "#000000"; history.pushState("ViewSplat", null); const viewer = new GaussianSplats3D.Viewer(viewerOptions); - viewer.loadSplatBuffer(splatBuffer, sceneOptions) + viewer.loadSplatBuffersIntoMesh([splatBuffer], [splatBufferOptions]) .then(() => { viewer.start(); }); diff --git a/demo/stump.html b/demo/stump.html index 1a1df2e..36f0c52 100644 --- a/demo/stump.html +++ b/demo/stump.html @@ -37,9 +37,7 @@ }); let path = 'assets/data/stump/stump'; path += isMobile() ? '.splat' : '_high.splat'; - viewer.loadFile(path, { - 'halfPrecisionCovariancesOnGPU': true - }) + viewer.loadFile(path) .then(() => { viewer.start(); }); diff --git a/demo/truck.html b/demo/truck.html index 5676906..696290e 100644 --- a/demo/truck.html +++ b/demo/truck.html @@ -37,9 +37,7 @@ }); let path = 'assets/data/truck/truck'; path += isMobile() ? '.splat' : '_high.splat'; - viewer.loadFile(path, { - 'halfPrecisionCovariancesOnGPU': true - }) + viewer.loadFile(path) .then(() => { viewer.start(); }); diff --git a/package-lock.json b/package-lock.json index 79be686..65bb380 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { - "name": "gaussian-splat-3d", - "version": "1.0.0", + "name": "gaussian-splats-3d", + "version": "0.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "gaussian-splat-3d", - "version": "1.0.0", + "name": "gaussian-splats-3d", + "version": "0.1.3", "license": "MIT", "dependencies": { - "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-terser": "0.4.4", "@rollup/pluginutils": "5.0.5", "http-server": "14.1.1", "install": "0.13.0", diff --git a/package.json b/package.json index cf41b1c..d780a38 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "type": "git", "url": "https://github.com/mkkellogg/GaussianSplat3D" }, - "version": "0.1.2", + "version": "0.1.3", "description": "Three.js-based 3D Gaussian splat viewer", "main": "src/index.js", "author": "Mark Kellogg", @@ -60,7 +60,7 @@ "url-loader": "4.1.1" }, "dependencies": { - "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-terser": "0.4.4", "@rollup/pluginutils": "5.0.5", "http-server": "14.1.1", "install": "0.13.0", diff --git a/src/LoadingSpinner.js b/src/LoadingSpinner.js index 8862607..8b7fe60 100644 --- a/src/LoadingSpinner.js +++ b/src/LoadingSpinner.js @@ -4,17 +4,24 @@ export class LoadingSpinner { this.message = message || 'Loading...'; this.container = container || document.body; + this.spinnerDivContainerOuter = document.createElement('div'); + this.spinnerDivContainerOuter.className = 'outerContainer'; + this.spinnerDivContainerOuter.style.display = 'none'; + this.spinnerDivContainer = document.createElement('div'); + this.spinnerDivContainer.className = 'container'; + this.spinnerDiv = document.createElement('div'); - this.messageDiv = document.createElement('div'); - this.spinnerDivContainer.className = 'loaderContainer'; this.spinnerDiv.className = 'loader'; - this.spinnerDivContainer.style.display = 'none'; + + this.messageDiv = document.createElement('div'); this.messageDiv.className = 'message'; this.messageDiv.innerHTML = this.message; + this.spinnerDivContainer.appendChild(this.spinnerDiv); this.spinnerDivContainer.appendChild(this.messageDiv); - this.container.appendChild(this.spinnerDivContainer); + this.spinnerDivContainerOuter.appendChild(this.spinnerDivContainer); + this.container.appendChild(this.spinnerDivContainerOuter); const style = document.createElement('style'); style.innerHTML = ` @@ -28,7 +35,12 @@ export class LoadingSpinner { width: 180px; } - .loaderContainer { + .outerContainer { + width: 100%; + height: 100%; + } + + .container { position: absolute; top: 50%; left: 50%; @@ -61,15 +73,24 @@ export class LoadingSpinner { } `; - this.spinnerDivContainer.appendChild(style); + this.spinnerDivContainerOuter.appendChild(style); } show() { - this.spinnerDivContainer.style.display = 'block'; + this.spinnerDivContainerOuter.style.display = 'block'; } hide() { - this.spinnerDivContainer.style.display = 'none'; + this.spinnerDivContainerOuter.style.display = 'none'; + } + + setContainer(container) { + if (this.container) { + this.container.removeChild(this.spinnerDivContainerOuter); + } + this.container = container; + this.container.appendChild(this.spinnerDivContainerOuter); + this.spinnerDivContainerOuter.style.zIndex = this.container.style.zIndex + 1; } setMessage(msg) { diff --git a/src/RenderableViewer.js b/src/RenderableViewer.js new file mode 100644 index 0000000..5028399 --- /dev/null +++ b/src/RenderableViewer.js @@ -0,0 +1,53 @@ +import * as THREE from 'three'; +import { Viewer } from './Viewer.js'; + +export class RenderableViewer extends THREE.Group { + + constructor(options = {}) { + super(); + + options.selfDrivenMode = false; + options.useBuiltInControls = false; + options.rootElement = null; + options.ignoreDevicePixelRatio = false; + options.initializeFromExternalUpdate = true; + options.camera = undefined; + options.renderer = undefined; + + this.viewer = new Viewer(options); + + this.callbackMesh = this.createCallbackMesh(); + this.add(this.callbackMesh); + this.callbackMesh.onBeforeRender = this.onBeforeRender.bind(this); + + } + + addSceneFromFile(fileURL, options = {}) { + if (options.showLoadingSpinner !== false) options.showLoadingSpinner = true; + return this.viewer.loadFile(fileURL, options).then(() => { + this.add(this.viewer.splatMesh); + }); + } + + addScenesFromFiles(files, showLoadingSpinner) { + if (showLoadingSpinner !== false) showLoadingSpinner = true; + return this.viewer.loadFiles(files, showLoadingSpinner).then(() => { + this.add(this.viewer.splatMesh); + }); + } + + onBeforeRender(renderer, scene, camera) { + this.viewer.update(renderer, camera); + } + + createCallbackMesh() { + const geometry = new THREE.SphereGeometry(1, 8, 8); + const material = new THREE.MeshBasicMaterial(); + material.colorWrite = false; + material.depthWrite = false; + const mesh = new THREE.Mesh(geometry, material); + mesh.frustumCulled = false; + return mesh; + } + +} diff --git a/src/SplatBuffer.js b/src/SplatBuffer.js index 109b6fe..844c738 100644 --- a/src/SplatBuffer.js +++ b/src/SplatBuffer.js @@ -112,7 +112,7 @@ export class SplatBuffer { return this.splatBufferData; } - getCenter(index, outCenter = new THREE.Vector3()) { + getCenter(index, outCenter = new THREE.Vector3(), transform) { let bucket = [0, 0, 0]; const centerBase = index * SplatBuffer.CenterComponentCount; if (this.compressionLevel > 0) { @@ -128,6 +128,7 @@ export class SplatBuffer { outCenter.y = this.centerArray[centerBase + 1]; outCenter.z = this.centerArray[centerBase + 2]; } + if (transform) outCenter.applyMatrix4(transform); return outCenter; } @@ -150,38 +151,34 @@ export class SplatBuffer { } } - getScale(index, outScale = new THREE.Vector3()) { - const scaleBase = index * SplatBuffer.ScaleComponentCount; - outScale.set(fbf(this.scaleArray[scaleBase]), fbf(this.scaleArray[scaleBase + 1]), fbf(this.scaleArray[scaleBase + 2])); - return outScale; - } - - setScale(index, scale) { - const scaleBase = index * SplatBuffer.ScaleComponentCount; - this.scaleArray[scaleBase] = tbf(scale.x); - this.scaleArray[scaleBase + 1] = tbf(scale.y); - this.scaleArray[scaleBase + 2] = tbf(scale.z); - } + getScaleAndRotation = function() { + + const scaleMatrix = new THREE.Matrix4(); + const rotationMatrix = new THREE.Matrix4(); + const tempMatrix = new THREE.Matrix4(); + const tempPosition = new THREE.Vector3(); + + return function(index, outScale = new THREE.Vector3(), outRotation = new THREE.Quaternion(), transform) { + const scaleBase = index * SplatBuffer.ScaleComponentCount; + outScale.set(fbf(this.scaleArray[scaleBase]), fbf(this.scaleArray[scaleBase + 1]), fbf(this.scaleArray[scaleBase + 2])); + const rotationBase = index * SplatBuffer.RotationComponentCount; + outRotation.set(fbf(this.rotationArray[rotationBase + 1]), fbf(this.rotationArray[rotationBase + 2]), + fbf(this.rotationArray[rotationBase + 3]), fbf(this.rotationArray[rotationBase])); + if (transform) { + scaleMatrix.makeScale(outScale.x, outScale.y, outScale.z); + rotationMatrix.makeRotationFromQuaternion(outRotation); + tempMatrix.copy(scaleMatrix).multiply(rotationMatrix).multiply(transform); + tempMatrix.decompose(tempPosition, outRotation, outScale); + } + }; - getRotation(index, outRotation = new THREE.Quaternion()) { - const rotationBase = index * SplatBuffer.RotationComponentCount; - outRotation.set(fbf(this.rotationArray[rotationBase + 1]), fbf(this.rotationArray[rotationBase + 2]), - fbf(this.rotationArray[rotationBase + 3]), fbf(this.rotationArray[rotationBase])); - return outRotation; - } + }(); - setRotation(index, rotation) { - const rotationBase = index * SplatBuffer.RotationComponentCount; - this.rotationArray[rotationBase] = tbf(rotation.w); - this.rotationArray[rotationBase + 1] = tbf(rotation.x); - this.rotationArray[rotationBase + 2] = tbf(rotation.y); - this.rotationArray[rotationBase + 3] = tbf(rotation.z); - } - - getColor(index, outColor = new THREE.Vector4()) { + getColor(index, outColor = new THREE.Vector4(), transform) { const colorBase = index * SplatBuffer.ColorComponentCount; outColor.set(this.colorArray[colorBase], this.colorArray[colorBase + 1], this.colorArray[colorBase + 2], this.colorArray[colorBase + 3]); + // TODO: apply transform for spherical harmonics return outColor; } @@ -197,7 +194,7 @@ export class SplatBuffer { return this.splatCount; } - fillCovarianceArray(covarianceArray) { + fillCovarianceArray(covarianceArray, destOffset, transform) { const splatCount = this.splatCount; const scale = new THREE.Vector3(); @@ -205,6 +202,9 @@ export class SplatBuffer { const rotationMatrix = new THREE.Matrix3(); const scaleMatrix = new THREE.Matrix3(); const covarianceMatrix = new THREE.Matrix3(); + const transformedCovariance = new THREE.Matrix3(); + const transform3x3 = new THREE.Matrix3(); + const transform3x3Transpose = new THREE.Matrix3(); const tempMatrix4 = new THREE.Matrix4(); for (let i = 0; i < splatCount; i++) { @@ -222,92 +222,116 @@ export class SplatBuffer { rotationMatrix.setFromMatrix4(tempMatrix4); covarianceMatrix.copy(rotationMatrix).multiply(scaleMatrix); - const M = covarianceMatrix.elements; - covarianceArray[SplatBuffer.CovarianceSizeFloats * i] = M[0] * M[0] + M[3] * M[3] + M[6] * M[6]; - covarianceArray[SplatBuffer.CovarianceSizeFloats * i + 1] = M[0] * M[1] + M[3] * M[4] + M[6] * M[7]; - covarianceArray[SplatBuffer.CovarianceSizeFloats * i + 2] = M[0] * M[2] + M[3] * M[5] + M[6] * M[8]; - covarianceArray[SplatBuffer.CovarianceSizeFloats * i + 3] = M[1] * M[1] + M[4] * M[4] + M[7] * M[7]; - covarianceArray[SplatBuffer.CovarianceSizeFloats * i + 4] = M[1] * M[2] + M[4] * M[5] + M[7] * M[8]; - covarianceArray[SplatBuffer.CovarianceSizeFloats * i + 5] = M[2] * M[2] + M[5] * M[5] + M[8] * M[8]; + transformedCovariance.copy(covarianceMatrix).transpose().premultiply(covarianceMatrix); + const covBase = SplatBuffer.CovarianceSizeFloats * (i + destOffset); + + if (transform) { + transform3x3.setFromMatrix4(transform); + transform3x3Transpose.copy(transform3x3).transpose(); + transformedCovariance.multiply(transform3x3Transpose); + transformedCovariance.premultiply(transform3x3); + } + + covarianceArray[covBase] = transformedCovariance.elements[0]; + covarianceArray[covBase + 1] = transformedCovariance.elements[3]; + covarianceArray[covBase + 2] = transformedCovariance.elements[6]; + covarianceArray[covBase + 3] = transformedCovariance.elements[4]; + covarianceArray[covBase + 4] = transformedCovariance.elements[7]; + covarianceArray[covBase + 5] = transformedCovariance.elements[8]; } } - fillCenterArray(outCenterArray) { + fillCenterArray(outCenterArray, destOffset, transform) { const splatCount = this.splatCount; let bucket = [0, 0, 0]; + const center = new THREE.Vector3(); for (let i = 0; i < splatCount; i++) { - const centerBase = i * SplatBuffer.CenterComponentCount; + const centerSrcBase = i * SplatBuffer.CenterComponentCount; + const centerDestBase = (i + destOffset) * SplatBuffer.CenterComponentCount; if (this.compressionLevel > 0) { const bucketIndex = Math.floor(i / this.bucketSize); bucket = new Float32Array(this.splatBufferData, this.bucketsBase + bucketIndex * this.bytesPerBucket, 3); const sf = this.compressionScaleFactor; const sr = this.compressionScaleRange; - outCenterArray[centerBase] = (this.centerArray[centerBase] - sr) * sf + bucket[0]; - outCenterArray[centerBase + 1] = (this.centerArray[centerBase + 1] - sr) * sf + bucket[1]; - outCenterArray[centerBase + 2] = (this.centerArray[centerBase + 2] - sr) * sf + bucket[2]; + center.x = (this.centerArray[centerSrcBase] - sr) * sf + bucket[0]; + center.y = (this.centerArray[centerSrcBase + 1] - sr) * sf + bucket[1]; + center.z = (this.centerArray[centerSrcBase + 2] - sr) * sf + bucket[2]; } else { - outCenterArray[centerBase] = this.centerArray[centerBase]; - outCenterArray[centerBase + 1] = this.centerArray[centerBase + 1]; - outCenterArray[centerBase + 2] = this.centerArray[centerBase + 2]; + center.x = this.centerArray[centerSrcBase]; + center.y = this.centerArray[centerSrcBase + 1]; + center.z = this.centerArray[centerSrcBase + 2]; } + if (transform) { + center.applyMatrix4(transform); + } + outCenterArray[centerDestBase] = center.x; + outCenterArray[centerDestBase + 1] = center.y; + outCenterArray[centerDestBase + 2] = center.z; } } - fillScaleArray(outScaleArray) { - const fbf = this.fbf.bind(this); - const splatCount = this.splatCount; - for (let i = 0; i < splatCount; i++) { - const scaleBase = i * SplatBuffer.ScaleComponentCount; - outScaleArray[scaleBase] = fbf(this.scaleArray[scaleBase]); - outScaleArray[scaleBase + 1] = fbf(this.scaleArray[scaleBase + 1]); - outScaleArray[scaleBase + 2] = fbf(this.scaleArray[scaleBase + 2]); - } - } - - fillRotationArray(outRotationArray) { - const fbf = this.fbf.bind(this); - const splatCount = this.splatCount; - for (let i = 0; i < splatCount; i++) { - const rotationBase = i * SplatBuffer.RotationComponentCount; - outRotationArray[rotationBase] = fbf(this.rotationArray[rotationBase]); - outRotationArray[rotationBase + 1] = fbf(this.rotationArray[rotationBase + 1]); - outRotationArray[rotationBase + 2] = fbf(this.rotationArray[rotationBase + 2]); - outRotationArray[rotationBase + 3] = fbf(this.rotationArray[rotationBase + 3]); - } - } - - fillColorArray(outColorArray) { + fillColorArray(outColorArray, destOffset, transform) { const splatCount = this.splatCount; for (let i = 0; i < splatCount; i++) { - const colorBase = i * SplatBuffer.ColorComponentCount; - outColorArray[colorBase] = this.colorArray[colorBase]; - outColorArray[colorBase + 1] = this.colorArray[colorBase + 1]; - outColorArray[colorBase + 2] = this.colorArray[colorBase + 2]; - outColorArray[colorBase + 3] = this.colorArray[colorBase + 3]; + const colorSrcBase = i * SplatBuffer.ColorComponentCount; + const colorDestBase = (i + destOffset) * SplatBuffer.ColorComponentCount; + outColorArray[colorDestBase] = this.colorArray[colorSrcBase]; + outColorArray[colorDestBase + 1] = this.colorArray[colorSrcBase + 1]; + outColorArray[colorDestBase + 2] = this.colorArray[colorSrcBase + 2]; + outColorArray[colorDestBase + 3] = this.colorArray[colorSrcBase + 3]; + // TODO: implement application of transform for spherical harmonics } } swapVertices(indexA, indexB) { - this.getCenter(indexA, tempVector3A); - this.getCenter(indexB, tempVector3B); - this.setCenter(indexB, tempVector3A); - this.setCenter(indexA, tempVector3B); - - this.getScale(indexA, tempVector3A); - this.getScale(indexB, tempVector3B); - this.setScale(indexB, tempVector3A); - this.setScale(indexA, tempVector3B); - - this.getRotation(indexA, tempQuaternion4A); - this.getRotation(indexB, tempQuaternion4B); - this.setRotation(indexB, tempQuaternion4A); - this.setRotation(indexA, tempQuaternion4B); - - this.getColor(indexA, tempVector4A); - this.getColor(indexB, tempVector4B); - this.setColor(indexB, tempVector4A); - this.setColor(indexA, tempVector4B); + const getScale = (index, outScale = new THREE.Vector3()) => { + const scaleBase = index * SplatBuffer.ScaleComponentCount; + outScale.set(fbf(this.scaleArray[scaleBase]), fbf(this.scaleArray[scaleBase + 1]), fbf(this.scaleArray[scaleBase + 2])); + return outScale; + }; + + const setScale = (index, scale) => { + const scaleBase = index * SplatBuffer.ScaleComponentCount; + this.scaleArray[scaleBase] = tbf(scale.x); + this.scaleArray[scaleBase + 1] = tbf(scale.y); + this.scaleArray[scaleBase + 2] = tbf(scale.z); + }; + + const getRotation = (index, outRotation = new THREE.Quaternion()) => { + const rotationBase = index * SplatBuffer.RotationComponentCount; + outRotation.set(fbf(this.rotationArray[rotationBase + 1]), fbf(this.rotationArray[rotationBase + 2]), + fbf(this.rotationArray[rotationBase + 3]), fbf(this.rotationArray[rotationBase])); + return outRotation; + }; + + const setRotation = (index, rotation) => { + const rotationBase = index * SplatBuffer.RotationComponentCount; + this.rotationArray[rotationBase] = tbf(rotation.w); + this.rotationArray[rotationBase + 1] = tbf(rotation.x); + this.rotationArray[rotationBase + 2] = tbf(rotation.y); + this.rotationArray[rotationBase + 3] = tbf(rotation.z); + }; + + getCenter(indexA, tempVector3A); + getCenter(indexB, tempVector3B); + setCenter(indexB, tempVector3A); + setCenter(indexA, tempVector3B); + + getScale(indexA, tempVector3A); + getScale(indexB, tempVector3B); + setScale(indexB, tempVector3A); + setScale(indexA, tempVector3B); + + getRotation(indexA, tempQuaternion4A); + getRotation(indexB, tempQuaternion4B); + setRotation(indexB, tempQuaternion4A); + setRotation(indexA, tempQuaternion4B); + + getColor(indexA, tempVector4A); + getColor(indexB, tempVector4B); + setColor(indexB, tempVector4A); + setColor(indexA, tempVector4B); } diff --git a/src/SplatMesh.js b/src/SplatMesh.js index 6ac3be8..b365191 100644 --- a/src/SplatMesh.js +++ b/src/SplatMesh.js @@ -4,42 +4,25 @@ import { uintEncodedFloat, rgbaToInteger } from './Util.js'; export class SplatMesh extends THREE.Mesh { - static buildMesh(splatBuffer, renderer, splatAlphaRemovalThreshold = 1, halfPrecisionCovariancesOnGPU = false, - devicePixelRatio = 1, enableDistancesComputationOnGPU = true) { - const geometry = SplatMesh.buildGeomtery(splatBuffer); - const material = SplatMesh.buildMaterial(); - return new SplatMesh(splatBuffer, geometry, material, renderer, splatAlphaRemovalThreshold, - halfPrecisionCovariancesOnGPU, devicePixelRatio, enableDistancesComputationOnGPU); - } - - constructor(splatBuffer, geometry, material, renderer, splatAlphaRemovalThreshold = 1, - halfPrecisionCovariancesOnGPU = false, devicePixelRatio = 1, enableDistancesComputationOnGPU = true) { - super(geometry, material); - this.splatBuffer = splatBuffer; - this.geometry = geometry; - this.material = material; - this.renderer = renderer; - this.splatTree = null; - this.splatDataTextures = null; - this.splatAlphaRemovalThreshold = splatAlphaRemovalThreshold; + constructor(halfPrecisionCovariancesOnGPU = false, devicePixelRatio = 1, enableDistancesComputationOnGPU = true) { + super({'morphAttributes': {}, 'fake': true}, null); + this.renderer = undefined; this.halfPrecisionCovariancesOnGPU = halfPrecisionCovariancesOnGPU; this.devicePixelRatio = devicePixelRatio; this.enableDistancesComputationOnGPU = enableDistancesComputationOnGPU; - this.buildSplatTree(); - - if (this.enableDistancesComputationOnGPU) { - this.distancesTransformFeedback = { - 'id': null, - 'program': null, - 'centersBuffer': null, - 'outDistancesBuffer': null, - 'centersLoc': -1, - 'viewProjLoc': -1, - }; - this.setupDistancesTransformFeedback(); - } - - this.resetLocalSplatDataAndTexturesFromSplatBuffer(); + this.splatBuffers = []; + this.splatTree = null; + this.splatDataTextures = null; + this.distancesTransformFeedback = { + 'id': null, + 'vertexShader': null, + 'fragmentShader': null, + 'program': null, + 'centersBuffer': null, + 'outDistancesBuffer': null, + 'centersLoc': -1, + 'viewProjLoc': -1, + }; } static buildMaterial() { @@ -225,9 +208,9 @@ export class SplatMesh extends THREE.Mesh { return material; } - static buildGeomtery(splatBuffer) { + static buildGeomtery(splatBuffers) { - const splatCount = splatBuffer.getSplatCount(); + let totalSplatCount = SplatMesh.getTotalSplatCount(splatBuffers); const baseGeometry = new THREE.BufferGeometry(); baseGeometry.setIndex([0, 1, 2, 0, 2, 3]); @@ -243,24 +226,85 @@ export class SplatMesh extends THREE.Mesh { const geometry = new THREE.InstancedBufferGeometry().copy(baseGeometry); - const splatIndexArray = new Uint32Array(splatCount); + const splatIndexArray = new Uint32Array(totalSplatCount); const splatIndexes = new THREE.InstancedBufferAttribute(splatIndexArray, 1, false); splatIndexes.setUsage(THREE.DynamicDrawUsage); geometry.setAttribute('splatIndex', splatIndexes); - geometry.instanceCount = splatCount; + geometry.instanceCount = totalSplatCount; return geometry; } + dispose() { + this.disposeMeshData(); + if (this.enableDistancesComputationOnGPU) { + this.disposeGPUResources(); + } + } + + disposeMeshData() { + if (this.geometry && !this.geometry.fake) { + this.geometry.dispose(); + this.geometry = null; + } + for (let textureKey in this.splatDataTextures) { + if (this.splatDataTextures.hasOwnProperty(textureKey)) { + const textureContainer = this.splatDataTextures[textureKey]; + if (textureContainer.texture) { + textureContainer.texture.dispose(); + textureContainer.texture = null; + } + } + } + this.splatDataTextures = null; + if (this.material) { + this.material.dispose(); + this.material = null; + } + this.splatTree = null; + } + + build(splatBuffers, splatBufferOptions) { + this.disposeMeshData(); + this.splatBuffers = splatBuffers; + this.splatBufferOptions = splatBufferOptions; + this.buildSplatTransforms(); + this.geometry = SplatMesh.buildGeomtery(this.splatBuffers); + this.material = SplatMesh.buildMaterial(); + this.buildSplatTree(); + if (this.enableDistancesComputationOnGPU) { + this.setupDistancesTransformFeedback(); + } + this.resetLocalSplatDataAndTexturesFromSplatBuffer(); + } + + buildSplatTransforms() { + this.splatTransforms = []; + for (let splatBufferOptions of this.splatBufferOptions) { + if (splatBufferOptions) { + let positionArray = splatBufferOptions['position'] || [0, 0, 0]; + let rotationArray = splatBufferOptions['rotation'] || [0, 0, 0, 1]; + let scaleArray = splatBufferOptions['scale'] || [1, 1, 1]; + const position = new THREE.Vector3().fromArray(positionArray); + const rotation = new THREE.Quaternion().fromArray(rotationArray); + const scale = new THREE.Vector3().fromArray(scaleArray); + const transform = new THREE.Matrix4(); + transform.compose(position, rotation, scale); + this.splatTransforms.push(transform); + } + } + } + buildSplatTree() { - this.splatTree = new SplatTree(10, 500); + this.splatTree = new SplatTree(8, 1000); console.time('SplatTree build'); const splatColor = new THREE.Vector4(); - this.splatTree.processSplatBuffer(this.splatBuffer, (splatIndex) => { - this.splatBuffer.getColor(splatIndex, splatColor); - return splatColor.w > this.splatAlphaRemovalThreshold; + this.splatTree.processSplatMesh(this, (splatBufferIndex, splatBuffer, splatLocalIndex, transform) => { + splatBuffer.getColor(splatLocalIndex, splatColor, transform); + const splatBufferOptions = this.splatBufferOptions[splatBufferIndex]; + return splatColor.w > (splatBufferOptions.splatAlphaRemovalThreshold || 1); }); console.timeEnd('SplatTree build'); @@ -297,19 +341,26 @@ export class SplatMesh extends THREE.Mesh { } updateLocalSplatDataFromSplatBuffer() { - const splatCount = this.splatBuffer.getSplatCount(); + const splatCount = this.getSplatCount(); this.covariances = new Float32Array(splatCount * 6); - this.colors = new Uint8Array(splatCount * 4); this.centers = new Float32Array(splatCount * 3); - this.splatBuffer.fillCovarianceArray(this.covariances); - this.splatBuffer.fillCenterArray(this.centers); - this.splatBuffer.fillColorArray(this.colors); + this.colors = new Uint8Array(splatCount * 4); + + let offset = 0; + for (let i = 0; i < this.splatBuffers.length; i++) { + const splatBuffer = this.splatBuffers[i]; + const transform = this.splatTransforms[i]; + splatBuffer.fillCovarianceArray(this.covariances, offset, transform); + splatBuffer.fillCenterArray(this.centers, offset, transform); + splatBuffer.fillColorArray(this.colors, offset, transform); + offset += splatBuffer.getSplatCount(); + } } allocateAndStoreLocalSplatDataInTextures() { const COVARIANCES_ELEMENTS_PER_TEXEL = 2; const CENTER_COLORS_ELEMENTS_PER_TEXEL = 4; - const splatCount = this.splatBuffer.getSplatCount(); + const splatCount = this.getSplatCount(); const covariancesTextureSize = new THREE.Vector2(4096, 1024); while (covariancesTextureSize.x * covariancesTextureSize.y * COVARIANCES_ELEMENTS_PER_TEXEL < splatCount * 6) { @@ -403,7 +454,7 @@ export class SplatMesh extends THREE.Mesh { const viewport = new THREE.Vector2(); return function(renderDimensions, cameraFocalLengthX, cameraFocalLengthY) { - const splatCount = this.splatBuffer.getSplatCount(); + const splatCount = this.getSplatCount(); if (splatCount > 0) { viewport.set(renderDimensions.x * this.devicePixelRatio, renderDimensions.y * this.devicePixelRatio); @@ -421,115 +472,194 @@ export class SplatMesh extends THREE.Mesh { } getSplatCount() { - return this.splatBuffer.getSplatCount(); + return SplatMesh.getTotalSplatCount(this.splatBuffers); } - getCenters() { - return this.centers; + static getTotalSplatCount(splatBuffers) { + let totalSplatCount = 0; + for (let splatBuffer of splatBuffers) totalSplatCount += splatBuffer.getSplatCount(); + return totalSplatCount; } - getColors() { - return this.colors; - } + disposeGPUResources() { + + if (!this.renderer) return; - getCovariances() { - return this.covariances; + const gl = this.renderer.getContext(); + + if (this.distancesTransformFeedback.vao) { + gl.deleteVertexArray(this.distancesTransformFeedback.vao); + this.distancesTransformFeedback.vao = null; + } + if (this.distancesTransformFeedback.program) { + gl.deleteProgram(this.distancesTransformFeedback.program); + gl.deleteShader(this.distancesTransformFeedback.vertexShader); + gl.deleteShader(this.distancesTransformFeedback.fragmentShader); + this.distancesTransformFeedback.program = null; + this.distancesTransformFeedback.vertexShader = null; + this.distancesTransformFeedback.fragmentShader = null; + } + this.disposeGPUBufferResources(); + if (this.distancesTransformFeedback.id) { + gl.deleteTransformFeedback(this.distancesTransformFeedback.id); + this.distancesTransformFeedback.id = null; + } } - setupDistancesTransformFeedback() { + disposeGPUBufferResources() { - const splatCount = this.getSplatCount(); + if (!this.renderer) return; - const createShader = (gl, type, source) => { - const shader = gl.createShader(type); - if (!shader) { - console.error('Fatal error: gl could not create a shader object.'); - return null; - } + const gl = this.renderer.getContext(); - gl.shaderSource(shader, source); - gl.compileShader(shader); - - const compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS); - if (!compiled) { - let typeName = 'unknown'; - if (type === gl.VERTEX_SHADER) typeName = 'vertex shader'; - else if (type === gl.FRAGMENT_SHADER) typeName = 'fragement shader'; - const errors = gl.getShaderInfoLog(shader); - console.error('Failed to compile ' + typeName + ' with these errors:' + errors); - gl.deleteShader(shader); - return null; + if (this.distancesTransformFeedback.centersBuffer) { + this.distancesTransformFeedback.centersBuffer = null; + gl.deleteBuffer(this.distancesTransformFeedback.centersBuffer); + } + if (this.distancesTransformFeedback.outDistancesBuffer) { + gl.deleteBuffer(this.distancesTransformFeedback.outDistancesBuffer); + this.distancesTransformFeedback.outDistancesBuffer = null; + } + } + + setRenderer(renderer) { + if (renderer !== this.renderer) { + this.renderer = renderer; + if (this.enableDistancesComputationOnGPU && this.getSplatCount() > 0) { + this.setupDistancesTransformFeedback(); + this.updateCentersGPUBufferForDistancesComputation(); } + } + } - return shader; - }; + setupDistancesTransformFeedback = function() { - const vsSource = - `#version 300 es - in ivec3 center; - uniform ivec3 viewProj; - flat out int distance; - void main(void) { - distance = center.x * viewProj.x + center.y * viewProj.y + center.z * viewProj.z; - } - `; + let currentRenderer; + let currentSplatCount; - const fsSource = - `#version 300 es - precision lowp float; - out vec4 fragColor; - void main(){} - `; + return function() { + const splatCount = this.getSplatCount(); - const gl = this.renderer.getContext(); + if (!this.renderer || (currentRenderer === this.renderer && currentSplatCount === splatCount)) return; + const rebuildGPUObjects = (currentRenderer !== this.renderer); + const rebuildBuffers = currentSplatCount !== splatCount; + if (rebuildGPUObjects) { + this.disposeGPUResources(); + } else if (rebuildBuffers) { + this.disposeGPUBufferResources(); + } - const currentVao = gl.getParameter(gl.VERTEX_ARRAY_BINDING); + const gl = this.renderer.getContext(); + + const createShader = (gl, type, source) => { + const shader = gl.createShader(type); + if (!shader) { + console.error('Fatal error: gl could not create a shader object.'); + return null; + } + + gl.shaderSource(shader, source); + gl.compileShader(shader); + + const compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS); + if (!compiled) { + let typeName = 'unknown'; + if (type === gl.VERTEX_SHADER) typeName = 'vertex shader'; + else if (type === gl.FRAGMENT_SHADER) typeName = 'fragement shader'; + const errors = gl.getShaderInfoLog(shader); + console.error('Failed to compile ' + typeName + ' with these errors:' + errors); + gl.deleteShader(shader); + return null; + } + + return shader; + }; - this.distancesTransformFeedback.vao = gl.createVertexArray(); - gl.bindVertexArray(this.distancesTransformFeedback.vao); + const vsSource = + `#version 300 es + in ivec3 center; + uniform ivec3 viewProj; + flat out int distance; + void main(void) { + distance = center.x * viewProj.x + center.y * viewProj.y + center.z * viewProj.z; + } + `; + + const fsSource = + `#version 300 es + precision lowp float; + out vec4 fragColor; + void main(){} + `; + + const currentVao = gl.getParameter(gl.VERTEX_ARRAY_BINDING); + const currentProgram = gl.getParameter(gl.CURRENT_PROGRAM); + + if (rebuildGPUObjects) { + this.distancesTransformFeedback.vao = gl.createVertexArray(); + } - this.distancesTransformFeedback.program = gl.createProgram(); - const vertexShader = createShader(gl, gl.VERTEX_SHADER, vsSource); - const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fsSource); - if (!vertexShader || !fragmentShader) { - throw new Error('Could not compile shaders for distances computation on GPU.'); - } - gl.attachShader(this.distancesTransformFeedback.program, vertexShader); - gl.attachShader(this.distancesTransformFeedback.program, fragmentShader); - gl.transformFeedbackVaryings(this.distancesTransformFeedback.program, ['distance'], gl.SEPARATE_ATTRIBS); - gl.linkProgram(this.distancesTransformFeedback.program); - - const linked = gl.getProgramParameter(this.distancesTransformFeedback.program, gl.LINK_STATUS); - if (!linked) { - const error = gl.getProgramInfoLog(program); - console.error('Fatal error: Failed to link program: ' + error); - gl.deleteProgram(this.distancesTransformFeedback.program); - gl.deleteShader(fragmentShader); - gl.deleteShader(vertexShader); - throw new Error('Could not link shaders for distances computation on GPU.'); - } + gl.bindVertexArray(this.distancesTransformFeedback.vao); + + if (rebuildGPUObjects) { + const program = gl.createProgram(); + const vertexShader = createShader(gl, gl.VERTEX_SHADER, vsSource); + const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fsSource); + if (!vertexShader || !fragmentShader) { + throw new Error('Could not compile shaders for distances computation on GPU.'); + } + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.transformFeedbackVaryings(program, ['distance'], gl.SEPARATE_ATTRIBS); + gl.linkProgram(program); + + const linked = gl.getProgramParameter(program, gl.LINK_STATUS); + if (!linked) { + const error = gl.getProgramInfoLog(program); + console.error('Fatal error: Failed to link program: ' + error); + gl.deleteProgram(program); + gl.deleteShader(fragmentShader); + gl.deleteShader(vertexShader); + throw new Error('Could not link shaders for distances computation on GPU.'); + } + + this.distancesTransformFeedback.program = program; + this.distancesTransformFeedback.vertexShader = vertexShader; + this.distancesTransformFeedback.vertexShader = fragmentShader; + } - gl.useProgram(this.distancesTransformFeedback.program); + gl.useProgram(this.distancesTransformFeedback.program); - this.distancesTransformFeedback.centersLoc = gl.getAttribLocation(this.distancesTransformFeedback.program, 'center'); - this.distancesTransformFeedback.viewProjLoc = gl.getUniformLocation(this.distancesTransformFeedback.program, 'viewProj'); + this.distancesTransformFeedback.centersLoc = gl.getAttribLocation(this.distancesTransformFeedback.program, 'center'); + this.distancesTransformFeedback.viewProjLoc = gl.getUniformLocation(this.distancesTransformFeedback.program, 'viewProj'); - this.distancesTransformFeedback.centersBuffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, this.distancesTransformFeedback.centersBuffer); - gl.enableVertexAttribArray(this.distancesTransformFeedback.centersLoc); - gl.vertexAttribIPointer(this.distancesTransformFeedback.centersLoc, 3, gl.INT, 0, 0); + if (rebuildGPUObjects || rebuildBuffers) { + this.distancesTransformFeedback.centersBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, this.distancesTransformFeedback.centersBuffer); + gl.enableVertexAttribArray(this.distancesTransformFeedback.centersLoc); + gl.vertexAttribIPointer(this.distancesTransformFeedback.centersLoc, 3, gl.INT, 0, 0); + } - this.distancesTransformFeedback.outDistancesBuffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, this.distancesTransformFeedback.outDistancesBuffer); - gl.bufferData(gl.ARRAY_BUFFER, splatCount * 4, gl.DYNAMIC_COPY); + if (rebuildGPUObjects || rebuildBuffers) { + this.distancesTransformFeedback.outDistancesBuffer = gl.createBuffer(); + } + gl.bindBuffer(gl.ARRAY_BUFFER, this.distancesTransformFeedback.outDistancesBuffer); + gl.bufferData(gl.ARRAY_BUFFER, splatCount * 4, gl.DYNAMIC_COPY); - this.distancesTransformFeedback.id = gl.createTransformFeedback(); - gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, this.distancesTransformFeedback.id); - gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, this.distancesTransformFeedback.outDistancesBuffer); + if (rebuildGPUObjects) { + this.distancesTransformFeedback.id = gl.createTransformFeedback(); + } + gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, this.distancesTransformFeedback.id); + gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, this.distancesTransformFeedback.outDistancesBuffer); - if (currentVao) gl.bindVertexArray(currentVao); + if (currentProgram) gl.useProgram(currentProgram); + if (currentVao) gl.bindVertexArray(currentVao); - } + currentRenderer = this.renderer; + currentSplatCount = splatCount; + }; + + }(); getIntegerCenters(padFour) { const splatCount = this.getSplatCount(); @@ -556,6 +686,9 @@ export class SplatMesh extends THREE.Mesh { } updateCentersGPUBufferForDistancesComputation() { + + if (!this.renderer) return; + const gl = this.renderer.getContext(); const currentVao = gl.getParameter(gl.VERTEX_ARRAY_BINDING); @@ -570,6 +703,8 @@ export class SplatMesh extends THREE.Mesh { computeDistancesOnGPU(viewProjMatrix, outComputedDistances) { + if (!this.renderer) return; + const iViewProjMatrix = this.getIntegerMatrixArray(viewProjMatrix); const iViewProj = [iViewProjMatrix[2], iViewProjMatrix[6], iViewProjMatrix[10]]; diff --git a/src/Viewer.js b/src/Viewer.js index 5a9850a..234c1b9 100644 --- a/src/Viewer.js +++ b/src/Viewer.js @@ -15,37 +15,39 @@ const MINIMUM_DISTANCE_TO_NEW_FOCAL_POINT = .75; export class Viewer { - constructor(params = {}) { - - if (!params.cameraUp) params.cameraUp = [0, 1, 0]; - if (!params.initialCameraPosition) params.initialCameraPosition = [0, 10, 15]; - if (!params.initialCameraLookAt) params.initialCameraLookAt = [0, 0, 0]; - if (params.selfDrivenMode === undefined) params.selfDrivenMode = true; - if (params.useBuiltInControls === undefined) params.useBuiltInControls = true; - - this.rootElement = params.rootElement; - this.usingExternalCamera = params.camera ? true : false; - this.usingExternalRenderer = params.renderer ? true : false; - - this.cameraUp = new THREE.Vector3().fromArray(params.cameraUp); - this.initialCameraPosition = new THREE.Vector3().fromArray(params.initialCameraPosition); - this.initialCameraLookAt = new THREE.Vector3().fromArray(params.initialCameraLookAt); - - this.scene = params.scene; - this.renderer = params.renderer; - this.camera = params.camera; - this.useBuiltInControls = params.useBuiltInControls; - this.controls = null; + constructor(options = {}) { + + if (!options.cameraUp) options.cameraUp = [0, 1, 0]; + if (!options.initialCameraPosition) options.initialCameraPosition = [0, 10, 15]; + if (!options.initialCameraLookAt) options.initialCameraLookAt = [0, 0, 0]; + + if (options.selfDrivenMode === undefined) options.selfDrivenMode = true; + if (options.useBuiltInControls === undefined) options.useBuiltInControls = true; + this.rootElement = options.rootElement; - this.ignoreDevicePixelRatio = params.ignoreDevicePixelRatio || false; + this.ignoreDevicePixelRatio = options.ignoreDevicePixelRatio || false; this.devicePixelRatio = this.ignoreDevicePixelRatio ? 1 : window.devicePixelRatio; - this.selfDrivenMode = params.selfDrivenMode; + if (options.halfPrecisionCovariancesOnGPU === undefined) options.halfPrecisionCovariancesOnGPU = true; + this.halfPrecisionCovariancesOnGPU = options.halfPrecisionCovariancesOnGPU; + + this.cameraUp = new THREE.Vector3().fromArray(options.cameraUp); + this.initialCameraPosition = new THREE.Vector3().fromArray(options.initialCameraPosition); + this.initialCameraLookAt = new THREE.Vector3().fromArray(options.initialCameraLookAt); + + this.scene = options.scene; + this.renderer = options.renderer; + this.camera = options.camera; + this.useBuiltInControls = options.useBuiltInControls; + this.controls = null; + + this.selfDrivenMode = options.selfDrivenMode; this.selfDrivenUpdateFunc = this.selfDrivenUpdate.bind(this); - this.gpuAcceleratedSort = params.gpuAcceleratedSort; + this.gpuAcceleratedSort = options.gpuAcceleratedSort; if (this.gpuAcceleratedSort !== true && this.gpuAcceleratedSort !== false) { - this.gpuAcceleratedSort = true; + if (this.isMobile()) this.gpuAcceleratedSort = false; + else this.gpuAcceleratedSort = true; } this.showMeshCursor = false; @@ -81,19 +83,38 @@ export class Viewer { this.mouseDownPosition = new THREE.Vector2(); this.mouseDownTime = null; + this.loadingSpinner = new LoadingSpinner(null, this.rootElement || document.body); + this.loadingSpinner.hide(); + + this.usingExternalCamera = undefined; + this.usingExternalRenderer = undefined; + this.initializeFromExternalUpdate = options.initializeFromExternalUpdate || false; this.initialized = false; - this.init(); + if (!this.initializeFromExternalUpdate) this.init(); } init() { if (this.initialized) return; - if (!this.rootElement && !this.usingExternalRenderer) { - this.rootElement = document.createElement('div'); - this.rootElement.style.width = '100%'; - this.rootElement.style.height = '100%'; - document.body.appendChild(this.rootElement); + if (!this.initializeFromExternalUpdate) { + this.usingExternalCamera = this.camera ? true : false; + this.usingExternalRenderer = this.renderer ? true : false; + } else { + this.usingExternalCamera = true; + this.usingExternalRenderer = true; + } + + if (!this.rootElement) { + if (!this.usingExternalRenderer) { + this.rootElement = document.createElement('div'); + this.rootElement.style.width = '100%'; + this.rootElement.style.height = '100%'; + this.rootElement.style.position = 'absolute'; + document.body.appendChild(this.rootElement); + } else { + this.rootElement = this.renderer.domElement.parentElement || document.body; + } } const renderDimensions = new THREE.Vector2(); @@ -148,9 +169,7 @@ export class Viewer { } this.setupInfoPanel(); - - this.loadingSpinner = new LoadingSpinner(null, this.rootElement); - this.loadingSpinner.hide(); + this.loadingSpinner.setContainer(this.rootElement); this.initialized = true; } @@ -305,6 +324,7 @@ export class Viewer { const renderDimensions = new THREE.Vector2(); return function() { + if (!this.splatMesh) return; const splatCount = this.splatMesh.getSplatCount(); if (splatCount > 0) { this.getRenderDimensions(renderDimensions); @@ -319,39 +339,31 @@ export class Viewer { }(); loadFile(fileURL, options = {}) { - if (options.position) options.position = new THREE.Vector3().fromArray(options.position); - if (options.orientation) options.orientation = new THREE.Quaternion().fromArray(options.orientation); - options.splatAlphaRemovalThreshold = options.splatAlphaRemovalThreshold || 1; - options.halfPrecisionCovariancesOnGPU = !!options.halfPrecisionCovariancesOnGPU; if (options.showLoadingSpinner !== false) options.showLoadingSpinner = true; - - if (options.showLoadingSpinner) this.loadingSpinner.show(); - const downloadProgress = (percent, percentLabel) => { - if (options.showLoadingSpinner) { - if (percent == 100) { - this.loadingSpinner.setMessage(`Download complete!`); - } else { - const suffix = percentLabel ? `: ${percentLabel}` : `...`; - this.loadingSpinner.setMessage(`Downloading${suffix}`); - } - } - if (options.onProgress) options.onProgress(percent, percentLabel, 'downloading'); - }; - return new Promise((resolve, reject) => { - let fileLoadPromise; - if (fileURL.endsWith('.splat')) { - fileLoadPromise = new SplatLoader().loadFromURL(fileURL, downloadProgress); - } else if (fileURL.endsWith('.ply')) { - fileLoadPromise = new PlyLoader().loadFromURL(fileURL, downloadProgress, 0, options.splatAlphaRemovalThreshold); - } else { - reject(new Error(`Viewer::loadFile -> File format not supported: ${fileURL}`)); - } - fileLoadPromise + if (options.showLoadingSpinner) this.loadingSpinner.show(); + const downloadProgress = (percent, percentLabel) => { + if (options.showLoadingSpinner) { + if (percent == 100) { + this.loadingSpinner.setMessage(`Download complete!`); + } else { + const suffix = percentLabel ? `: ${percentLabel}` : `...`; + this.loadingSpinner.setMessage(`Downloading${suffix}`); + } + } + if (options.onProgress) options.onProgress(percent, percentLabel, 'downloading'); + }; + this.loadFileToSplatBuffer(fileURL, options.splatAlphaRemovalThreshold, downloadProgress) .then((splatBuffer) => { if (options.showLoadingSpinner) this.loadingSpinner.hide(); if (options.onProgress) options.onProgress(0, '0%', 'processing'); - this.loadSplatBuffer(splatBuffer, options).then(() => { + const splatBufferOptions = { + 'rotation': options.rotation || options.orientation, + 'position': options.position, + 'scale': options.scale, + 'splatAlphaRemovalThreshold': options.splatAlphaRemovalThreshold, + }; + this.loadSplatBuffersIntoMesh([splatBuffer], [splatBufferOptions], options.showLoadingSpinner).then(() => { if (options.onProgress) options.onProgress(100, '100%', 'processing'); resolve(); }); @@ -362,45 +374,137 @@ export class Viewer { }); } - loadSplatBuffer(splatBuffer, options) { - if (options.showLoadingSpinner !== false) options.showLoadingSpinner = true; - return new Promise((resolve) => { - if (options.showLoadingSpinner) { - this.loadingSpinner.show(); - this.loadingSpinner.setMessage(`Processing splats...`); + loadFiles(files, showLoadingSpinner = true, onProgress = undefined) { + return new Promise((resolve, reject) => { + const fileCount = files.length; + const percentComplete = []; + if (showLoadingSpinner) this.loadingSpinner.show(); + const downloadProgress = (fileIndex, percent, percentLabel) => { + percentComplete[fileIndex] = percent; + let totalPercent = 0; + for (let i = 0; i < fileCount; i++) totalPercent += percentComplete[i] || 0; + totalPercent = totalPercent / fileCount; + percentLabel = `${totalPercent.toFixed(2)}%`; + if (showLoadingSpinner) { + if (totalPercent == 100) { + this.loadingSpinner.setMessage(`Download complete!`); + } else { + this.loadingSpinner.setMessage(`Downloading: ${percentLabel}`); + } + } + if (onProgress) onProgress(totalPercent, percentLabel, 'downloading'); + }; + + const downLoadPromises = []; + for (let i = 0; i < files.length; i++) { + const meshOptionsForFile = files[i] || {}; + const downloadPromise = this.loadFileToSplatBuffer(files[i].path, meshOptionsForFile.splatAlphaRemovalThreshold, + downloadProgress.bind(this, i)); + downLoadPromises.push(downloadPromise); } - window.setTimeout(() => { - this.setupSplatMesh(splatBuffer, options.splatAlphaRemovalThreshold, options.position, - options.orientation, options.halfPrecisionCovariancesOnGPU, - this.devicePixelRatio, this.gpuAcceleratedSort); - this.setupSortWorker(splatBuffer).then(() => { - if (options.showLoadingSpinner) this.loadingSpinner.hide(); + + Promise.all(downLoadPromises) + .then((splatBuffers) => { + if (showLoadingSpinner) this.loadingSpinner.hide(); + if (onProgress) options.onProgress(0, '0%', 'processing'); + this.loadSplatBuffersIntoMesh(splatBuffers, files, showLoadingSpinner).then(() => { + if (onProgress) onProgress(100, '100%', 'processing'); resolve(); }); - }, 1); + }) + .catch((e) => { + reject(new Error(`Viewer::loadFiles -> Could not load one or more files.`)); + }); + }); + } + + loadFileToSplatBuffer(fileURL, plySplatAlphaRemovalThreshold = 1, onProgress = undefined) { + const downloadProgress = (percent, percentLabel) => { + if (onProgress) onProgress(percent, percentLabel, 'downloading'); + }; + return new Promise((resolve, reject) => { + let fileLoadPromise; + if (fileURL.endsWith('.splat')) { + fileLoadPromise = new SplatLoader().loadFromURL(fileURL, downloadProgress); + } else if (fileURL.endsWith('.ply')) { + fileLoadPromise = new PlyLoader().loadFromURL(fileURL, downloadProgress, 0, plySplatAlphaRemovalThreshold); + } else { + reject(new Error(`Viewer::loadFileToSplatBuffer -> File format not supported: ${fileURL}`)); + } + fileLoadPromise + .then((splatBuffer) => { + resolve(splatBuffer); + }) + .catch(() => { + reject(new Error(`Viewer::loadFileToSplatBuffer -> Could not load file ${fileURL}`)); + }); }); } - setupSplatMesh(splatBuffer, splatAlphaRemovalThreshold = 1, position = new THREE.Vector3(), quaternion = new THREE.Quaternion(), - halfPrecisionCovariancesOnGPU = false, devicePixelRatio = 1, gpuAcceleratedSort = true) { - const splatCount = splatBuffer.getSplatCount(); - console.log(`Splat count: ${splatCount}`); + loadSplatBuffersIntoMesh = function() { - this.splatMesh = SplatMesh.buildMesh(splatBuffer, this.renderer, splatAlphaRemovalThreshold, - halfPrecisionCovariancesOnGPU, devicePixelRatio, gpuAcceleratedSort); - this.splatMesh.position.copy(position); - this.splatMesh.quaternion.copy(quaternion); - this.splatMesh.frustumCulled = false; - this.updateSplatMeshUniforms(); + let loadPromise; + let loadCount = 0; + + return function(splatBuffers, splatBufferOptions = [], showLoadingSpinner = true) { + this.splatRenderingInitialized = false; + loadCount++; + const performLoad = () => { + return new Promise((resolve) => { + if (showLoadingSpinner) { + this.loadingSpinner.show(); + this.loadingSpinner.setMessage(`Processing splats...`); + } + window.setTimeout(() => { + if (this.sortWorker) this.sortWorker.terminate(); + this.sortWorker = null; + this.sortRunning = false; + this.updateSplatMesh(splatBuffers, splatBufferOptions); + this.setupSortWorker(this.splatMesh).then(() => { + loadCount--; + if (loadCount === 0) { + if (showLoadingSpinner) this.loadingSpinner.hide(); + this.splatRenderingInitialized = true; + this.updateView(true, true); + } + resolve(); + }); + }, 1); + }); + }; + if (!loadPromise) { + loadPromise = performLoad(); + } else { + loadPromise = loadPromise.then(() => { + return performLoad(); + }); + } + return loadPromise; + }; + + }(); + updateSplatMesh(splatBuffers, splatBufferOptions) { + if (!this.splatMesh) { + this.splatMesh = new SplatMesh(this.halfPrecisionCovariancesOnGPU, this.devicePixelRatio, this.gpuAcceleratedSort); + } + const allSplatBuffers = this.splatMesh.splatBuffers || []; + const allSplatBufferOptions = this.splatMesh.splatBufferOptions || []; + allSplatBuffers.push(...splatBuffers); + allSplatBufferOptions.push(...splatBufferOptions); + this.splatMesh.build(allSplatBuffers, allSplatBufferOptions); + if (this.renderer) this.splatMesh.setRenderer(this.renderer); + const splatCount = this.splatMesh.getSplatCount(); + console.log(`Total splat count: ${splatCount}`); + this.splatMesh.frustumCulled = false; this.splatRenderCount = splatCount; } - setupSortWorker(splatBuffer) { + setupSortWorker(splatMesh) { return new Promise((resolve) => { - const splatCount = splatBuffer.getSplatCount(); - this.sortWorker = createSortWorker(splatCount); - this.sortWorker.onmessage = (e) => { + const splatCount = splatMesh.getSplatCount(); + const sortWorker = createSortWorker(splatCount); + sortWorker.onmessage = (e) => { if (e.data.sortDone) { this.sortRunning = false; this.splatMesh.updateIndexes(this.sortWorkerSortedIndexes, e.data.splatRenderCount); @@ -409,26 +513,25 @@ export class Viewer { this.sortRunning = false; } else if (e.data.sortSetupPhase1Complete) { console.log('Sorting web worker WASM setup complete.'); - this.sortWorker.postMessage({ + sortWorker.postMessage({ 'centers': this.splatMesh.getIntegerCenters(true).buffer }); this.sortWorkerSortedIndexes = new Uint32Array(e.data.sortedIndexesBuffer, - e.data.sortedIndexesOffset, splatBuffer.getSplatCount()); + e.data.sortedIndexesOffset, splatCount); this.sortWorkerIndexesToSort = new Uint32Array(e.data.indexesToSortBuffer, - e.data.indexesToSortOffset, splatBuffer.getSplatCount()); + e.data.indexesToSortOffset, splatCount); this.sortWorkerPrecomputedDistances = new Int32Array(e.data.precomputedDistancesBuffer, - e.data.precomputedDistancesOffset, splatBuffer.getSplatCount()); + e.data.precomputedDistancesOffset, splatCount); for (let i = 0; i < splatCount; i++) this.sortWorkerIndexesToSort[i] = i; } else if (e.data.sortSetupComplete) { console.log('Sorting web worker ready.'); - this.splatMesh.updateIndexes(this.sortWorkerSortedIndexes, splatBuffer.getSplatCount()); + this.splatMesh.updateIndexes(this.sortWorkerSortedIndexes, splatCount); 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); - this.updateView(true, true); - this.splatRenderingInitialized = true; + this.sortWorker = sortWorker; resolve(); } }; @@ -528,6 +631,65 @@ export class Viewer { } } + selfDrivenUpdate() { + if (this.selfDrivenMode) { + requestAnimationFrame(this.selfDrivenUpdateFunc); + } + this.update(); + this.render(); + } + + setRenderer(renderer) { + this.renderer = renderer; + if (this.splatMesh) this.splatMesh.setRenderer(this.renderer); + } + + setCamera(camera) { + this.camera = camera; + if (this.controls) this.controls.object = camera; + } + + update(renderer, camera) { + if (renderer) this.setRenderer(renderer); + if (camera) this.setCamera(camera); + if (this.initializeFromExternalUpdate) { + this.init(); + } + if (!this.initialized || !this.splatRenderingInitialized) return; + if (this.controls) this.controls.update(); + this.updateView(); + this.updateForRendererSizeChanges(); + this.updateSplatMeshUniforms(); + this.updateMeshCursor(); + this.updateFPS(); + this.timingSensitiveUpdates(); + this.updateInfo(); + this.updateControlPlane(); + } + + render = function() { + + return function() { + if (!this.initialized || !this.splatRenderingInitialized) return; + const hasRenderables = (scene) => { + for (let child of scene.children) { + if (child.visible) { + return true; + } + } + return false; + }; + const savedAuoClear = this.renderer.autoClear; + this.renderer.autoClear = false; + if (hasRenderables(this.scene)) this.renderer.render(this.scene, this.camera); + this.renderer.render(this.splatMesh, this.camera); + 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; + }; + + }(); + updateFPS = function() { let lastCalcTime = getCurrentTime(); @@ -559,36 +721,12 @@ export class Viewer { this.camera.aspect = currentRendererSize.x / currentRendererSize.y; this.camera.updateProjectionMatrix(); } - if (this.splatRenderingInitialized) { - this.updateSplatMeshUniforms(); - } lastRendererSize.copy(currentRendererSize); } }; }(); - selfDrivenUpdate() { - if (this.selfDrivenMode) { - requestAnimationFrame(this.selfDrivenUpdateFunc); - } - this.update(); - this.render(); - } - - update() { - if (this.controls) { - this.controls.update(); - } - this.updateView(); - this.updateForRendererSizeChanges(); - this.updateMeshCursor(); - this.updateFPS(); - this.timingSensitiveUpdates(); - this.updateInfo(); - this.updateControlPlane(); - } - timingSensitiveUpdates = function() { let lastUpdateTime; @@ -691,39 +829,38 @@ export class Viewer { const renderDimensions = new THREE.Vector2(); return function() { - if (this.showInfo) { - const splatCount = this.splatMesh.getSplatCount(); - this.getRenderDimensions(renderDimensions); + if (!this.showInfo) return; + const splatCount = this.splatMesh.getSplatCount(); + this.getRenderDimensions(renderDimensions); - const cameraPos = this.camera.position; - const cameraPosString = `[${cameraPos.x.toFixed(5)}, ${cameraPos.y.toFixed(5)}, ${cameraPos.z.toFixed(5)}]`; - this.infoPanelCells.cameraPosition.innerHTML = cameraPosString; + const cameraPos = this.camera.position; + const cameraPosString = `[${cameraPos.x.toFixed(5)}, ${cameraPos.y.toFixed(5)}, ${cameraPos.z.toFixed(5)}]`; + this.infoPanelCells.cameraPosition.innerHTML = cameraPosString; - const cameraLookAt = this.controls.target; - const cameraLookAtString = `[${cameraLookAt.x.toFixed(5)}, ${cameraLookAt.y.toFixed(5)}, ${cameraLookAt.z.toFixed(5)}]`; - this.infoPanelCells.cameraLookAt.innerHTML = cameraLookAtString; + const cameraLookAt = this.controls.target; + const cameraLookAtString = `[${cameraLookAt.x.toFixed(5)}, ${cameraLookAt.y.toFixed(5)}, ${cameraLookAt.z.toFixed(5)}]`; + this.infoPanelCells.cameraLookAt.innerHTML = cameraLookAtString; - const cameraUp = this.camera.up; - const cameraUpString = `[${cameraUp.x.toFixed(5)}, ${cameraUp.y.toFixed(5)}, ${cameraUp.z.toFixed(5)}]`; - this.infoPanelCells.cameraUp.innerHTML = cameraUpString; + const cameraUp = this.camera.up; + const cameraUpString = `[${cameraUp.x.toFixed(5)}, ${cameraUp.y.toFixed(5)}, ${cameraUp.z.toFixed(5)}]`; + this.infoPanelCells.cameraUp.innerHTML = cameraUpString; - if (this.showMeshCursor) { - const cursorPos = this.sceneHelper.meshCursor.position; - const cursorPosString = `[${cursorPos.x.toFixed(5)}, ${cursorPos.y.toFixed(5)}, ${cursorPos.z.toFixed(5)}]`; - this.infoPanelCells.cursorPosition.innerHTML = cursorPosString; - } else { - this.infoPanelCells.cursorPosition.innerHTML = 'N/A'; - } + if (this.showMeshCursor) { + const cursorPos = this.sceneHelper.meshCursor.position; + const cursorPosString = `[${cursorPos.x.toFixed(5)}, ${cursorPos.y.toFixed(5)}, ${cursorPos.z.toFixed(5)}]`; + this.infoPanelCells.cursorPosition.innerHTML = cursorPosString; + } else { + this.infoPanelCells.cursorPosition.innerHTML = 'N/A'; + } - this.infoPanelCells.fps.innerHTML = this.currentFPS; - this.infoPanelCells.renderWindow.innerHTML = `${renderDimensions.x} x ${renderDimensions.y}`; + this.infoPanelCells.fps.innerHTML = this.currentFPS; + this.infoPanelCells.renderWindow.innerHTML = `${renderDimensions.x} x ${renderDimensions.y}`; - const renderPct = this.splatRenderCount / splatCount * 100; - this.infoPanelCells.renderSplatCount.innerHTML = - `${this.splatRenderCount} splats out of ${splatCount} (${renderPct.toFixed(2)}%)`; + const renderPct = this.splatRenderCount / splatCount * 100; + this.infoPanelCells.renderSplatCount.innerHTML = + `${this.splatRenderCount} splats out of ${splatCount} (${renderPct.toFixed(2)}%)`; - this.infoPanelCells.sortTime.innerHTML = `${this.lastSortTime.toFixed(3)} ms`; - } + this.infoPanelCells.sortTime.innerHTML = `${this.lastSortTime.toFixed(3)} ms`; }; }(); @@ -737,29 +874,6 @@ export class Viewer { } } - render = function() { - - return function() { - const hasRenderables = (scene) => { - for (let child of scene.children) { - if (child.visible) { - return true; - } - } - return false; - }; - - const savedAuoClear = this.renderer.autoClear; - this.renderer.autoClear = false; - if (hasRenderables(this.scene)) this.renderer.render(this.scene, this.camera); - this.renderer.render(this.splatMesh, this.camera); - 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; - }; - - }(); - updateView = function() { const tempMatrix = new THREE.Matrix4(); @@ -847,4 +961,8 @@ export class Viewer { getSplatMesh() { return this.splatMesh; } + + isMobile() { + return navigator.userAgent.includes('Mobi'); + } } diff --git a/src/index.js b/src/index.js index 769fe29..d1247bf 100644 --- a/src/index.js +++ b/src/index.js @@ -3,11 +3,15 @@ import { PlyLoader } from './PlyLoader.js'; import { SplatLoader } from './SplatLoader.js'; import { SplatBuffer } from './SplatBuffer.js'; import { Viewer } from './Viewer.js'; +import { RenderableViewer } from './RenderableViewer.js'; +import { OrbitControls } from './OrbitControls.js'; export { PlyParser, PlyLoader, SplatLoader, SplatBuffer, - Viewer + Viewer, + RenderableViewer, + OrbitControls }; diff --git a/src/raycaster/Raycaster.js b/src/raycaster/Raycaster.js index fe48e65..68aee48 100644 --- a/src/raycaster/Raycaster.js +++ b/src/raycaster/Raycaster.js @@ -84,10 +84,12 @@ export class Raycaster { } if (node.data.indexes && node.data.indexes.length > 0) { for (let i = 0; i < node.data.indexes.length; i++) { - const splatIndex = node.data.indexes[i]; - splatTree.splatBuffer.getCenter(splatIndex, tempCenter); - splatTree.splatBuffer.getRotation(splatIndex, tempRotation); - splatTree.splatBuffer.getScale(splatIndex, tempScale); + const splatGlobalIndex = node.data.indexes[i]; + const splatLocalIndex = splatTree.getSplatLocalIndex(splatGlobalIndex); + const splatBuffer = splatTree.getSplatBufferForSplat(splatGlobalIndex); + const splatTransform = splatTree.getTransformForSplat(splatGlobalIndex); + splatBuffer.getCenter(splatLocalIndex, tempCenter, splatTransform); + splatBuffer.getScaleAndRotation(splatLocalIndex, tempScale, tempRotation, splatTransform); if (tempScale.x <= scaleEpsilon || tempScale.y <= scaleEpsilon || tempScale.z <= scaleEpsilon) { continue; @@ -102,6 +104,7 @@ export class Raycaster { // Raycast against actual splat ellipsoid ... doesn't actually work as well // as the approximated sphere approach /* + splatBuffer.getRotation(splatLocalIndex, tempRotation, splatTransform); tempScaleMatrix.makeScale(tempScale.x, tempScale.y, tempScale.z); tempRotationMatrix.makeRotationFromQuaternion(tempRotation); fromSphereSpace.copy(tempScaleMatrix).premultiply(tempRotationMatrix); diff --git a/src/splattree/SplatTree.js b/src/splattree/SplatTree.js index b312f31..7ebc4f4 100644 --- a/src/splattree/SplatTree.js +++ b/src/splattree/SplatTree.js @@ -6,39 +6,71 @@ export class SplatTree { constructor(maxDepth, maxCentersPerNode) { this.maxDepth = maxDepth; this.maxCentersPerNode = maxCentersPerNode; - this.splatBuffer = null; + this.splatMesh = []; this.sceneDimensions = new THREE.Vector3(); this.sceneMin = new THREE.Vector3(); this.sceneMax = new THREE.Vector3(); this.rootNode = null; this.addedIndexes = {}; this.nodesWithIndexes = []; + this.globalSplatIndexToLocalSplatIndexMap = {}; + this.globalSplatIndexToSplatBufferIndexMap = {}; } - processSplatBuffer(splatBuffer, filterFunc = () => true) { - this.splatBuffer = splatBuffer; - this.addedIndexes = {}; - this.nodesWithIndexes = []; - const splatCount = splatBuffer.getSplatCount(); + getSplatBufferForSplat(globalIndex) { + return this.splatMesh.splatBuffers[this.globalSplatIndexToSplatBufferIndexMap[globalIndex]]; + } + + getTransformForSplat(globalIndex) { + return this.splatMesh.splatTransforms[this.globalSplatIndexToSplatBufferIndexMap[globalIndex]]; + } + + getSplatLocalIndex(globalIndex) { + return this.globalSplatIndexToLocalSplatIndexMap[globalIndex]; + } + processSplatMesh(splatMesh, filterFunc = () => true) { const center = new THREE.Vector3(); - for (let i = 0; i < splatCount; i++) { - if (filterFunc(i)) { - splatBuffer.getCenter(i, center); - if (i === 0 || center.x < this.sceneMin.x) this.sceneMin.x = center.x; - if (i === 0 || center.x > this.sceneMax.x) this.sceneMax.x = center.x; - if (i === 0 || center.y < this.sceneMin.y) this.sceneMin.y = center.y; - if (i === 0 || center.y > this.sceneMax.y) this.sceneMax.y = center.y; - if (i === 0 || center.z < this.sceneMin.z) this.sceneMin.z = center.z; - if (i === 0 || center.z > this.sceneMax.z) this.sceneMax.z = center.z; + this.splatMesh = splatMesh; + this.sceneMin = new THREE.Vector3(); + this.sceneMax = new THREE.Vector3(); + this.addedIndexes = {}; + this.nodesWithIndexes = []; + this.globalSplatIndexToLocalSplatIndexMap = {}; + this.globalSplatIndexToSplatBufferIndexMap = {}; + + let totalSplatCount = 0; + let validSplatCount = 0; + for (let s = 0; s < this.splatMesh.splatBuffers.length; s++) { + const splatBuffer = this.splatMesh.splatBuffers[s]; + const splatCount = splatBuffer.getSplatCount(); + const transform = this.splatMesh.splatTransforms[s]; + for (let i = 0; i < splatCount; i++) { + if (filterFunc(s, splatBuffer, i, transform)) { + splatBuffer.getCenter(i, center, transform); + if (validSplatCount === 0 || center.x < this.sceneMin.x) this.sceneMin.x = center.x; + if (validSplatCount === 0 || center.x > this.sceneMax.x) this.sceneMax.x = center.x; + if (validSplatCount === 0 || center.y < this.sceneMin.y) this.sceneMin.y = center.y; + if (validSplatCount === 0 || center.y > this.sceneMax.y) this.sceneMax.y = center.y; + if (validSplatCount === 0 || center.z < this.sceneMin.z) this.sceneMin.z = center.z; + if (validSplatCount === 0 || center.z > this.sceneMax.z) this.sceneMax.z = center.z; + validSplatCount++; + } + this.globalSplatIndexToLocalSplatIndexMap[totalSplatCount] = i; + this.globalSplatIndexToSplatBufferIndexMap[totalSplatCount] = s; + totalSplatCount++; } } this.sceneDimensions.copy(this.sceneMin).sub(this.sceneMin); const indexes = []; - for (let i = 0; i < splatCount; i ++) { - if (filterFunc(i)) { + for (let i = 0; i < totalSplatCount; i ++) { + const splatLocalIndex = this.getSplatLocalIndex(i); + const splatBufferIndex = this.globalSplatIndexToSplatBufferIndexMap[i]; + const splatBuffer = this.getSplatBufferForSplat(i); + const transform = this.getTransformForSplat(i); + if (filterFunc(splatBufferIndex, splatBuffer, splatLocalIndex, transform)) { indexes.push(i); } } @@ -46,10 +78,10 @@ export class SplatTree { this.rootNode.data = { 'indexes': indexes }; - this.processNode(this.rootNode, splatBuffer); + this.processNode(this.rootNode, splatMesh); } - processNode(node, splatBuffer) { + processNode(node, splatMesh) { const splatCount = node.data.indexes.length; if (splatCount < this.maxCentersPerNode || node.depth > this.maxDepth) { @@ -103,12 +135,15 @@ export class SplatTree { const center = new THREE.Vector3(); for (let i = 0; i < splatCount; i++) { - const splatIndex = node.data.indexes[i]; - splatBuffer.getCenter(splatIndex, center); + const splatGlobalIndex = node.data.indexes[i]; + const splatLocalIndex = this.getSplatLocalIndex(splatGlobalIndex); + const splatBuffer = this.getSplatBufferForSplat(splatGlobalIndex); + const transform = this.getTransformForSplat(splatGlobalIndex); + splatBuffer.getCenter(splatLocalIndex, center, transform); for (let j = 0; j < childrenBounds.length; j++) { if (childrenBounds[j].containsPoint(center)) { splatCounts[j]++; - baseIndexes[j].push(splatIndex); + baseIndexes[j].push(splatGlobalIndex); } } } @@ -123,7 +158,7 @@ export class SplatTree { node.data = {}; for (let child of node.children) { - this.processNode(child, splatBuffer); + this.processNode(child, splatMesh); } }